总原理:
将[1,n]分解成若干特定的子区间(数量不超过4*n)
用线段树对“编号连续”的一些点,进行修改或者统计操作,修改和统计的复杂度都是O(log2(n))
用线段树统计的东西,必须符合区间加法,(也就是说,如果已知左右两子树的全部信息,比如要能够推出父节点);否则,不可能通过分成的子区间来得到[L,R]的统计结果。
一个问题,只要能化成对一些“连续点”的修改和统计问题,基本就可以用线段树来解决了
注意:区分3个概念:原数组下标,线段树中的下标和存储下标。
原数组下标,这里都默认下标从1开始(一般用a数组表示,a [1] , a[2] …… a [n] )
线段树下标,是指该节点所管理的局部点集区,。一般都用区间 [ l , r ] 表示,即这一点存储的是 a [l] ~ a [r] 区间内的局部解、局部信息。
存储下标,是指该元素在总内存池 arr 中实际的存储位置,一般用 arr [ root ]表示。存储方式类似于“完全二叉树”,详细见下面的图
注意,在元数个数N确定的时候,数字对 [ l , r ] 的值 和 root 的值,是对应的。
每一个arr[root],都有一个对应的、专属于它自己管理的区间 [ l , r ]
因为对于确定的N,构建的线段树是唯一确定的
具体落实到计算上,就是 l、r 和 root同步变化,例如:(l , mid , root*2) 、 (mid+1 , r , root*2+1)
线段树的解题关键:
1:要推出父节点,我需要知道两个子节点的那些信息?————这个决定了, 每个单位结点的数据结构(需要设置哪些成员变量),如何设置node
2:在1的基础上,已知两个子节点的完整信息,我该如何推出父节点?————定义push_up函数
3:如果是区间更新,还需要思考:如何设置lazytag?如何用设置的lazy_tag更新所需数据值?
Lazytag的设置,主要是要求:求sum的时候,根据从父区间传下来的lazy_tag,能正确更新子区间所有内部数据值和他的lazy值,从而正确求出sum
线段树的存储结构:
假设有某个结点,它的信息存储在 arr [x] 中,那么它的左子节点信息存储在 arr [2x] 中,右结点信息存储在 arr [2x+1]中
并且,arr [1] 为整个树的根节点 ,所以,整体的统计信息、最终的答案,是存在节点1中的。
线段树的代码组成:
1:build函数:用于建立整个线段树。
2:upgrade,修改(更新)函数。注意区分:单点更新、区间更新。
3:push_up: 父节点回溯更新函数
如果是区间更新,还需要:push_down,用于:把lazy_tag往下推一级,推到该节点的子节点,并更新该节点左右子节点的:数据值、lazy_tag值。
一、build函数:构建线段树
原理:
开始时是区间[1,n] ,通过递归来逐步分解;
线段树对于每个n的分解是唯一的,所以n相同的线段树,结构相同。
先向下递归地寻找叶节点所在位置(寻找长度为1的区间),
一旦找到就把值放到这个位置,
并向上回溯地修改所有父节点的值。
递归遍历的顺序,十分类似于树的后续遍历。
例子:N == 10 ;原数组a:1,2,3,4,5,6,7,8,9,10;要求:区间和
过程如图:
那么问题来了,究竟要递归到多深,才到可以存放原始数据的叶节点??
这取决于:[l,r]什么时候有l==r;其实质是,取决于总结点的个数N.
这也就是为什么我们说 “线段树对于每个n的分解是唯一的,n相同的线段树,结构相同。”
代码:
void build(int l,int r,int root)//线段树建树 { if(l == r) { num[root]=1; return; } // [l,mid]:左子树 [mid+1,r]:右子树 int mid = (l+r)/2;
build(l,mid,root*2);
build(mid+1,r,root*2+1);
push_up(root); //对于这个根节点root,先把左右子树都算出来了,再来更新它的值。 //沿路回溯。回溯到的点root,都是被 [la , rb] 或其子区间影响到的点,边回溯边更新 } //调用:build(1,N,1);
二、线段树的点修改
首先由根节点1向下递归,找到对应的叶节点,
然后,修改叶节点的值,
再向上返回,在函数返回的过程中,更新路径上的节点的统计信息。
void upgrade(int p,int val,int l,int r,int root) //单点更新的upgrade算法:把找点p,当成找区间[p,p] { if(l==r) { num[root]+=val; return ; } int mid = (l+r)/2; if(p>mid) //如果p>当前区间的中点,说明我想找的[p,p]区间,在右半边 upgrade(p,val,mid+1,r,root*2+1); else upgrade(p,val,l,mid,root*2); //沿路回溯。回溯到的点root,都是被[p,p]区间影响到的点,边回溯边更新 num[root] = num[root*2] + num[root*2+1]; }
三、线段树的区间修改 —— 引入 lazy_tag
和点修改一样,也是将区间分成子区间,
首先由根节点1向下递归,找到对应的叶节点,
然后,修改本节点的值,
再向上返回,在函数返回的过程中,更新路径上的节点的统计信息。
由此可见向上更新的部分,是一毛一样的。
不同的是,点修改不用向下修改(因为对下面的没有影响,并且也没有下一级节点了,它自己就是最底层叶节点),
可是区间修改,理论上来说是需要向下修改的(因为父区间变动,子区间也会变动)
但是又不能,每次一区间修改,就连带着修改下面的所有子结点,这也tm太慢了。
所以,我们引入了 lazy_tag , 这样就可以不用把区间内的每个点都按照单点更新的方式更新一遍。
我们暂时不更新下面的子节点,而是打上一个标记,什么时候要用到子节点了,什么时候再看着这个标签,更新子节点。
(一般都是sum的时候用push_down更新子节点,因为求和的时候需要先知道两个子节点的值)
lazy_tag的含义:
本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。(注意:打上懒惰标记的节点,它自己是已经更新过了的)
即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.
打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。
这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。
有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。
重申,请注意:区间修改才需要懒惰标记,单点修改不需要。
示例代码:
void upgrade(int la,int rb,int l,int r ,int val,int root) { /*以后就永远设定: la、rb为需更新区间的左、右端点(一直不变); l、r为当前区间的左、右端点,(随递归更新) root为当前 [l , r ] 对应的根存储位置(随递归更新) */ //若本次所看区间,整个就包含在所要查询的区间之内 if(la<=l && rb>=r) { num[root] = (r-l+1)*val; //把本区间num更新为正确值 lazy[root] = val; //增加lazy标记,表示:本区间的Sum正确,子区间的Sum仍需要根据lazy的值来调整 return ; } push_down(root,r-l+1); //在继续递归之前,先把当前root 的标记往下推 //每次都是先下推,再更新,从而保证,计算root的时候,它的左右两子树都已经是正确值,左右两子树都不存在lazy更新延迟,都已经更新好了。 int mid = (l+r)/2; if(la<=mid) { upgrade(la,rb,l,mid,val,root*2); } if(rb>mid) { upgrade(la,rb,mid+1,r,val,root*2+1); } push_up(root); }
四、push_up函数
我的目标是,已知两个子节点的某些信息,利用push_up函数推出父节点
思考:如果要用两个子节点推出父节点,我需要知道子节点的那些信息?————这些信息都是线段树需要维护的。
然后,已知了所有所需信息之后,具体该怎么推?————决定了:如何具体的写出 push_up函数
注意:push_up函数内,两个子节点的信息,是已经更新过的、正确的信息。因为在调用push_up之前,已经调用过push_down函数,也就是说,两个子节点的信息,是已经被更新过了的,是正确的。
五、push_down函数
一、push_down函数总共需要干三件事:
1:更新左右子节点的lazy值,也就是:把父节点root上面的lazy标记,下推到两个子节点上
2:依据lazy_tag的定义,更新左右子节点的num值,使左右子节点的值成为“被更新过的、正确的值”。
3:把父节点root自身的lazy清空。因为lazy_tag已经被下推,就向上查询来看,父节点自身的lazy_tag已经不需要了
二、注意:
1:开始一切动作之前,要先特判:此父节点上究竟有没有lazy_tag。如果本就没有lazy_tag,千万别更新,会wa。
2:lazy_tag的意义可以被任意定义;关于‘一’中的“更新”,具体的更新方法也是多种多样的,但关键是:
lazy_tag的定义,要能跟 “更新num的操作”对应得上。
也就是说,必须要能够根据lazy_tag,正确更新结点的num值才可以。
示例代码:
(此处lazy_tag存储的是:当前段中,每个结点被更新成的数值)
(num的意义是:此区间上的数值累加和)
(所以根据这里lazy_tag的定义,每个num 的更新方法就是:lazy的数值*区间长度)
void push_down(int root,int len) //传入:父节点root 和 区间长度len { if(lazy[root] == 0) //如果此节点根本没有lazy_tag,直接返回,不作处理 return; lazy[root*2] = lazy[root]; //把lazy下推到两个子节点上 lazy[root*2+1] = lazy[root]; num[root*2] = lazy[root*2]*(len-(len)/2); //更新两个子节点的num num[root*2+1] = lazy[root*2+1]*((len)/2);、 lazy[root]=0; //清空父节点自身的lazy_tag }
整体的示例代码:

//线段树——————区间修改,区间查询 //题意:一个线性数组,不断把某区间内的值修改“为”另一个输入值,查询区间内和。 #include <iostream> #include <cstdio> #include <cstring> #define maxn 100005 int num[maxn*4];//开四倍空间 int lazy[maxn*4]; using namespace std; void push_up(int root)//根节点状态更新 { num[root] = num[root*2] + num[root*2+1]; } void build(int l,int r,int root)//线段树建树 { if(l == r) { num[root]=1; return; } int mid = (l+r)/2; build(l,mid,root*2); build(mid+1,r,root*2+1); push_up(root); } void push_down(int root,int len)//传入:root结点下标、对应的当前[l,r]区间长度 { if(lazy[root] == 0)//假若这个节点根本没有lazy_tag return; lazy[root*2] = lazy[root]; lazy[root*2+1] = lazy[root]; num[root*2] = lazy[root*2]*(len-(len)/2); num[root*2+1] = lazy[root*2+1]*((len)/2); lazy[root]=0; } void upgrade(int la,int rb,int l,int r ,int val,int root)//线段更新 //la、rb为需更新的区间左、右端点,l、r为当前区间左、右端点,root为当前l、r对应的根存储位置 { if(la<=l && rb>=r) { num[root] = (r-l+1)*val; lazy[root] = val; return ; } push_down(root,r-l+1); int mid = (l+r)/2; if(la<=mid) { upgrade(la,rb,l,mid,val,root*2); } if(rb>mid) { upgrade(la,rb,mid+1,r,val,root*2+1); } push_up(root); } int query(int la,int rb,int l,int r,int root)//查询区间[la,rb]的值 { if(l>=la && r<=rb) { return num[root]; } push_down(root,r-l+1); int mid = (l+r)/2; int ans=0; if(la<=mid) { ans += query(la,rb,l,mid,root*2); } if(rb>mid) { ans +=query(la,rb,mid+1,r,root*2+1); } return ans; } int main() { int t; scanf("%d",&t); for(int o=1;o<=t;o++) { int N;//原数组结点总数 scanf("%d",&N); memset(num,0,sizeof(num)); memset(lazy,0,sizeof(lazy)); build(1,N,1);//建树 int x; scanf("%d",&x); while(x--) { int la,rb,val; scanf("%d%d%d",&la,&rb,&val); upgrade(la,rb,1,N,val,1);//更新[l,r]区间结点,每个结点都被更新“成”val query(la,rb,1,N,1);//查询区间[la,rb]的值 } } return 0; }