浅谈线段树
博客内容主要来自https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html
感谢原博主大大,代码部分我根据我的习惯进行了更改
数据结构——线段树
1、引例
A.给出n个数,n<=100,和m个询问,每次询问区间[l,r]的和,并输出。
一种回答:这也太简单了,O(n)枚举搜索就行了。
另一种回答:还用得着o(n)枚举,前缀和o(1)就搞定。
那好,我再修改一下题目。
B.给出n个数,n<=100,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。
回答:o(n)枚举。
动态修改最起码不能用静态的前缀和做了。
好,我再修改题目:
C.给出n个数,n<=1000000,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。
回答:o(n)枚举绝对超时。
再改:
D,给出n个数,n<=1000000,和m个操作,每个操作修改一段连续区间[a,b]的值
回答:从a枚举到b,一个一个改。。。。。。有点儿常识的人都知道超时
那怎么办?这就需要一种强大的数据结构:线段树。
二、基本概念
1、线段树是一棵二叉搜索树,它储存的是一个区间的信息。
2、每个节点以结构体的方式存储,结构体包含以下几个信息:
区间左端点、右端点;(这两者必有)
这个区间要维护的信息(事实际情况而定,数目不等)。
3、线段树的基本思想:二分。
4、线段树一般结构如图所示:
5、特殊性质:
由上图可得,
1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]
2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质
三、线段树的基础操作
注:以下基础操作均以引例中的求和为例,结构体以此为例:
struct node { int l; int r; int mid() { return (l+r)/2.0; } ll sum;//每一个节点的sum ll add;//延迟标记数组 } tree[MAXN<<2];
线段树的基础操作主要有5个:
建树、单点查询、单点修改、区间查询、区间修改。
1、建树,即建立一棵线段树
① 主体思路:a、对于二分到的每一个结点,给它的左右端点确定范围。
b、如果是叶子节点,存储要维护的信息。
c、状态合并。(这里的状态是求和)
②代码
void Build(int l,int r,int i) { tree[i].l=l; tree[i].r=r; tree[i].add=0;///区间修改时需要 tree[i].sum=0; if(l==r)///叶子节点 { scanf("%lld",&tree[i].sum);///存储需要维护的信息 return ;///注意!! } int m=tree[i].mid(); Build(l,m,i<<1);///左孩子 Build(m+1,r,i<<1|1);///右孩子 push_up(i);///向上回溯 }
根据题目要求写状态合并的函数,这个给出一个区间求和的函数
void push_up(int i) { tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum; }
③注意
a.结构体要开4倍空间,为啥自己画一个[1,10]的线段树就懂了
b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了。
2、单点查询,即查询一个点的状态,设待查询点为x
①主体思路:与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为 mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子
②代码
int query(int x,int i)///单点查询 { if(tree[i].l==tree[i].r)///叶子节点,同时也是目目标节点 { return tree[i].sum; }int m=tree[i].mid(); if(x<=m) { return query(x,i<<1);///左孩子 } else { return query(x,i<<1|1);///右孩子 } }
③正确性分析:
因为如果不是目标位置,由if—else语句对目标位置定位,逐步缩小目标范围,最后一定能只到达目标叶子节点。
3、单点修改,即更改某一个点的状态。用引例中的例子,对第x个数加上y
①主体思路
结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。
②代码
void update(int l,int r,int i,int v,int num) { if(l==r&&l==num)///找到目标位置 { tree[i].value=v; return ; } int m=tree[i].mid(); if(m>=num) { update(l,m,i<<1,v,num); } else { update(m+1,r,i<<1|1,v,num); } push_up(i); }
4、区间查询,即查询一段区间的状态,在引例中为查询区间[x,y]的和
①主体思路
mid=(l+r)/2
y<=mid ,即 查询区间全在,当前区间的左子区间,往左孩子走
x>mid 即 查询区间全在,当前区间的右子区间,往右孩子走
否则,两个子区间都走
②代码
void query(int l,int r,int i) { if(tree[i].l==l&&tree[i].r==r)///叶子节点,同时也是目目标节点 { ans+=tree[i].sum; return ; } ///push_down(i,tree[i].r-tree[i].l+1);区间修改时使用 int m=tree[i].mid(); if(r<=m) { query(l,r,i<<1);///目标位置比中点靠左,递归到左孩子 } else if(l>m)///目标位置比中点靠右,递归到右孩子 { query(l,r,i<<1|1); } else///占两段 { query(l,m,i<<1);///左孩子 query(m+1,r,i<<1|1);///右孩子 } }
③正确性分析
情况1,3不用说,对于情况2,最差情况是搜到叶子节点,此时一定满足情况1
5、区间修改,即修改一段连续区间的值,我们已给区间[a,b]的每个数都加x为例讲解
Ⅰ.引子
有人可能就想到了:
修改的时候只修改对查询有用的点。
对,这就是区间修改的关键思路。
为了实现这个,我们引入一个新的状态——懒标记。
Ⅱ 懒标记
(懒标记比较难理解,我尽力讲明白。。。。。。)
1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。
2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。
3、实现思路(重点):
a.原结构体中增加新的变量,存储这个懒标记。
b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。
c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧
d.下传操作:
3部分:①当前节点的懒标记累积到子节点的懒标记中。
②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。
这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?
因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积
③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。
懒标记下穿代码:
void push_down(int i,int L)///L为区间长度 { if(tree[i].add) { tree[i<<1].add+=tree[i].add; tree[i<<1|1].add+=tree[i].add; tree[i<<1].sum+=tree[i].add*(L-(L>>1)); tree[i<<1|1].sum+=tree[i].add*(L>>1); tree[i].add=0; } }
Ⅲ 完整的区间修改代码:
void update(int l,int r,int i,int v) { if(tree[i].l==l&&tree[i].r==r)///找到目标位置 { tree[i].sum+=(ll)v*(r-l+1); tree[i].add+=(ll)v;///懒标记+v return ; } push_down(i,tree[i].r-tree[i].l+1);///懒标记下传 int m = tree[i].mid(); if(r<=m) { update(l,r,i<<1,v); } else if(l>m) { update(l,r,i<<1|1,v); } else { update(l,m,i<<1,v);///左孩子 update(m+1,r,i<<1|1,v);///右孩子 } push_up(i);///向上回溯,更改区间状态 }
Ⅳ.懒标记的引入对其他基本操作的影响
因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。
所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句 push_down(i,tree[i].r-tree[i].l+1);
三、总结
模板:
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define ll long long int 5 ll ans; 6 const int MAXN=2e5+10; 7 using namespace std; 8 struct node 9 { 10 int l; 11 int r; 12 int mid() 13 { 14 return (l+r)/2.0; 15 } 16 ll sum;///每一个节点的sum 17 ll add;///延迟标记数组 18 } tree[MAXN<<2]; 19 void push_up(int i) 20 { 21 tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum; 22 } 23 void push_down(int i,int L)///L为区间长度 24 { 25 if(tree[i].add) 26 { 27 tree[i<<1].add+=tree[i].add; 28 tree[i<<1|1].add+=tree[i].add; 29 tree[i<<1].sum+=tree[i].add*(L-(L>>1)); 30 tree[i<<1|1].sum+=tree[i].add*(L>>1); 31 tree[i].add=0; 32 } 33 } 34 void Build(int l,int r,int i) 35 { 36 tree[i].l=l; 37 tree[i].r=r; 38 tree[i].add=0; 39 tree[i].sum=0; 40 if(l==r)///叶子节点 41 { 42 scanf("%lld",&tree[i].sum);///存储需要维护的信息 43 return ; 44 } 45 int m=tree[i].mid(); 46 Build(l,m,i<<1);///左孩子 47 Build(m+1,r,i<<1|1);///右孩子 48 push_up(i);///向上回溯 49 } 50 void query(int l,int r,int i) 51 { 52 if(tree[i].l==l&&tree[i].r==r)///叶子节点,同时也是目目标节点 53 { 54 ans+=tree[i].sum; 55 return ; 56 } 57 push_down(i,tree[i].r-tree[i].l+1); 58 int m=tree[i].mid(); 59 if(r<=m) 60 { 61 query(l,r,i<<1);///目标位置比中点靠左,递归到左孩子 62 } 63 else if(l>m)///目标位置比中点靠右,递归到右孩子 64 { 65 query(l,r,i<<1|1); 66 } 67 else///占两段 68 { 69 query(l,m,i<<1);///左孩子 70 query(m+1,r,i<<1|1);///右孩子 71 } 72 /*if(l<=m) 73 { 74 query(l,m,i<<1);///左孩子 75 } 76 if(m<r) 77 { 78 query(m+1,r,i<<1|1);///右孩子 79 }*/ 80 } 81 82 void update(int l,int r,int i,int v) 83 { 84 if(tree[i].l==l&&tree[i].r==r)///找到目标位置 85 { 86 tree[i].sum+=(ll)v*(r-l+1); 87 tree[i].add+=(ll)v;///懒标记+v 88 return ; 89 } 90 push_down(i,tree[i].r-tree[i].l+1);///懒标记下传 91 int m = tree[i].mid(); 92 if(r<=m) 93 { 94 update(l,r,i<<1,v); 95 } 96 else if(l>m) 97 { 98 update(l,r,i<<1|1,v); 99 } 100 else 101 { 102 update(l,m,i<<1,v);///左孩子 103 update(m+1,r,i<<1|1,v);///右孩子 104 } 105 /* 106 if(l<=m) 107 { 108 update(l,m,i<<1,v);///左孩子 109 } 110 if(m<r) 111 { 112 update(m+1,r,i<<1|1,v);///右孩子 113 }*/ 114 push_up(i);///向上回溯,更改区间状态 115 } 116 int main() 117 { 118 int n,m,a,b,d; 119 char c; 120 scanf("%d%d",&n,&m); 121 Build(1,n,1);///前两个参数是节点的左右端点,最后一个参数是节点在结构体中的位置 122 while(m--) 123 { 124 scanf(" %c",&c); 125 if(c=='Q') 126 { 127 ans=0; 128 scanf("%d%d",&a,&b); 129 query(a,b,1);///同Build 130 printf("%lld\n",ans); 131 } 132 else if(c=='C') 133 { 134 scanf("%d%d%d",&a,&b,&d); 135 update(a,b,1,d); 136 } 137 } 138 return 0; 139 }