树状数组&线段树
作者:互联网
树状数组与线段树
树状数组
$ 5 \times 5$
常见用处:可以快速解决部分基于区间上的更新以及求和问题。
-
相比于线段树,树状数组代码比线段树短,思路更清晰,速度也更快,是解决单点问题的不二之选
原理:
由大节点存储小节点信息,查询时只需要查询大节点即可
最上面的块,$c_8$管理整个数组a($c_1,c_2,c_3,c_4,c_5,c_6,c_7,c_8$八个数),$c_4$管理的是$a_1,a_2,a_3,a_4$。其余同理。
基本用法与操作(单点修改&区间查询)
-
而这棵树有着如下规律
$c[i]=a[i-2k+1]+a[i-2k+2]+...+a[i]$;
-
k为i二进制中从最低位到最高位连续0的长度,也即要找到从最低为开始,第一个是1的位。
-
而根据计算机二进制运算特性,可得知 :$2^k$=i&(-i)。
因此引入
lowbit
:int lowbit(int x){ //求x二进制表示中,最低位1的位置 //lowbit(0b10110000) ==0b00010000 // ~~~^~~~~ ~~~^~~~~ return x&(-x)//-x==~(x+1) }
若想使树状数组中一个元素(位置为x)加上k
- 首先需要把含有x的全部节点都加上k
- 由此只需要把$c_x$及其所有父节点都加上k
- 而从下往上,有规律,父节点$c_f=c_s+lowbit(c_s)$;
因此可得:
void add(int x,int k){ while(x<=n){//n不越界,最大下标为n c[x]+=k; x+=lowbit[x];//找到父节点 } }
求x的前缀和
-
节点x与其斜上的节点和(如图上x=7时,前缀和:
($c_4+c_6+c_7$)
-
与add操作相似,可找到规律x节点斜上节点$c_y=c_x-lowbit(x)$
int getsum(int x){ int ans=0; while(x>0){ ans+=c[x]; x-=lowbit(x); } return ans; }
应对区间更新,单点查询时,将前缀和树状数组改为差分树状数组即可
-
区间加&区间求和(区间修改&区间查询)
维护a的差分数组b,若要求一个前缀r的前缀和,可推导出r的前缀和与差分数组的关系。
推导:
$\sum_{i=1}^ra_i$
$=\sum_{i=1}r\sum_{j=1}ib_i$
$=\sum_{i=1}^rb_i\times(r-i+1)$
$=(r+1)\times \sum_{i=1}rb_i-\sum_{i=1}r(b_i\times i)$
- 参考几何图形更易理解
需要两个树状数组来维护:一个$t_1$维护$b_i$,一个$t_2$维护$i\times b_i$。
int t1[MAXN],t2[MAXN],n;
inline int lowbit(int x){return x&(-x);}
void add(int k,int v)//k的前缀和增加v
{
int v1=k*v;
while(k<=n){
t1[k]+=v,t2[k]+=v1;
k+=lowbit(k);
}
}
void getsum(int *t,int x){
int ans=0;
while(x>=0){
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
void add1(int l,int r,int v){
add(l,v),add(r+1,-v);
}
long long getsum1(int l,int r){
return (r+1ll)*getsum(t1,r)-1ll*l*getsum(t1,l-1)-
(getsum(t2,r)-getsum(t2,l-1));
}
线段树
基本结构&建树
- 把每个长度不为1的区间划分为左右两个区间求解。把整个线段分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。可以方便大部分区间操作。
- 如果有数组a={10,11,12,13,14};用数组d来保存线段树$d_i$保存线段树编号为$i$节点的值(即为该区间总和)
初始化建树
当区间长度为1时,可以根据a数组相应位置的值初始化该节点。否则从中件分割为两个子区间,递归建树,最后合并。
void build(int s,int t,int p){
//对[s,t]区间建树,当前根编号为p
if(s==t){
d[p]=a[s];
return;
}
int m=s+((t-s)>>1);
build(s,m,p*2),build(m+1,t,p*2+1);
//2*p是p的左儿子,2*p+1是p的右儿子。
d[p]=d[p*2]+d[p*2+1];
}
线段树的区间查询
查询区间线段树有可以直接得到解,若无,可以拆解区间再求和。
-
一般,如果要查询的区间是[l,r],则可以将其拆解成最多为$O(\log n)$个极大的区间,合并这些区间可求解。
int getsum(int l,int r,int s,int t,int p){ //[l,r]为查询区间,[s,t]为当前节点包含的区间 //p为当前节点编号 if(l<=s&&t<=r)//当前节点区间小于查询区间,直接返回 return d[p]; int m=s+((t-s)>>1),sum=0;//m平分[s,t] if(l<=m) sum+=getsum(l,r,s,m,p*2); //如果求和需要左儿子,查询左儿子 if(r>m) sum+=getseum(l,r,m+1,t,p*2+); //右边与左边同理 return sum; }
int getsum(int l,int r,int s,int t,int p){ //版本2>>来源geeksforgeeks if(r<s||l>t) return 0; if(l>=s&&r<=t) return d[p]; int m=s+(t-s)>>1; return getsum(l,r,s,m,2*p)+getsum(l,r,m+1,t,2*p+1); }
线段树的区间修改&懒惰标记
如果要修改区间[l,r],把包含区间[l,r]中的节点都遍历一次、修改一次,时间复杂度无法承受。因此,引入一个懒惰标记。
- 简单来说就是延迟对节点的修改,要修改时做标记,再下一次访问带有标记的节点时才修改。
引入数组t,每个节点增加$t_i$。
现在准备把[3,5]上的每个数都加上5。类似区间查询,找到了两个极大区间[3,3]和[4,5](分别对应线段树3号和5号点),并标记对应t。
虽然3号节点信息虽然被修改了,但其左右儿子节点还未更新。在三号节点做着标记。以便以后要访问其子节点时顺便更新,不浪费多余时间现在更新。
void updateUtil(int l,int r,int c,int s,int t,int p){
if(l<=s&&r>=t){
st[p]+=(t-s+1)*c;//st segment_tree
b[p]+=c;
return;
}
int m=s+((t-s)>>1);
if(b[p]){
st[2*p]+=(m-s+1)*b[p],st[2*p+1]=(t-m)*b[p];
b[2*p]+=b[p],b[2*p+1]=b[p];
b[p]=0;
}
if(m>=l)
updateUtil(l,r,c,s,m,2*p);
if(m<r)
updateUtil(l,r,c,m+1,t,2*p+1);
d[p]=d[2*p]+d[2*p+1];
}
- 加了懒惰标记后的getsum
LL getsum(int l,int r,int s,int t,int p){
if(t<l||s>r)return 0L;
if(s>=l&&t<=r)return st[p];
int m=s+((t-s)>>1);
//多了一个pushdown操作
if(b[p]){
st[2*p]+=(m-s+1)*b[p],st[2*p+1]+=(t-m)*b[p];
b[2*p]+=b[p],b[2*p+1]+=b[p];
b[p]=0;
}
return getsum(l,r,s,m,2*p)+getsum(l,r,m+1,t,2*p+1);
}
标签:return,树状,int,线段,getsum,数组,区间,节点 来源: https://www.cnblogs.com/ckcyi/p/15712986.html