论线段树:一
《论线段树》
——线段树精细讲解第一篇 ——Yeasion_Nein出品
首先在这里说明:如果你不知道二叉搜索树是什么的话,请先去学二叉搜索树谢谢。(安利一篇自己的Blog啦~~)
至于为什么的话,就是因为线段树就是建立在二叉搜索树的基础上。否则可想而知树的遍历会有多复杂,当然,能不能遍历都还不好说。
这里只是对于线段树的一个入门的概念和代码实现过程,至于更高级的操作大多数是是在线段树的一些高级题目中会有涉及。
首先我们举一个例子:(原题:【甩链接】)
告诉你现在有一堆数。哦,就是一堆数列,然后让你维护下面的几个操作:
1.将某一个区间内的每一个数都加上一个x。
2.求出某一个区间内所有数的和。
这样,我们先做一组样例来试试看。
1 5 4 2 3.
然后让你进行五组操作:
求2~4的和。
将2~3的所有数加上2。
求3~4的和。
将1~5的所有数加上1。
求1~4的和。
按照暴力出奇迹的思路来看,(并没有什么问题。)这道题不过是单纯的模拟。
嗯,那种时间复杂度为O(不能过)的模拟。
那么在这里我们首先建一个特殊的二叉搜索树。
嗯,大家都看到了,这个二叉搜索数的节点就是一个单元区间,而图中标绿的就是其叶子结点,也就是单元区间长度为1的节点。
对于每一个节点,我们定义一个left,一个right,代表左右端点,而当left=right的时候,就是叶子结点了。
借于二叉搜索数的特殊性质,我们知道对一个节点x,他的leftson(左儿子)的编号就是x的编号的两倍,而其rightson(右儿子)的编号就是x的编号的两倍+1.
现在我们来谈一下建数的问题。
首先,对于整个的线段树,我们要有一个struct。当然,结构体的大小是4倍。
struct point { int left;//左端点 int right;//右端点 int sum;//区间和 }edge[MAXN*4];
其实建树也是一个很简单的操作,就是一个build函数。而在build函数中,主要更新的就是区间和sum。
void build(int left,int right,int now)//now:当前区间的编号 { if(left==right)//到达叶子结点 { edge[now].sum=value[left];//更新区间和 return ; } int mid=(left+right)/2;//取中间 build(left,mid,now*2); build(right,mid,now*2+1); update(now);//更新操作。 }
而其中的那个update(now)就是更新编号为now的节点的区间和sum。很简单,就是将其左子树的sum和右子树的sum加起来。因为这是一个回溯的操作,所以在更新now的sum的时候,其左子树的sum和右子树的sum都已经被更新完毕了。
#define leftson now*2 #define rightson now*2+1 void update(int now) { edge[now].sum=edge[leftson].sum+edge[rightson].sum; }
建树完毕了,那么接下来就是区间修改和求和操作了。
在这里我们定义一个lazy tag(懒标记),为什么要定义这个东西呢?按我的话来说,这叫“偷懒延迟”。就是说,当我们接下来要更改的东西不会对下面的操作造成影响的话,我们就可以不去修改它。
也就是说,我们对于将要修改的对象放一个tag,等到下一次又要修改或者询问这个节点的时候,我们才将tag下放给它的子树。
tag的put函数中一共要修改四个元素:
1.左儿子的tag(非左子树)
2.左儿子的sum(非左子树)
3.右儿子的tag(非右子树)
4.右儿子的sum(非右子树)
所以下面就是tag的put函数了:
void put(long long now,long long mid,long long left,long long right) { if(tag[now])//如果now节点已经有tag了 {//注意tag的put函数只是下放给now节点的左右儿子,而非左右子树 tag[leftson]+=tag[now];//为左儿子打tag tag[rightson]+=tag[now];//为右儿子打tag edge[leftson].sum+=(mid-left+1)*tag[now];//更新左儿子 edge[rightson].sum+=(right-mid)*tag[now];//更新右儿子 tag[now]=0; //更新完毕,清除tag标记。 } }
可以更加形象的理解一下这个lazy tag,就是以为有的修改是对查询没有任何作用的,但是我们又要耗费大量的时间进行修改,于是就会十分两费时间,所以在这里才引入了懒标记,用的时候才进行下放,不用的时候就由你的“爸爸”帮你放着。
当然,上述只是Lazy Tag 的下放而已,区间修改可不能忘了“修改”啊。
void change(long long left,long long right,long long now,long long v,long long now_left,long long now_right) { //v表示要加的数值。 //注意:left和righjt是当前找到的节点的左右端点。 //而now_left 和now_right是查询中要修改的区间的左右端点。 if(now_left<=left) if(now_right>=right)//如果要修改的区间,完全包含当前节点的区间 { tag[now]+=v; edge[now].sum+=(right-left+1)*v; return ; } long long mid=(left+right)/2; put(now,mid,left,right); if(now_left<=mid)//继续进行左区间 change(left,mid,now*2,v,now_left,now_right); if(mid<now_right)//继续进行右区间 change(mid+1,right,now*2+1,v,now_left,now_right); update(now); }
OK,那么区间修改到此结束,下面是查询。其实看完了上面这些之后查询反而变成了最简单的事情了。
直接借鉴上面change就好。
long long ask(long long left,long long right,long long now,long long now_left,long long now_right) { //注意:left和righjt是当前找到的节点的左右端点。 //而now_left 和now_right是查询中的区间的左右端点。 long long ans=0; if(now_left<=left)//完全包含 if(now_right>=right) return edge[now].sum; //直接返回当前节点的值就好。 long long mid=(left+right)/2;//取中间 put(now,mid,left,right);//下放标记 if(mid>=now_left) //更新左边 ans+=ask(left,mid,now*2,now_left,now_right); if(mid<now_right) //更新右边 ans+=ask(mid+1,right,now*2+1,now_left,now_right); return ans;//返回答案 }
好,最基本的线段树到此先告一段落。
因为这只是最基本的线段树操作,如果想学习更高深的一些算法还要多家联系才行。
(该博客的代码完全现场手打,可能有误,请读者尽量不要照抄)
好的那么下面发一下全部代码。
//Yeasion_Nein 出品 #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define MAXN 1000010 using namespace std; long long value[MAXN],tag[MAXN]; struct point { long long left;//左端点 long long right;//右端点 long long sum;//区间和 }edge[MAXN*4]; #define leftson now*2 #define rightson now*2+1 void update(long long now) { edge[now].sum=edge[leftson].sum+edge[rightson].sum; } void build(long long left,long long right,long long now)//now:当前区间的编号 { if(left==right)//到达叶子结点 { edge[now].sum=value[left];//更新区间和 return ; } long long mid=(left+right)/2;//取中间 build(left,mid,now*2); build(mid+1,right,now*2+1); update(now);//更新操作。 } void put(long long now,long long mid,long long left,long long right) { if(tag[now])//如果now节点已经有tag了 {//注意tag的put函数只是下放给now节点的左右儿子,而非左右子树 tag[leftson]+=tag[now];//为左儿子打tag tag[rightson]+=tag[now];//为右儿子打tag edge[leftson].sum+=(mid-left+1)*tag[now];//更新左儿子 edge[rightson].sum+=(right-mid)*tag[now];//更新右儿子 tag[now]=0; //更新完毕,清除tag标记。 } } void change(long long left,long long right,long long now,long long v,long long now_left,long long now_right) { //v表示要加的数值。 //注意:left和righjt是当前找到的节点的左右端点。 //而now_left 和now_right是查询中要修改的区间的左右端点。 if(now_left<=left) if(now_right>=right)//如果要修改的区间,完全包含当前节点的区间 { tag[now]+=v; edge[now].sum+=(right-left+1)*v; return ; } long long mid=(left+right)/2; put(now,mid,left,right); if(now_left<=mid)//继续进行左区间 change(left,mid,now*2,v,now_left,now_right); if(mid<now_right)//继续进行右区间 change(mid+1,right,now*2+1,v,now_left,now_right); update(now); } long long ask(long long left,long long right,long long now,long long now_left,long long now_right) { //注意:left和righjt是当前找到的节点的左右端点。 //而now_left 和now_right是查询中的区间的左右端点。 long long ans=0; if(now_left<=left)//完全包含 if(now_right>=right) return edge[now].sum; //直接返回当前节点的值就好。 long long mid=(left+right)/2;//取中间 put(now,mid,left,right);//下放标记 if(mid>=now_left) //更新左边 ans+=ask(left,mid,now*2,now_left,now_right); if(mid<now_right) //更新右边 ans+=ask(mid+1,right,now*2+1,now_left,now_right); return ans;//返回答案 } int main() { long long n,m; scanf("%lld%lld",&n,&m); for(long long i=1;i<=n;i++) scanf("%lld",&value[i]); build(1,n,1); for(long long i=1;i<=m;i++) { long long p; scanf("%lld",&p); if(p==1) { long long x,y,z; scanf("%lld%lld%lld",&x,&y,&z); change(1,n,1,z,x,y); } else if(p==2) { long long x,y; scanf("%lld%lld",&x,&y); printf("%lld\n",ask(1,n,1,x,y)); } } return 0; }
——Yeasion_Nein