树状数组
# 树状数组
树状数组用于维护和查询区间前缀和
# 二进制编码
树状数组时利用数的二进制特征进行检索的一种树状的结构
我们发现,圈里带数字的节点我们定义为 ,一个节点上的 定义为其树下直练的子节点的和,例如
这样子我们就能把前缀和的查询控制在 了
考虑维护,我们发现当有一个 改变时,需要改变的 就是 的祖先节点,也就是 的复杂度
观察发现,查询操作实际上是每次去掉二进制下的最后一个 ,例如:,也就是
维护操作是每次在二进制的最后的 加上
例如如果修改了 ,那么需要修改 ,也就是
所以,树状数组的关键就是如何找到一个数的二进制的最后一个
我们利用负数的补码表示,可以得到二进制下的最后一个 ,也就是
例如
惊奇的发现, 数组所包括的元素个数与 相等,也就是说, 记录了 的元素之和
这样子利用了数的二进制下的一些特性就可以把查询优化到 级别了
# 算法实现
# 单点修改+区间查询
我们很容易就能写出来
struct tree{
int n;
vector<int> c;
tree(int n){this->n=n;c.assign(n+1,0);}
void add(int x,int val){
for(;x<=n;x+=x&-x)c[x]+=val;
}
int sum(int x){
int ret=0;
for(;x;x-=x&-x)ret+=c[x];
return ret;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 区间修改+单点查询
我们利用前缀和的思想,如果想要在 加上 就在 ,然后求前缀和就是答案
# 区间修改+区间查询
这里只用一个树状数组是做不到了,我们需要差分数组+树状数组的深度结合
求区间和 问题转化为如何求
定义 是 的差分数组,可得
就化简成了两个和式的形式,就可以用树状数组来处理了,一个实现 一个实现
tree t1(n),t2(n);
vector<int> a(n+1,0);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
int d=a[i]-a[i-1];
t1.add_x(i,d);
t2.add_x(i,(i-1)*d);
}
while(m--){
int op;cin>>op;
if(op==1){
int L,R,d;cin>>L>>R>>d;
t1.add_x(L,d);t1.add_x(R+1,-d);
t2.add_x(L,(L-1)*d);t2.add_x(R+1,-(R+1-1)*d);
}
else{
int L,R;cin>>L>>R;
cout<<R*t1.get(R)-t2.get(R)-((L-1)*t1.get(L-1)-t2.get(L-1))<<'\n';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 二维区间修改+区间查询
二维其实是一维的拓展,也是考虑构造差分数组
然后需要查询
所以问题就转化为如何求 ,利用差分数组可得
所以,我们只需要构造四个树状数组来维护 就好了
查询和求和的复杂度都为 但缺点是占用空间大
# 区间最值
其实树状数组也可以解决区间最值问题
回到这个图,前缀和树状数组中, 中存放的是 中每个数的和,那么在求最值的树状数组中就是记录的最大值
单点修改: 我们需要修改 以及 的祖先节点
如何修改,对于祖先节点,重新求一次 ,也就是扫一遍每个直接的儿子节点
void update(int x,int val){
while(x<=n){
c[x]=val;
for(int i=1;i<lowbit(x);i<<=1)
c[x]=max(c[x],c[x-i]);
x+=lowbit(x);
}
}
2
3
4
5
6
7
8
区间最值查询:
如果 那么 区间包含了 所包含的所有结点,还多出来一块 则
如果 那么 区间在 所包含的区间内,则往前递推
可以写成递推的形式
int query(int L,int R){
int ans=0;
while(L<=R){
ans=max(ans,a[R]);R--;
while(R-L>=lowbit(R)){
ans=max(ans,c[R]);
R-=lowbit(R);
}
}
return ans;
}
2
3
4
5
6
7
8
9
10
11
更新和查询的时间复杂度都为