C++ 算法竞赛:树状数组与线段树详解及实战

Leo
Leo
发布时间:2026-10-16 | 4人浏览过
原创

面对频繁的单点修改与区间求和问题,传统前缀和方法在数据更新时会超时。本文将深入解析 C++ 中高效的树状数组与线段树,对比其底层逻辑与性能表现,并提供可直接运行的标准代码模板及典型应用场景。

树状数组(BIT)

树状数组是一种利用二进制特性来高效维护前缀和的数据结构,它在单点修改和前缀查询操作上的时间复杂度均为 O(log n),且常数极小,空间占用少。

第1步:理解树状数组的结构与 lowbit 运算

树状数组的核心在于如何快速定位父节点和子节点,这依赖于 lowbit(x) 函数。它返回整数 x 在二进制表示下最低位的 1 所对应的值(即最后一个 1 及其后的 0)。

lowbit(x) = x & (-x)

例如,若 x = 6 (二进制 0110),则 lowbit(6) = 2 (二进制 0010)。这个运算使得我们可以快速找到区间覆盖关系。

第2步:实现核心操作 add 与 query

树状数组通常用数组 tr[] 存储,tr[i] 代表原数组中一段区间的和。具体范围由 i - lowbit(i) + 1 到 i 决定。

  • add(x, v):将位置 x 的值增加 v。因为修改一个元素会影响包含它的所有区间和,所以需要沿着父节点路径向上更新。路径为 x -> x + lowbit(x) -> ...。
  • query(x):计算前缀 [1, x] 的和。通过将区间分解为树状数组中若干个节点的和来计算。路径为 x -> x - lowbit(x) -> ...。
int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}

int query(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

第3步:完整应用——动态求连续区间和

题目要求在数列上进行两种操作: 1. 修改某个元素的值。 2. 查询某段子数列 [a, b] 的连续和。

如果直接使用普通前缀和,每次修改元素后需要重新计算后续所有前缀和,时间复杂度为 O(n),在大规模数据下会超时。而树状数组可以在 O(log n) 时间内完成这两类操作。

注意:树状数组通常基于 1-based 索引,因此在读取输入和处理时需确保索引正确。

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int n, m;
int a[N], tr[N]; // tr[N] 用于存储树状数组状态

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}

int query(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    // 初始化:先将所有初始值加入树状数组
    for (int i = 1; i <= n; i ++ ) 
    {
        scanf("%d", &a[i]);
        add(i, a[i]);
    }

    while (m -- )
    {
        int k, x, y;
        scanf("%d%d%d", &k, &x, &y);
        if (k == 0) // 查询区间 [x, y] 的和
            printf("%d\n", query(y) - query(x - 1));
        else add(x, y); // 将第 x 个数加上 y(注意题目语义是加值而非赋值,若为赋值需先减旧值)
    }

    return 0;
}

第4步:树状数组与普通前缀和的选择

普通前缀和是离线算法,支持 O(1) 查询但不支持动态修改(修改代价高);树状数组是在线算法,支持动态修改且查询效率高。当数据静态不变时首选前缀和;当涉及频繁的混合修改与查询时,树状数组是更优解。

第5步:进阶应用——统计左下方点数(二维偏序简化版)

例题:“数星星”。给定 N 个点的坐标,定义每个点的等级为其左下方(含正左、正下)的点的数目。输出各级别点的数量。

由于输入数据已经按 y 坐标递增、y 相同按 x 递增排序,因此我们只需考虑 x 坐标。当前点输入的之前所有点,其 y 坐标一定小于等于当前点 y 坐标。因此,问题转化为:查询当前点 x 坐标之前有多少个点已经被插入过。这正是树状数组擅长的单点插入与前缀查询。

算法流程: 1. 依次读入每个点的 x, y。

2. 实时查询当前 x 之前的总点数:level[sum(x)]++。 3. 将当前 x 插入树状数组:add(x)。 4. 最后输出 level[0]` 到 `level[n-1] 的统计结果。
#include <bits/stdc++.h>
using namespace std;

const int N = 32010;
int n;
int tr[N], level[N]; // level 记录各等级的星星数量

int lowbit(int x)
{
    return x & -x;
}

// 注意这里 add 默认加 1
void add(int x)
{
    for (int i = x; i < N; i += lowbit(i)) tr[i] ++ ;
}

