树状数组

树状数组(Fenwick Tree / Binary Indexed Tree, BIT)是一种维护前缀和的数据结构,支持在 O(log n) 时间内进行单点更新和前缀查询。凭借其代码量小、常数极小的优势,它是竞赛和实际开发中的利器。以下是树状数组的常见用法及对应的C++实现。


0. 基础模板

核心操作:

  • add(i, delta): 将位置 i 的元素增加 delta,同时维护所有受影响的树状数组节点。
  • sum(i): 查询前缀和 [1, i]。
  • range_sum(l, r): 查询区间和 [l, r] = sum(r) - sum(l-1)。

注意: 树状数组的下标通常从 1 开始,因此需要将输入数据映射到 1-indexed。

template<typename T>
struct BIT {
    int n;
    vector<T> tree;
    BIT(int n) : n(n), tree(n + 1, 0) {}
    void add(int i, T delta) {
        while (i <= n) {
            tree[i] += delta;
            i += i & -i;
        }
    }
    T sum(int i) {
        T res = 0;
        while (i > 0) {
            res += tree[i];
            i -= i & -i;
        }
        return res;
    }
    T range_sum(int l, int r) { return sum(r) - sum(l - 1); }
};

1. 单点修改,区间查询

这是树状数组最经典的应用场景。
操作:

  • add(pos, val) 将原数组 a[pos] 的值增加 val
  • query(l, r) 返回 a[l..r] 的元素和。
BIT<int> bit(n);
bit.add(x, v);          // a[x] += v
int ans = bit.range_sum(l, r);

2. 区间修改,单点查询

利用差分思想,将区间修改转化为单点修改。
设差分数组 d[i] = a[i] - a[i-1],则 a[pos] = Σ d[1..pos]
对区间 [l, r] 加上 val,只需 d[l] += val; d[r+1] -= val

BIT<int> bit(n);
// 区间 [l, r] 全部加 val
bit.add(l, val);
bit.add(r + 1, -val);
// 查询 a[pos] 的当前值
int cur = bit.sum(pos);

3. 区间修改,区间查询

同样是基于差分,但需要维护两个树状数组。

推导:
Σa[1..p] = Σ(i=1..p) Σ(j=1..i) d[j] = (p+1) * Σd[1..p] - Σ(i*d[i])
因此需要两个 BIT,一个维护 d[i],另一个维护 i * d[i]
区间修改操作:对 d[l] += val, d[r+1] -= val
同时 i*d[i] 的 BIT 也要做相应加法。
区间查询 [l, r] = prefix(r) - prefix(l-1)

struct RangeBIT {
    int n;
    BIT<long long> bit1, bit2; // bit1: d[i], bit2: i*d[i]
    RangeBIT(int n) : n(n), bit1(n), bit2(n) {}
    void range_add(int l, int r, long long val) {
        bit1.add(l, val);
        bit1.add(r + 1, -val);
        bit2.add(l, l * val);
        bit2.add(r + 1, -(r + 1) * val);
    }
    long long prefix_sum(int p) {
        return (p + 1) * bit1.sum(p) - bit2.sum(p);
    }
    long long range_sum(int l, int r) {
        return prefix_sum(r) - prefix_sum(l - 1);
    }
};

4. 求逆序对

树状数组可以 O(n log maxVal) 求逆序对数。
基本思路: 将数组元素离散化(保证值域 1~n),从左到右扫描,每遇到一个数 x,查询树状数组中大于 x 的已出现元素个数(即 query(n) - query(x)),然后将 x 加入树状数组。

int count_inversions(vector<int>& a) {
    int n = a.size();
    // 离散化
    vector<int> sorted = a;
    sort(sorted.begin(), sorted.end());
    sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
    for (int& v : a) v = lower_bound(sorted.begin(), sorted.end(), v) - sorted.begin() + 1;

    BIT<int> bit(n);
    long long ans = 0;
    for (int i = 0; i < n; ++i) {
        ans += bit.range_sum(a[i] + 1, n); // 比 a[i] 大的个数
        bit.add(a[i], 1);
    }
    return ans;
}

5. 维护权值树状数组(第 k 小)

如果我们要动态查询集合中第 k 小的数(k 从 1 开始),可以使用权值树状数组二分查找。
利用树状数组的节点累加特性,通过倍增快速定位到第 k 小的位置。
复杂度: O(log N)。

int find_kth(BIT<int>& bit, int k) {
    int n = bit.n, idx = 0, bitmask = 1 << (int)log2(n);
    while (bitmask) {
        int t = idx + bitmask;
        if (t <= n && bit.tree[t] < k) {
            idx = t;
            k -= bit.tree[t];
        }
        bitmask >>= 1;
    }
    return idx + 1; // 1‑indexed
}
// 用法:bit 记录每个值出现的次数,动态 insert/remove。

6. 二维树状数组

支持在二维平面上进行单点更新和子矩阵求和。
操作: add(x, y, delta) 更新 (x,y) 的权值,query(x, y) 查询从 (1,1) 到 (x,y) 的子矩阵和。
通过容斥可得任意子矩阵和。

struct BIT2D {
    int n, m;
    vector<vector<int>> tree;
    BIT2D(int n, int m) : n(n), m(m), tree(n + 1, vector<int>(m + 1, 0)) {}
    void add(int x, int y, int delta) {
        for (int i = x; i <= n; i += i & -i)
            for (int j = y; j <= m; j += j & -j)
                tree[i][j] += delta;
    }
    int sum(int x, int y) {
        int res = 0;
        for (int i = x; i > 0; i -= i & -i)
            for (int j = y; j > 0; j -= j & -j)
                res += tree[i][j];
        return res;
    }
    int query(int x1, int y1, int x2, int y2) {
        return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1);
    }
};

7. 树上问题(DFS 序 + 树状数组)

将树按 DFS 序展开,子树对应一个连续区间,用树状数组维护区间和。
适用场景: 子树加法、子树求和,或链上操作(需配合树链剖分)。
实现: 第一次 DFS 记录每个节点的 in[x](进入时间戳)和 out[x](离开时间戳),子树节点区间为 [in[x], out[x]]

vector<vector<int>> adj;
vector<int> in, out;
int timer = 0;
void dfs(int u, int fa) {
    in[u] = ++timer;
    for (int v : adj[u])
        if (v != fa) dfs(v, u);
    out[u] = timer;
}

// 对子树 x 全体加 val
bit.range_add(in[x], out[x], val); // 需配合区间修改BIT
// 查询节点 x 的当前值
int val = bit.sum(in[x]); // 单点查询

9. 其他常见扩展用法

  • 离线处理区间不同值个数:将询问按右端点排序,树状数组维护每种颜色最后一次出现的位置(经典HH项链问题)。
  • 差分数组的 K 次多项式前缀和:涉及组合数的高阶前缀和需结合公式推导,常用多个 BIT 实现。
  • 三维偏序(CDQ分治):树状数组常作为内层查询结构。
  • 维护集合中大于/小于某个值的元素和(权值BIT):与第 k 小类似,可直接查询。

💡 总结

树状数组的核心是 单点更新和前缀查询,所有扩展都基于这两点通过数学变换得出。它的优点是常数极小、代码短,但功能不如线段树灵活。在面对需要动态维护前缀信息的场景时,BIT 往往是首选方案。

所有函数均封装在结构体中,使用时注意 下标从 1 开始,并根据需要将 int 改为 long long

posted @ 2026-05-09 16:19  miao-jc  阅读(42)  评论(0)    收藏  举报