一些奇怪的树状数组技巧
先来个几个比较常用的:
(O(n)) 初始化树状数组
不要做 (O(n)) 次单点修改,类似拓扑排序从 (1 o n) 一个一个向直接祖先上传答案就行了。
(O(1)) 清空树状数组
常见于点分治题。
做法是给每个树状数组的节点设置一个时间戳 (t_i),并记录当前时间戳 (Time)。
- 如果当前时间戳与节点时间戳相同,那么该节点的值是正确的;
- 如果不同,这个位置上的值将被重置为 (0),并更新时间戳 (t_i gets Time)。
想清空时直接 (Timegets 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 imes d_i) 就可以了。
例题!
维护一个数组 (A),支持:
- 区间加 (1);
- 区间求 (sumlimits_{i=l}^{r} i imes A_i)。
这相当于是区间加等差数列,不妨考虑维护二阶差分序列 (dd)。再通过二阶前缀和的方法求出来原数组。
自然,你又只需要三个树状数组,分别维护 (sum dd_k),(sum k imes dd_k),(sum k^2 imes 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 = sumlimits_{i=k-lowbit(k)+1}^{k} a_i)。
维护区间最值时,一个节点 (k) 维护的就是 (b_k = maxlimits_{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;
随时更新……