int sum(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int x, y;
        scanf("%d%d", &x, &y);
        x ++ ; // 树状数组要求坐标从 1 开始,防止 lowbit(0) 死循环
        level[sum(x)] ++ ; // 统计当前等级
        add(x); // 插入该 x 坐标
    }

    for (int i = 0; i < n; i ++ ) printf("%d\n", level[i]);

    return 0;
}

线段树(Segment Tree)

线段树是基于分治思想的二叉树结构,每个节点表示一个区间。它可以灵活地处理区间查询与区间/单点修改,不仅限于求和,还可以用于求最大值、最小值等。

第1步:基础操作 pushup 与 build

线段树通常使用结构体或数组存储,数组大小一般开 4N 以容纳所有节点。

  • pushup(u):由子节点信息更新父节点。例如求和时,tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum。
  • build(u, l, r):递归构建线段树。叶子节点 (l==r) 直接赋值,内部节点先递归左右子树,再调用 pushup。
struct Node
{
    int l, r;//左右端点区域
    int sum;//存储区间和
}tr[N * 4]; // 空间至少开 4N

void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[r]};
    else
    {
        tr[u] = {l, r};
        int mid = (l + r) >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

第2步:单点修改 modify/change

单点修改是指改变区间内某个特定位置的值。递归找到叶子节点进行修改,回溯过程中通过 pushup 更新经过的父节点信息。

void change(int u, int x, int v)
{
    if(tr[u].l == x && tr[u].r == x) 
    {
        tr[u].sum += v; // 题目需求是加法修改
    }
    else
    {
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(x <= mid) change(u << 1, x, v);
        else change(u << 1 | 1, x, v);
        pushup(u);
    }
}

第3步:区间查询 query

查询区间 [l, r] 的信息。若当前节点区间完全包含在目标区间内,直接返回值;否则根据中点 mid 判断递归方向。需注意,如果查询范围跨越了中点,则需要分别查询左右子树并合并结果(如求和需相加,求 max 需取较大值)。

int query(int u, int l, int r)
{
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    else
    {
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(r <= mid) return query(u << 1, l, r);          // 完全在左子树
        else if(l > mid) return query(u << 1 | 1, l, r); // 完全在右子树
        else
        {
            // 跨越中点,需合并结果
            int left = query(u << 1, l, r);
            int right = query(u << 1 | 1, l, r);
            return left + right;
        }
    }
}

第4步:实战演练——动态求连续区间和完整版

结合以上组件,以下是完整的单点修改区间求和代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int n, m;
int w[N];

struct Node
{
    int l, r;
    int sum;
}tr[N * 4];

void pushup(int u){ tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; }

void build(int u, int l, int r)
{
    if(l == r) tr[u] = {l, r, w[r]};
    else
    {
        tr[u] = {l, r};
        int mid = (l + r) >> 1;
        build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

void change(int u, int x, int v)
{
    if(tr[u].l == x && tr[u].r == x) 
    {
        tr[u].sum += v;
    }
    else
    {
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(x <= mid) change(u << 1, x, v);
        else change(u << 1 | 1, x, v);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    int mid = (tr[u].l + tr[u].r) >> 1;
    if(r <= mid) return query(u << 1, l, r);
    if(l > mid) return query(u << 1 | 1, l, r);
    return query(u << 1, l, r) + query(u << 1 | 1, l, r);
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> w[i];
    build(1, 1, n);
    while(m --)
    {
        int op, l, r;
        cin >> op >> l >> r;
        if(op == 0) cout << query(1, l, r) << endl;
        else change(1, l, r);
    }
    return 0;
}

第5步:变式——数列区间最大值

仅需修改结构体中的存储变量名(如改为 maxv)以及 pushup 逻辑。此时 pushup 不再求和,而是取子节点的最大值。query 中的合并逻辑也相应改为取最大值。

struct Node
{
    int l, r;
    int maxv;
}tr[4 * N];

// 仅展示关键变化
int pushup(Node &t, int l, int r)
{
    return t.maxv = max(tr[l].maxv, tr[r].maxv);
}

// 查询时取 max
if(r <= mid) maxv = query(u << 1, l, r);
else if(l > mid) maxv = query(u << 1 | 1, l, r);
else maxv = max(query(u << 1, l, r), query(u << 1 | 1, l, r));

以上就是 C++ 语言中关于树状数组与线段树的详细内容,更多关于 C++ 数据结构优化的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表