参考资料:
[1]:ACM线段树的区间合并(图文) [提取码:o380]
[2]:博客
首先让我们来看这个问题:
- 给出一个序列,由 0和1 组成
- 给出一个区间 [ l , r ]
- 求这个区间中只包含1的子串的最大长度
我们可以使用一下解法:
- 区间 DP : 时间复杂度O(n3)的预处理,O(n1)的询问
- 胡乱预处理+询问O(n2)
但是如果增加新的条件呢?
- 给出a,b,要求将第a位的值改为b
- 刚才给出的解法都是依靠预处理来达到之后的询问时间的
- 如果我们要做修改操作,就要重新预处理
联想线段树的作用,他刚好可以处理区间查询,更新问题
- 如果我们能够使用线段树来解决这类区间查询问题(但并不是简单的带修改求和或RMQ)
- 我们可以达到询问的O(logn和查询的O(logn)
- 线段树的建造是O(nlogn),查询的总时间就是O(nlogn)
- 即使不带修改也要比前面的方法优秀
解决这类问题,线段树由固定的套路
- 这类问题基本是“带修改的区间查询”“连续”问题
- 他可以问你一个区间的LCIS或者是一个区间的连续某数字等等
那么,介绍了线段树区间更新的功能后,开始回归正题,如何使用?
如图所示,是由"1101111011"构造的一颗线段树,要求某区间连续的1的个数(一部分,都画出来太麻烦了,主要是用于方便理解概念用的)
在此之前,先声明一下我的代码风格:
1 #define ls(x) (x<<1)//左儿子 2 #define rs(x) (x<<1|1)//右儿子 3 4 struct SegmentTree 5 { 6 int l,r; 7 int lsum,rsum; 8 int sum; 9 int mark;//懒惰标记 10 int mid() 11 { 12 return l+((r-l)>>1); 13 } 14 int len() 15 { 16 return r-l+1; 17 } 18 }segTree[4*maxn];
①首先介绍一下相关概念
lsum : 从本节点区间最左端开始(向右)一共有lsum个连续的1;(segTree[1].lsum = 2)
rsum : 从本节点区间最右端开始(向左)一共有rsum个连续的1;(segTree[1].rsum = 2)
sum : 本区间一共最多有 sum 个连续的1;(segTree[1].sum = 4)
mark : 区间更新的懒惰标记,比如,如果将区间[l,r]中所有的1变为0,那么 mark = 0 就意味着要将此区间中的所有1变为0;
②如何求解lsum,rsum,sum ? 下面介绍函数pushUp(int pos)的作用
pushUp(int pos) : 把当前pos结点的"左右儿子的节点"信息更新到pos结点;
例如,假设求出 1号节点"1101111011"的左右儿子节点的lsum,rsum,sum,那么便可通过pushUp(1)来求出1号节点的lsum,rsum,sum值;
segTree[1].lsum = segTree[ls(1)].lsum;
segTree[1].rsum = segTree[rs(1)].rsum;
这两个操作是毋庸置疑的,1号节点的lsum至少为ls(1)号节点的lsum,但有没有可能比 segTree[ls(1)].lsum 大呢?
答案是肯定的,如果 segTree[ls(1)].lsum = segTree[ls(1)].len(),那么
segTree[1].lsum = segTree[ls(1)].lsum + segTree[rs(1)].rsum;
如上图,如果将1号节点的左儿子中的'0'改为'1',那么segTree[1].lsum = 5+2 = 7;
segTree[1].rsum 同理;
那segTree[1].sum 该如何求呢?
很显然,它等于 (左儿子的sum值,右儿子的sum值,左儿子的rsum+右儿子的lsum值)的最大值;
pushUp()函数代码如下:
1 //返回三者最大值 2 int Max(int a,int b,int c) 3 { 4 return max(max(a,b),c); 5 } 6 void pushUp(int pos) 7 { 8 segTree[pos].lsum=segTree[ls(pos)].lsum; 9 segTree[pos].rsum=segTree[rs(pos)].rsum; 10 11 //判断是否可以增加 12 if(segTree[pos].lsum == segTree[ls(pos)].len()) 13 segTree[pos].lsum += segTree[rs(pos)].lsum; 14 if(segTree[pos].rsum == segTree[rs(pos)].len()) 15 segTree[pos].rsum += segTree[ls(pos)].rsum; 16 17 segTree[pos].sum=Max(segTree[ls(pos)].sum, 18 segTree[rs(pos)].sum, 19 segTree[ls(pos)].rsum+segTree[rs(pos)].lsum); 20 }
③介绍完pushUp(int pos)函数后,下面来介绍pushDown(int pos)函数的作用
pushDown(int pos) : 把当前pos结点的信息传递给儿子结点
还记得区间更新懒惰标记中的pushDown(int pos)函数吗?
之所以能够正确求解,就是这个函数的作用,在区间更新,查询操作中,每来到一个大区间,都要将这个区间的
懒惰标记向下传递,这样才不至于出错。
那么线段树区间合并中的pushDown(int pos)的作用也差不多;
假设有两种操作
1.将区间[l,r]全变为'0';
2.将区间[l,r]全变为'1';
那么,相应的,mark就需要有三个取值:
mark = -1 : 不做任何操作;
mark = 1 : 将区间[l,r]全变为'1';
mark = 0 : 将区间[l,r]全变为'0';
如果segTree[pos].mark = 1,那么在向下传递标记的时候,左右儿子的lsum,rsum,sum分别全都变为segTree[ls(pos)].len(),segTree[rs(pos)].len();
如果segTree[pos].mark = 0,那么在向下传递标记的时候,左右儿子的lsum,rsum,sum全都变为0
pushDown(int pos)代码如下:
1 void F(int pos,int val) 2 { 3 segTree[pos].lsum=val; 4 segTree[pos].rsum=val; 5 segTree[pos].sum=val; 6 } 7 /** 8 mark=-1 : no operator 9 mark= 1 : 0变成1 10 mark= 0 : 1变成0 11 */ 12 void pushDown(int pos) 13 { 14 int &mark=segTree[pos].mark; 15 if(mark == -1) 16 return ; 17 segTree[ls(pos)].mark=mark; 18 segTree[rs(pos)].mark=mark; 19 if(mark == 0) 20 { 21 F(ls(pos),0); 22 F(rs(pos),0); 23 } 24 else 25 { 26 F(ls(pos),segTree[ls(pos)].len()); 27 F(rs(pos),segTree[rs(pos)].len()); 28 } 29 mark=-1; 30 }
④介绍完主要的pushUp(),pushDown()后,建树,查询,更新操作就比较简单了,具体细节看代码