线段树是一种常用的数据结构,经常用于多次对数组进行修改和查询的题目中。
相比于与其有同样作用的前缀和与树状数组,线段树的优点在于它对于修改与查询都有log(n)的复杂度以及它可以维护区间值的特性
(前缀和修改复杂度是O(n),而树状数组只能维护前缀的操作值:例如(1-n)的最大值)
关于线段树的结构,我们可以将其看为一个几乎完全的二叉树结构,因为其本质就是将区间二分在记录整个区间的操作值,所以它的空间复杂度就是(2*N)
例如有一数组T{1,3,2,6},我们以线段树的结构记录它区间和的结构图是这样的
将(1-4)的区间二分至单点再从叶子节点开始边回溯边建树,整个过程可以用dfs来实现,代码如下
1 void B(int n,int l,int r){int mid=(l+r)/2;//二分区间 2 if(l==r) 3 {T[n]=A[l];return;}//当区间为叶子节点时赋值 4 B(n*2,l,mid); 5 B(n*2+1,mid+1,r);//递归构建树 6 T[n]=T[n*2]+T[n*2+1];//回溯给父亲节点建立节点 7 }
对于一颗线段树,我们有单点查询,单点修改,区间查询,区间修改4种操作
1.单点查询
就是二分区间查找要查询的点,找到后回溯即可
1 int Q(int n,int l,int r,int q){int mid=(l+r)/2,ans;//n是当前坐标l,r是当前区间,q为查询节点 2 if((q>r)||(q<l))return 0; 3 if(l==r){ 4 if(q==l)return T[n].w; 5 return 0; 6 } 7 L(n,r-l+1);//lazy优化,对于区间修改十分省时间 8 return Q(n*2+1,mid+1,r,q)+Q(n*2,l,mid,q);//返回查询的值 9 }
2.单点更改
依旧是二分区间查找要更改的点,回溯的时候维护区间的操作值
1 void C(int n,int l,int r,int lo,int w){int mid=(l+r)/2; 2 if(l==r){ 3 if(lo==l)T[n]+=w; 4 return; 5 } 6 if(lo<=mid) 7 C(n*2,l,mid,lo,w); 8 if(lo>=mid+1) 9 C(n*2+1,mid+1,r,lo,w);//二分查找要更改的点 10 T[n]=T[n*2]+T[n*2+1];//回溯维护区间的操作值 11 }
3.区间查询
与单点查询类似,但返回两个子区间的操作值
1 int Q(int n,int l,int r,int b,int e){int mid=(l+r)/2,x=0,y=0; 2 if((b>r)||(e<l))return 0; 3 if((l>=b)&&(r<=e))return T[n]; 4 if(b<=mid) 5 x=Q(n*2,l,mid,b,e); 6 if(e>=mid+1) 7 y=Q(n*2+1,mid+1,r,b,e); 8 return x+y;//返回两个区间的操作和 9 }
在讲区间修改之前,我们需要知道一个对于线段树极为重要的操作——延迟处理(lazy)
当我们修改一个区间的值时(例如更改1,2的值),我们会将所有与这个区间相关的点都进行更改,如图
(图中画红圈的就是修改的点)
看起来复杂度并没有多大,但是当修改的区间很大时所需的时间会远超预计
(例如修改1-N/2的区间,用红框框住的就是要修改的节点)
而很多时候对于区间查询我们经常在很高的节点就返回值使得我们对于如此高的复杂度的操作显得没有意义,例如对于上图(1-N/2)区间的查询我们只需递归一层即可以得到答案,而不用递归到叶子节点。
所以我们可以将一些区间完全被修改区间所包含的节点作一个标记并停止递归,等以后再查询此节点时在将这个节点以下未修改的节点进行修改
1 void L(int n,int l){ 2 if(T[n].m){//当此节点被标记过 3 T[n*2].m+=T[n].m; 4 T[n*2+1].m+=T[n].m;//将子节点打上标记 5 T[n*2].w+=(l-(l/2))*T[n].m; 6 T[n*2+1].w+=(l/2)*T[n].m;//处理该节点的子节点 7 }T[n].m=0;//将标记清零 8 }
因为用了lazy,我们对于递归到的每个点都要判断一下这个点是否有标记,防止出现查找(修改)值未修改的情况
了解了lazy,我们就可以理解区间修改了,区间修改和单点修改类似,只不过对于当前区间属于修改区间的时候不再递归,而是打上标记并退出
1 void C(int n,int l,int r,int ll,int lr,int w){int mid=(l+r)/2,le=0; 2 if((ll>r)||(lr<l))return; 3 if((l>=ll)&&(r<=lr)) 4 {T[n].m+=w;T[n].w+=(LL)w*(r-l+1);return;} 5 L(n,r-l+1);//判断是否被标记过以及修改标记 6 C(n*2,l,mid,ll,lr,w); 7 C(n*2+1,mid+1,r,ll,lr,w); 8 T[n].w=T[n*2].w+T[n*2+1].w; 9 }
这就是线段树的几种用法,COGS上有不少关于线段树的习题,这里就留几道模板题的链接:
264.数列操作:单点修改区间查询
1316. 数列操作b:区间修改单点查询
1317. 数列操作c:区间修改区间查询