线段树入门(分块讲解)
在一些题目涉及到区间修改和区间求和的情况,如果我们每次修改与求和的时间复杂度均为O(n)在大数据的情况下是会超时的,因此我们要引进一个维护一个区间的数据结构——线段树.
[算法描述(线段树)]
线段树顾名思义就是由线段组成的树,我们知道线段有两个端点中间有一条直线,在线段树中我们可以把每一个节点内都看成一个线段,每个节点都维护个左端点l和右端点r那么该节点所维护的区间就是(l,r),每一个节点再维护一个相对于这个区间的数值例如和或最大值或最小值.
[代码(线段树)]
struct Segment_Tree{ //线段树(结构体形式) long long sum; //这个线段树维护的数值(可以是和也可以是最大值,最小值) long long lazy; //懒标记(后面讲) }tree[maxm<<2]; //maxm为这个线段树的大小一般要开4倍
[算法描述(建树)]
我们就给予这个树一个定义,我们把所有线段树的根节点均设为1,它维护的区间是1到n(n为维护的数据总数),每一个父节点(包括根节点,假定它维护区间为(l,r))维护的左节点的区间是(l,(l+r)>>1),右节点维护的区间是(((l+r)>>1)+1,r),因此我们得知线段树是一个二叉树,我们最后在设立一个边界就是当一个节点(l,r)l=r时它是这个线段树的一个叶子节点,就不继续递归建树了.在写函数之前我们先给建树等所有函数一个定义,由于我们知道根节点维护的区间,我们可以动态地求出每一个节点的区间(因为只和父节点有关且当父节点被访问到后才可能访问到子节点)我们便不把每个节点维护的区间记录在每个节点上了,因此后面的函数的变量可能有很多,我们给予每一个函数一个定义变量的顺序(若不需要某些变量则上整,当然变量可能会更多这时另行判断),第一个变量为当前更新的节点,第二个变量为当前节点维护的区间左端,第三个变量为当前节点维护的区间右端,第四个变量为需要修改的区间左端,第五个变量为需要修改的区间右端,第六个变量为需要修改的相应数值,我们将这些函数定下一个定义变量的规律便于我们记忆这些函数.
[代码(LC/RC)]
# define ll long long # define LC (x<<1) # define RC (x<<1|1)
其中 我们将每一个父节点的左节点定义为x<<1;右节点定义为(x<<1)+1及x<<1|1
[代码(push_up)]
inline void push_up(ll x){ //这个函数是用其子节点更新父节点的相应数值(可以是和,最大值,最小值甚至矩阵等等) tree[x].sum=tree[LC].sum+tree[RC].sum; //我们可以发现父节点维护的区间是两个子节点维护的区间之和(无交叉) return; }
[代码(build)]
void build(ll x,ll l,ll r){ //建树,x为当前建立的节点,其维护的区间为(l,r) if(l==r){ //l==r时这个节点记录的是叶子节点 tree[x].sum=a[l]; //直接赋值(a[l]为原先的序列) return; } int mid=(l+r)>>1; //取中间值并下取整,将其设为左子树和右子树的分界线 build(LC,l,mid); //递归建立左子树其维护区间为(l,mid) build(RC,mid+1,r); //递归建立右子树其维护区间为(mid+1,r)这里+1是因为使区间不重叠 push_up(x); //把子树信息更新到x节点上 return; }
[算法描述(区间修改,查询)]
我们现在来说线段树的一个重要操作——区间修改,假设我们需要修改区间(l,r),如果我们维护相应的叶子节点都修改一遍这个时间复杂度最坏的情况下是O(n log n)的,这样线段树也就没有优势了.于是我们引入了变量—懒标记(lazy),顾名思义懒标记懒标记,就是我们懒得修改从而打的标记,我们如何懒且能使答案正确呢,我们知道我们只需要修改区间(l,r),我们需要修改的点维护的区间一定在需要修改的区间内,因此如果一个节点再需要修改的区间内,我们可以直接修改一个大区间相对应的值,并不需要修改它的子节点,等我们需要单独提出该子节点的信息的时候在下传这个懒标记并修改这个子节点,这样我们可以在O(log n)的情况下完成一整个区间的修改了,对于查找区间相似的我们只需要查找完全在修改区间内的数值即可.
[代码(free)]
inline void free(ll x,ll l,ll r,ll k){ //修改节点x->(l,r)的懒标记修改值为k tree[x].lazy+=k; //更新懒标记数值 tree[x].sum+=k*(r-l+1); //区间修改数值 return; }
[代码(push_down)]
inline void push_down(ll x,ll l,ll r){ //下传懒标记(需要用到x的子树) ll mid=(l+r)>>1; //x为需要修改的节点,(l,r)为所维护的区间 free(LC,l,mid,tree[x].lazy); //向左子树传递懒标记 free(RC,mid+1,r,tree[x].lazy); //向右子树传递懒标记 tree[x].lazy=0; //情况节点x的懒标记 return; }
[代码(update)]
inline void update(ll x,ll l,ll r,ll q_l,ll q_r,ll k){ //节点x维护区间(l,r),需要修改的区间是q_l,q_r,修改相应数值为k if(q_l<=l && r<=q_r){ //如果这个节点维护的区间(l,r)完全在所需要修改的区间内 free(x,l,r,k); //先修改该区间的lazy并更新相应数值 return; } push_down(x,l,r); //这个区间维护的区间不在需要区间内(顺便下传懒标记) ll mid=(l+r)>>1; if(q_l<=mid){ update(LC,l,mid,q_l,q_r,k); //如果左子树中有节点维护的区间在修改区间内 } if(q_r>mid){ update(RC,mid+1,r,q_l,q_r,k); //如果右子树中有节点维护的区间在修改区间内 } push_up(x); //修改后当然要更新一下节点x return; }
[代码(区间查询)]
inline ll query(ll x,ll l,ll r,ll q_l,ll q_r){ ll res=0; //当前节点为x,维护区间(l,r)查询区间(q_l,q_r) //res这个根节点所能找到的区间 if(q_l<=l && r<=q_r){ //如果这个区间完全在查找区间内直接返回数值 return tree[x].sum; } ll mid=(l+r)>>1; push_down(x,l,r); //顺便释放一下这个点的懒标记 if(q_l<=mid){ res+=query(LC,l,mid,q_l,q_r); //向左节点索取答案 } if(q_r>mid){ res+=query(RC,mid+1,r,q_l,q_r); //向右节点更新答案 } return res; //返回答案 }
[代码(洛谷P3372)]
/* Name: Segment Tree Author: FZSZ-LinHua Date: 2018 06 11 Time complexity: ??? Algorithm: Segment Tree */ # include "cstdio" # include "iostream" # define ll long long # define LC (x<<1) # define RC (x<<1|1) using namespace std; const int maxm=100000+10; ll n,m,a[maxm]; struct Segment_Tree{ //线段树(结构体形式) long long sum; //这个线段树维护的数值(可以是和也可以是最大值,最小值) long long lazy; //懒标记(后面讲) }tree[maxm<<2]; //maxm为这个线段树的大小一般要开4倍 inline void push_up(ll x){ //这个函数是用其子节点更新父节点的相应数值(可以是和,最大值,最小值甚至矩阵等等) tree[x].sum=tree[LC].sum+tree[RC].sum; //我们可以发现父节点维护的区间是两个子节点维护的区间之和(无交叉) return; } void build(ll x,ll l,ll r){ //建树,x为当前建立的节点,其维护的区间为(l,r) if(l==r){ //l==r时这个节点记录的是叶子节点 tree[x].sum=a[l]; //直接赋值(a[l]为原先的序列) return; } int mid=(l+r)>>1; //取中间值并下取整,将其设为左子树和右子树的分界线 build(LC,l,mid); //递归建立左子树其维护区间为(l,mid) build(RC,mid+1,r); //递归建立右子树其维护区间为(mid+1,r)这里+1是因为使区间不重叠 push_up(x); //把子树信息更新到x节点上 return; } inline void free(ll x,ll l,ll r,ll k){ //修改节点x->(l,r)的懒标记修改值为k tree[x].lazy+=k; //更新懒标记数值 tree[x].sum+=k*(r-l+1); //区间修改数值 return; } inline void push_down(ll x,ll l,ll r){ //下传懒标记(需要用到x的子树) ll mid=(l+r)>>1; //x为需要修改的节点,(l,r)为所维护的区间 free(LC,l,mid,tree[x].lazy); //向左子树传递懒标记 free(RC,mid+1,r,tree[x].lazy); //向右子树传递懒标记 tree[x].lazy=0; //情况节点x的懒标记 return; } inline void update(ll x,ll l,ll r,ll q_l,ll q_r,ll k){ //节点x维护区间(l,r),需要修改的区间是q_l,q_r,修改相应数值为k if(q_l<=l && r<=q_r){ //如果这个节点维护的区间(l,r)完全在所需要修改的区间内 free(x,l,r,k); //先修改该区间的lazy并更新相应数值 return; } push_down(x,l,r); //这个区间维护的区间不在需要区间内(顺便下传懒标记) ll mid=(l+r)>>1; if(q_l<=mid){ update(LC,l,mid,q_l,q_r,k); //如果左子树中有节点维护的区间在修改区间内 } if(q_r>mid){ update(RC,mid+1,r,q_l,q_r,k); //如果右子树中有节点维护的区间在修改区间内 } push_up(x); //修改后当然要更新一下节点x return; } inline ll query(ll x,ll l,ll r,ll q_l,ll q_r){ ll res=0; if(q_l<=l && r<=q_r){ return tree[x].sum; } ll mid=(l+r)>>1; push_down(x,l,r); if(q_l<=mid){ res+=query(LC,l,mid,q_l,q_r); } if(q_r>mid){ res+=query(RC,mid+1,r,q_l,q_r); } return res; } int main(){ int x,y,c,k; scanf("%lld%lld",&n,&m); register int i; for(i=1;i<=n;i++){ scanf("%lld",&a[i]); } build(1,1,n); for(i=1;i<=m;i++){ scanf("%d",&c); if(c==1){ scanf("%d%d%d",&x,&y,&k); update(1,1,n,x,y,k); }else{ scanf("%d%d",&x,&y); printf("%lld\n",query(1,1,n,x,y)); } } return 0; }