【笔记】 对不起,我不会树状数组
一些奇怪的树状数组技巧
先来个几个比较常用的:
\(O(n)\) 初始化树状数组
不要做 \(O(n)\) 次单点修改,类似拓扑排序从 \(1 \to n\) 一个一个向直接祖先上传答案就行了。
\(O(1)\) 清空树状数组
常见于点分治题。
做法是给每个树状数组的节点设置一个时间戳 \(t_i\),并记录当前时间戳 \(Time\)。
- 如果当前时间戳与节点时间戳相同,那么该节点的值是正确的;
- 如果不同,这个位置上的值将被重置为 \(0\),并更新时间戳 \(t_i \gets Time\)。
想清空时直接 \(Time\gets Time+1\) 就行了。
树状数组二分
例题!
支持单点修改,查询区间最后一个前缀和 \(\le k\) 的位置。
只需对 bit 代码稍作修改即可。
class BIT{
int data[MX]
void add(int pos ,int val){
while(pos <= MX){
data[pos] += val;
pos += pos & -pos;
}
}
int lower_bound(int s){
int sum = 0 ,Ans = 0;
for(int i = lg2[MX] ; ~i ; --i){
Ans += 1 << i;
if(Ans >= MX || sum + data[Ans] > s) Ans -= 1 << i;
else sum += data[Ans];
}return Ans + 1;
}
}C;
复杂度 \(O(\log n)\)。
树状数组区间加区间求和
这是一个经典的问题,但新手(指我自己)往往无法想到如何解决。
树状数组维护区间加,单点求和是这个问题的弱化版,做法是维护原数组的差分数组 \(d\)。
一次区间加只要修改差分数组的 \(2\) 个位置。
一次单点查询只要查询差分数组的前缀和即可。
那么区间和怎么求呢?显然我们可以做 \(O(n)\) 次单点求值。
这样就得到了数组的原前缀和,两个前缀和相减就是区间和。
不过这个式子显然可以化简为:
你非常惊讶地发现只要维护两个树状数组,一个存 \(\sum d_i\),另一个存 \(\sum i \times d_i\) 就可以了。
例题!
维护一个数组 \(A\),支持:
- 区间加 \(1\);
- 区间求 \(\sum\limits_{i=l}^{r} i \times A_i\)。
这相当于是区间加等差数列,不妨考虑维护二阶差分序列 \(dd\)。再通过二阶前缀和的方法求出来原数组。
自然,你又只需要三个树状数组,分别维护 \(\sum dd_k\),\(\sum k \times dd_k\),\(\sum k^2 \times dd_k\) 就行啦!
不过常数好像有点大呢(笑)
一份示例代码
struct BIT{
LL data[MX];
void add(int x ,LL v){
while(x < MX){
data[x] = (data[x] + v + MOD) % MOD;
x += x & -x;
}
}
LL sum(int x){
LL s = 0;
while(x > 0){
s = (s + data[x]) % MOD;
x -= x & -x;
}
return s;
}
};
struct BIT3{
BIT A ,B ,C;
void add_d(int x ,LL v){
A.add(x ,v);
B.add(x ,x * v % MOD);
C.add(x ,x * v % MOD * x % MOD);
}
void add(int l ,int r ,LL v){
add_d(l ,l * v % MOD);
add_d(l + 1 ,(1 - l + MOD) * v % MOD);
add_d(r + 1 ,(MOD - r - 1) * v % MOD);
add_d(r + 2 ,r * v % MOD);
}
LL sum(LL x){
LL ans = (x * x + 3 * x + 2) % MOD * A.sum(x) % MOD;
ans = (ans - (3 + 2 * x) % MOD * B.sum(x) % MOD + MOD) % MOD;
ans = (ans + C.sum(x)) % MOD;
ans = (ans * iv) % MOD;
// iv 指的是 2 的逆元
return ans;
}
LL sum(int l ,int r){
return (sum(r) - sum(l - 1) + MOD) % MOD;
}
}C3;
树状数组单点修改区间最值
前排提醒:复杂度只能做到修改 \(O(\log ^2n)\),查询 \(O(\log ^2 n)\),比较鸡肋。
修改操作
类比维护区间和时候的操作,一个节点 \(k\) 维护的是 \(b_k = \sum\limits_{i=k-lowbit(k)+1}^{k} a_i\)。
维护区间最值时,一个节点 \(k\) 维护的就是 \(b_k = \max\limits_{i=k-lowbit(k)+1}^{k}a_i\)。
但,区间最值与区间和的区别就在于取最值没有逆运算,不可以通过差分的方式得到区间的答案。
但好在,我们依然可以让树状数组像线段树,通过子树的信息更新当前节点的信息。
树状数组上一个节点有最多 \(O(\log n)\) 个孩子(不知道为什么的 emmm,建议重学 BIT),单点修改最多会更新 \(O(\log n)\) 个节点。复杂度 \(O(\log^2 n)\)。
查询操作
此时我们访问 \(b\) 数组的复杂度是 \(O(1)\) 的,所以不要有什么忌惮啦!
显然最值操作不能差分。
有一种想法是像线段树那样提取出树状数组上的一些节点,把它们的答案拼成整体答案。
我们直接实现这个做法就可以了,假设询问区间为 \([l,r]\),设查询函数为 int qmax(int l ,int r):
- 如果 \(r-lowbit(r)<l-1\),返回 \(\max(a_r,\operatorname{qmax}(l,r-1))\)。
- 否则返回 \(\max(b_r , \operatorname{qmax}(l,r-lowbit(r)))\)。
容易发现,我们会在 \(O(\log n)\) 次递归后将最高位降低至少一位,所以复杂度是 \(O(\log^2 n)\)。
代码
struct BIT{
int data[MX] ,a[MX];
void set(int x ,int v){
a[x] = v;
while(x < MX){
data[x] = v;
for(int i = 1 ; i < (x & -x) ; i <<= 1){
data[x] = std::max(data[x] ,data[x - i]);
}
x += x & -x;
}
}
int qmax(int l ,int r){
int ans = INT_MIN;
while(l <= r){
if(r - (r & -r) < l - 1){
ans = std::max(ans ,a[r]);
--r;
}
else{
ans = std::max(ans ,data[r]);
r -= r & -r;
}
}
return ans;
}
}C;
随时更新……