作为数据结构专场的开端,也是最基础的数据结构之一,愿大家有个好的开始!!!
先来一道毒瘤题,仅供参考,希望初学者不要在意 https://www.luogu.org/problemnew/show/P3373
O、引例
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,r,w;//l,r分别表示区间左右端点,w表示区间和
}tree[4*n+1];
线段树的基础操作主要有5个:
建树、单点查询、单点修改、区间查询、区间修改。
1、建树,即建立一棵线段树
① 主体思路:a、对于二分到的每一个结点,给它的左右端点确定范围。
b、如果是叶子节点,存储要维护的信息。
c、状态合并。
②代码
1 #include<cstdio> 2 using namespace std; 3 struct sd{ 4 int l,r,w; 5 } 6 sd tree[10005]; 7 int ans; 8 void build(int l,int r,int k) 9 { 10 tree[k].l=l; 11 tree[k].r=r; 12 if(l==r) 13 { 14 scanf("%d",&tree[k].w); 15 return ; 16 } 17 int m=(l+r)/2; 18 build(l,m,k*2); 19 build(m+1,r,k*2+1); 20 tree[k].w=tree[k*2].w+tree[k*2+1].w; 21 } 22 int main() 23 { 24 int n; 25 scanf("%d",&n); 26 build(1,n,1); 27 return 0; 28 }
③注意
a.结构体要开4倍空间,为啥自己画一个[1,10]的线段树就懂了
b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了。
2、单点查询,即查询一个点的状态,设待查询点为x
①主体思路:与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为 mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子
1 #include<cstdio> 2 using namespace std; 3 struct sd{ 4 int l,r,w; 5 } 6 sd tree[10005]; 7 int ans; 8 int x;//目标查询点。 9 void search(v) 10 { 11 if(tree[v].l==tree[v].r) 12 { 13 ans=tree[v].w; 14 return; 15 } 16 int m=(tree[v].l+tree[v].r)/2; 17 if(x<=m)find(v*2); 18 if(x>m)find(v*2+1); 19 }
3、单点修改,即更改某一个点的状态。用引例中的例子,对第x个数加上y
①主体思路
结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。
1 #include<cstdio> 2 using namespace std; 3 struct sd{ 4 int l,r,w; 5 } 6 sd tree[10005]; 7 int change; 8 int x;//要修改的点。 9 void search(v) 10 { 11 if(tree[v].l==tree[v].r) 12 { 13 tree[v].w=tree[v].w+change; 14 return; 15 } 16 int m=(tree[v].l+tree[v].r)/2; 17 if(x<=m)find(v*2); 18 if(x>m)find(v*2+1); 19 tree[v].w=tree[v*2].w+tree[v*2+1].w; 20 }
4、区间查询,即查询一段区间的状态,在引例中为查询区间[x,y]的和
①主体思路
正确性分析
情况1,3不用说,对于情况2,最差情况是搜到叶子节点,此时一定满足情况1。
代码
#include<cstdio> using namespace std; struct sd{ int l,r,w; } sd tree[10005]; int x,y;//查询的目标区间。 int res=0; void make(int k) { if(tree[k].l>=x&&tree[k].r<=y) { res=res+tree[k].w;//根据题目要求。 return ; } int m=(tree[k].l+tree[k].r)/2; if(m<y)make(k*2+1); if(m>=x)make(k*2); }
5、区间修改,即修改一段连续区间的值,我们已给区间[a,b]的每个数都加x为例讲解
Ⅰ.引子
代码
1 #include<cstdio> 2 using namespace std; 3 struct sd{ 4 int l,r,w; 5 } 6 sd tree[10005]; 7 int x,y;//查询的目标区间。 8 int change; 9 void make(int k) 10 { 11 if(tree[k].l>=x&&tree[k].r<=y&&tree[k].l==tree[k].r) 12 { 13 tree[k].w=tree[k].w+change; 14 return ; 15 } 16 int m=(tree[k].l+tree[k].r)/2; 17 if(m<y)make(k*2+1); 18 if(m>=x)make(k*2); 19 tree[k].w=tree[k*2].w+tree[k*2+1].w; 20 }
有人可能就想到了:
修改的时候只修改对查询有用的点。
对,这就是区间修改的关键思路。
为了实现这个,我们引入一个新的状态——懒标记。
Ⅱ 懒标记
(懒标记比较难理解,自行领悟)
1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。
2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。
3、实现思路(重点):
a.原结构体中增加新的变量,存储这个懒标记。
b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。
c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧
d.下传操作:
3部分:①当前节点的懒标记累积到子节点的懒标记中。
②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。
这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?
因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积
③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。
懒标记下传代码:f为懒标记,其余变量与前面含义一致。
1 #include<cstdio> 2 using namespace std; 3 struct sd{ 4 int l,r,w; 5 int f;//懒标记。 6 } 7 sd tree[10005]; 8 int x,y;//查询的目标区间。 9 void down(int k)//下传操作 10 { 11 tree[k*2].f+=tree[k].f; 12 tree[k*2+1].f+=tree[k].f; 13 tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); 14 tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); 15 tree[k].f=0; 16 } 17 void add(int k)//修改操作 18 { 19 if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 20 { 21 tree[k].w+=(tree[k].r-tree[k].l+1)*x;//区间点的总数 22 tree[k].f+=x; 23 return; 24 } 25 if(tree[k].f) down(k); 26 //懒标记下传。只有不满足上面if条件才执行,所以一定会用到当前节点的子节点 27 int m=(tree[k].l+tree[k].r)/2; 28 if(a<=m) add(k*2); 29 if(b>m) add(k*2+1); 30 tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 31 }
Ⅳ.懒标记的引入对其他基本操作的影响
因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。
所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句if(tree[k].f) down(k),其余不变。
引入了懒标记的单点查询代码:
1 void ask(int k)//单点查询 2 { 3 if(tree[k].l==tree[k].r) 4 { 5 ans=tree[k].w; 6 return ; 7 } 8 if(tree[k].f) down(k);//懒标记下传,唯一需要更改的地方 9 int m=(tree[k].l+tree[k].r)/2; 10 if(x<=m) ask(k*2); 11 else ask(k*2+1); 12 }
引入了懒标记的区间查询代码:
1 void sum(int k)//区间查询 2 { 3 if(tree[k].l>=x&&tree[k].r<=y) 4 { 5 ans+=tree[k].w; 6 return; 7 } 8 if(tree[k].f) down(k)//懒标记下传,唯一需要更改的地方 9 int m=(tree[k].l+tree[k].r)/2; 10 if(x<=m) sum(k*2); 11 if(y>m) sum(k*2+1); 12 }
三、总结
线段树5种基本操作代码:
1 #include<cstdio> 2 using namespace std; 3 int n,p,a,b,m,x,y,ans; 4 struct node 5 { 6 int l,r,w,f; 7 }tree[400001]; 8 inline void build(int k,int ll,int rr)//建树 9 { 10 tree[k].l=ll,tree[k].r=rr; 11 if(tree[k].l==tree[k].r) 12 { 13 scanf("%d",&tree[k].w); 14 return; 15 } 16 int m=(ll+rr)/2; 17 build(k*2,ll,m); 18 build(k*2+1,m+1,rr); 19 tree[k].w=tree[k*2].w+tree[k*2+1].w; 20 } 21 inline void down(int k)//标记下传 22 { 23 tree[k*2].f+=tree[k].f; 24 tree[k*2+1].f+=tree[k].f; 25 tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); 26 tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); 27 tree[k].f=0; 28 } 29 inline void ask_point(int k)//单点查询 30 { 31 if(tree[k].l==tree[k].r) 32 { 33 ans=tree[k].w; 34 return ; 35 } 36 if(tree[k].f) down(k); 37 int m=(tree[k].l+tree[k].r)/2; 38 if(x<=m) ask_point(k*2); 39 else ask_point(k*2+1); 40 } 41 inline void change_point(int k)//单点修改 42 { 43 if(tree[k].l==tree[k].r) 44 { 45 tree[k].w+=y; 46 return; 47 } 48 if(tree[k].f) down(k); 49 int m=(tree[k].l+tree[k].r)/2; 50 if(x<=m) change_point(k*2); 51 else change_point(k*2+1); 52 tree[k].w=tree[k*2].w+tree[k*2+1].w; 53 } 54 inline void ask_interval(int k)//区间查询 55 { 56 if(tree[k].l>=a&&tree[k].r<=b) 57 { 58 ans+=tree[k].w; 59 return; 60 } 61 if(tree[k].f) down(k); 62 int m=(tree[k].l+tree[k].r)/2; 63 if(a<=m) ask_interval(k*2); 64 if(b>m) ask_interval(k*2+1); 65 } 66 inline void change_interval(int k)//区间修改 67 { 68 if(tree[k].l>=a&&tree[k].r<=b) 69 { 70 tree[k].w+=(tree[k].r-tree[k].l+1)*y; 71 tree[k].f+=y; 72 return; 73 } 74 if(tree[k].f) down(k); 75 int m=(tree[k].l+tree[k].r)/2; 76 if(a<=m) change_interval(k*2); 77 if(b>m) change_interval(k*2+1); 78 tree[k].w=tree[k*2].w+tree[k*2+1].w; 79 } 80 int main() 81 { 82 scanf("%d",&n);//n个节点 83 build(1,1,n);//建树 84 scanf("%d",&m);//m种操作 85 for(int i=1;i<=m;i++) 86 { 87 scanf("%d",&p); 88 ans=0; 89 if(p==1) 90 { 91 scanf("%d",&x); 92 ask_point(1);//单点查询,输出第x个数 93 printf("%d",ans); 94 } 95 else if(p==2) 96 { 97 scanf("%d%d",&x,&y); 98 change_point(1);//单点修改 99 } 100 else if(p==3) 101 { 102 scanf("%d%d",&a,&b);//区间查询 103 ask_interval(1); 104 printf("%d ",ans); 105 } 106 else 107 { 108 scanf("%d%d%d",&a,&b,&y);//区间修改 109 change_interval(1); 110 } 111 } 112 }
来一波纯线段树模板。
#include<cstdio> #include<cstring> #include<cmath> #include<algorithm> using namespace std; struct sd{ int l,r; long long w,f; }; sd tree[400005]; long long res; void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; } void build(int l,int r,int k) { tree[k].l=l; tree[k].r=r; if(l==r) { scanf("%d",&tree[k].w); return; } int m=(l+r)/2; build(l,m,k*2); build(m+1,r,k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } void add(int v,int x,int y,long long k) { if(tree[v].l>=x&&tree[v].r<=y) { tree[v].w+=(tree[v].r-tree[v].l+1)*k; tree[v].f+=k; return; } if(tree[v].f) down(v); int m=(tree[v].l+tree[v].r)/2; if(m<y)add(v*2+1,x,y,k); if(m>=x)add(v*2,x,y,k); tree[v].w=tree[v*2].w+tree[v*2+1].w; } void put(int v,int x,int y) { if(tree[v].l>=x&&tree[v].r<=y) { res=res+tree[v].w; return; } if(tree[v].f)down(v); int m=(tree[v].l+tree[v].r)/2; if(m<y)put(v*2+1,x,y); if(m>=x)put(v*2,x,y); } int main() { int n,m; scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { int order; scanf("%d",&order); if(order==1) { int x,y; long long k; scanf("%d%d%lld",&x,&y,&k); add(1,x,y,k); } if(order==2) { res=0; int x,y; scanf("%d%d",&x,&y); put(1,x,y); printf("%lld ",res); } } return 0; }
最后来几道练习题