普通树状数组

发布于:2025-08-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

树状数组

普通的树状数组维护的信息及运算要满足 结合律可差分,如加法、乘法、异或等。

Q1:为什么要满足 结合律

A1:说明要求的区间信息可能是由若干个已知区间信息拼凑起来的,故需要满足结合律。

Q2:为什么要满足 可差分

A2:说明要求的区间信息可能通过两个区间作差得出。

一维树状数组

不妨假设 aaa 是原始序列,以往我们要求 aaa 的区间 [l,r][l,r][l,r] 的信息,需要遍历区间。

树状数组的核心是维护 若干个块状 的信息,然后将这若干个块状信息 结合 为我们需要的区间信息。

也就是 一个位置表达更多的信息,这样才能达到减少时间复杂度的效果。

怎么选择块的大小呢?

已知任意一个正整数 iii 都能被写成 二进制 的形式。

不妨设 iii 的二进制可以写成 2k1+2k2+⋯+2km2^{k_1}+2^{k_2}+\cdots+2^{k_m}2k1+2k2++2kmk1<k2<⋯<kmk_1 <k_2<\cdots<k_mk1<k2<<km)。

这样可以看做一个长度为 iii 区间的信息 被分成了 mmm 信息。

所以,如果要求 1∼i1\sim i1i 的区间信息,只需要知道这 mmm 个块的信息就可以。

怎么维护这 mmm 块信息呢?

可以令 tree[i]tree[i]tree[i] 维护一个以 iii右端点大小为 2k12^{k_1}2k1 的区间信息。

此时就剩下了 m−1m-1m1 块区间。

j=i−2k1j=i-2^{k_1}j=i2k1,不难发现 jjj 的二进制表示恰好为 2k2+⋯+2km2^{k_2}+\cdots+2^{k_m}2k2++2km

继续令 tree[j]tree[j]tree[j] 维护一个以 jjj 为右端点,大小为 2k22^{k_2}2k2 的区间。

而对于一个位置,只令其维护其 二进制最小的 111 所代表的长度 的区间信息,这个值被称为 lowbitlowbitlowbit

不难看出,只需要遍历 mmmtreetreetree 的位置就能拼凑出 [1,i][1,i][1,i] 的区间信息。

而遍历的方向就是,不断地用 iii 减去其 lowbitlowbitlowbit

又因为 m≤log(i)m\le log(i)mlog(i),所以求出 [1,i][1,i][1,i] 的区间信息只需要 O(logn)O(logn)O(logn) 的复杂度。

于是得到了树状数组最核心的 状态设计

tree[i]tree[i]tree[i] 维护的是以 iii 为右端点,大小为 lowbit(i)lowbit(i)lowbit(i) 的区间信息。

区间查询

要求 [l,r][l,r][l,r] 的区间信息,可以先求出 [1,r],[1,l][1,r],[1,l][1,r],[1,l] 的区间信息,然后再作差。

而求出 [1,i][1,i][1,i] 的区间信息,需要从 iii 开始往下跳 log(i)log(i)log(i) 次,直到跳到 000

T getsum(int x) {
     T ans = 0;
     while (x) {
          ans += tree[x];
          x = x - lowbit(x);
     }
     return ans;
}

单点修改

要修改 a[x]a[x]a[x] 的值,就需要在树状数组中同步修改能管辖到 xxx 的所有位置。

因为 xxx 是最小的能管辖到 xxx 的位置,所以从 xxx 开始一直往上跳,直到跳出范围即可。

nnn 表示 aaa 数组的大小,不难写出单点修改 a[x]a[x]a[x] 的过程:

#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18

template<typename T>

// 单点修改和区间查询的树状数组
struct Fenwick {
     vector <T> tree;
     int n;

     Fenwick(int _n) : n(_n), tree(_n + 2){}

     // 求 1~x 的前缀和
     T getsum(int x) {
          T ans = 0;
          int L = x;
          while (x) {
               ans += tree[x];
               x = x - lowbit(x);
          }
          return ans;
     }

     // 区间查询
     T get_range_sum(int l, int r) {
          return getsum(r) - getsum(l - 1);
     }

     // 单点修改,是为了 range_add 服务
     void add(int x, T k) {
          while (x <= n) {
               tree[x] += k;
               x += lowbit(x);
          }
     }

     static inline int lowbit(int x) {
          return x & (-x);
     }
};
void slove () {
	Fenwick<int> tree(n);
}

signed main () {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    slove();
}

带区间加的区间和

如何给区间 [l,r][l,r][l,r] 都加上一个常数 kkk

显然不能使用 r−l+1r-l+1rl+1 次单点修改,这样时间复杂度比直接遍历还劣。

考虑到实现单点修改是 O(logn)O(logn)O(logn),实现区间查询是 O(logn)O(logn)O(logn)

不妨 用单点修改来代替区间加

给区间加上一个常数可以用 差分 解决,即给 lll 处加 kkkr+1r+1r+1 处减 kkk

如果此时再去求区间和,应该怎么办?

考虑查询 [1,l][1,l][1,l] 的区间和,则有
∑i=1l∑j=1idj \sum_{i=1}^{l}\sum_{j=1}^{i}d_j i=1lj=1idj
等价于
∑i=1ldi×(l−i+1)=(l+1)∑i=1l×di−∑i=1li×di \sum_{i=1}^{l}d_i\times(l-i+1)=(l+1)\sum_{i=1}^{l}\times d_i-\sum_{i=1}^{l}i\times d_i i=1ldi×(li+1)=(l+1)i=1l×dii=1li×di
能转换为 ∑i=1ldi×(l−i+1)\sum_{i=1}^{l}d_i\times(l-i+1)i=1ldi×(li+1) 是因为每个 did_idi 都被加了 l−i+1l-i+1li+1 次。

然后拆开括号得到 (l+1)∑i=1l×di(l+1)\sum_{i=1}^{l}\times d_i(l+1)i=1l×di∑i=1li×di\sum_{i=1}^{l}i\times d_ii=1li×di

拆开是因为一个树状数组是 无法同时维护 包含 两个变量 l,il,il,i 的表达式。

此时就只需要维护 did_idici=i×dic_i=i\times d_ici=i×di 这两个数组。

#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18

template<typename T>

// 区间修改 + 区间查询
struct Fenwick {
     vector <T> tree_d, tree_dx;
     int n;

     Fenwick(int _n) : n(_n), tree_d(_n + 2), tree_dx(_n + 2){}

     // 求 1~x 的前缀和
     T getsum(int x) {
          T ans1 = 0, ans2 = 0;
          int L = x;
          while (x) {
               ans1 += tree_d[x];
               ans2 += tree_dx[x];
               x = x - lowbit(x);
          }
          return (L + 1) * ans1 - ans2;
     }

     // 暴露在外的接口,求范围值
     T get_range_sum(int l, int r) {
          return getsum(r) - getsum(l - 1);
     }

     // 单点修改,是为了 range_add 服务
     void add(int x, T k) {
          while (x <= n) {
               tree_d[x] += k;
               tree_dx[x] += k * x;
               x += lowbit(x);
          }
     }

     // 暴露在外的接口,给区间 [l, r] 加上 x
     void range_add(int l, int r, T x) {
          add(l, x);
          add(r + 1, -x);
     }

     static inline int lowbit(int x) {
          return x & (-x);
     }
};
void slove () {
	Fenwick<int> tree(n);
}

signed main () {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    slove();
}


网站公告

今日签到

点亮在社区的每一天
去签到