博客内容主要来自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 }
来源:https://www.cnblogs.com/wkfvawl/p/9397657.html
