线段树初步:建树、单点查改、区间查询
线段树是一种二叉搜索树 ,与区间树 相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点 ---- 百度百科
说真的,线段树真的是个超级超级棒的数据结构(๑•̀ㅂ•́)و✧真的相当好用,理解难度低应用广泛还代码好写,初期可能代码上有点难度,但是熟练后就会发现她的美!
进入正题,本期重点:
1、线段树建树
2、单点查询
3、单点修改
4、区间查询
一、线段树建树
先来说说什么是线段树,线段树就是一个二叉树,它的每一个子节点的范围都是半个父节点所占的范围,而且左右儿子的范围之和就是父节点所占的范围
举个例子吧
1,2,3,4,5,6,7,8,9,10是给定的一串数字,以它为根节点建线段树
首先以给定数字作为根节点,如果把这是个数存在数组a里,在这里就是a[n]=n(我存数组喜欢从1开始),那么根节点的范围就是1<=n<=10
然后我们把这十个数分成两份----1,2,3,4,5和6,7,8,9,10,分别作为根节点的左儿子和右儿子,即根节点包含1~10,那么它的左儿子就是1~5,右儿子是6~10,即可得到如下图
以此类推我们继续往下分
最后就可以完成整个二叉树的建树了
用左边界和右边界来表示范围,如图
这样看就可以更直观的发现一个节点的区间为m,n;那么他的左儿子节点区间就是[m,(m+n)/2];右儿子节点区间就是[(m+n)/2+1,n];
来个例子看看吧?
我输入一行数,以他们的和建线段树
这是一个线段树建树的模板
来看看
先来看看节点怎么存
1 const int mm=100005;
2 struct tree{
3 int l,r; //左边界和右边界(left,right)
4 int sum; //存的是l,r这个区间的和
5 }a[mm*4];
每个节点三个元素,左边界(l),右边界(r),和在这个区域内的和(sum),节点a[n]的左右儿子就为a[n*2]和a[n*2+1]
接下来来看建树
我们先将一堆数字录入到in[mm]中
我们建树也是要一个节点一个节点建的,从根节点开始,
1 void build(int l,int r,int num) //l是左区间,r是右区间,num是节点编号
如果有不熟悉递归框架的朋友可以看这里:0基础算法基础学算法 第六弹 递归 - 球君 - 博客园 (cnblogs.com)
因为num的左右节点就是l和r了,所以。。。
void build(int l,int r,int num)
{
a[num].l=l;
a[num].r=r;
}
直接把l和r装上
当l和r相等之时,就不能在往下分了,而满足这个条件的num的sum就是in[l];
于是有了如下代码
void build(int l,int r,int num)
{
a[num].l=l;
a[num].r=r;
if(l==r)
{
a[num].sum=in[l];
return ;
}
}
那如果这一节点还可以再往下分,那就再分两半,一个区间为[l,(l+r)/2]另一个区间为[(l+r)/2+1,r],往下递归
1 void build(int l,int r,int num)
2 {
3 a[num].l=l;
4 a[num].r=r;
5 if(l==r)
6 {
7 a[num].sum=in[l];
8 return ;
9 }
10 int mid=(l+r)/2;
11 build(l,mid,num*2);
12 build(mid+1,r,num*2+1);
13 }
通过这个图,我们可以看出,一个父亲节点的sum就是两个儿子节点的sum之和
即如下代码
1 void build(int l,int r,int num)
2 {
3 a[num].l=l;
4 a[num].r=r;
5 if(l==r)
6 {
7 a[num].sum=in[l];
8 return ;
9 }
10 int mid=(l+r)/2;
11 build(l,mid,num*2);
12 build(mid+1,r,num*2+1);
13 a[num].sum=a[num*2].sum+a[num*2+1].sum;
14 }
以上就是建树部分的代码了,这时候我们再加上主函数进行录入,和存线段树用的结构体
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int mm=100005;
4 int in[mm];
5 struct tree{
6 int l,r;
7 int sum;
8 }a[mm*4];
9 void build(int l,int r,int num)
10 {
11 a[num].l=l;
12 a[num].r=r;
13 if(l==r)
14 {
15 a[num].sum=in[l];
16 return ;
17 }
18 int mid=(l+r)/2;
19 build(l,mid,num*2);
20 build(mid+1,r,num*2+1);
21 a[num].sum=a[num*2].sum+a[num*2+1].sum;
22 }
23 int main(){
24 //freopen("in.txt","r",stdin);
25 //freopen("out.txt","w",stdout);
26 for(int i=1;i<=10;i++)
27 {
28 cin>>in[i];
29 }
30 build(1,m,1);
31 return 0;
32 }
就是完整的线段树建树部分的代码了
再加一行代码调试一下
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int mm=100005;
4 int in[mm];
5 struct tree{
6 int l,r;
7 int sum;
8 }a[mm*4];
9 void build(int l,int r,int num)
10 {
11 a[num].l=l;
12 a[num].r=r;
13 if(l==r)
14 {
15 a[num].sum=in[l];
16 return ;
17 }
18 int mid=(l+r)/2;
19 build(l,mid,num*2);
20 build(mid+1,r,num*2+1);
21 a[num].sum=a[num*2].sum+a[num*2+1].sum;
22 }
23 int main(){
24 //freopen("in.txt","r",stdin);
25 //freopen("out.txt","w",stdout);
26 for(int i=1;i<=10;i++)
27 {
28 cin>>in[i];
29 }
30 build(1,10,1);
31 cout<<a[1].sum;
32 return 0;
33 }
结果如下
正常输出
来看下面一道题目,先只用完成建树的部分
P1816 忠诚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这里的建树和标准的建树几乎一模一样,只是每次建树时的sum改成存这个节点区间内的最小值minn就好
前面这段除了改变了sum变成minn以外没有区别
1 void build(int l,int r,int num)
2 {
3 a[num].l=l;
4 a[num].r=r;
5 if(l==r)
6 {
7 a[num].minn=in[l];
8 return ;
9 }
10 int mid=(l+r)/2;
11 build(l,mid,num*2);
12 build(mid+1,r,num*2+1);
13 }
只是最后父亲节点不再是将两个儿子节点的sum累加了,而是选取两个minn中的最小值
即完整建树
void build(int l,int r,int num)
{
a[num].l=l;
a[num].r=r;
if(l==r)
{
a[num].minn=in[l];
return ;
}
int mid=(l+r)/2;
build(l,mid,num*2);
build(mid+1,r,num*2+1);
a[num].minn=min(a[num*2].minn,a[num*2+1].minn) ;
}
暂时就是这些了,接下来的部分等一会再细讲
二、线段树单点查询
线段树是递归建树的,单点查询也是由递归完成的
大致思想就是从根节点开始,如果儿子节点的区间包含要查询的元素,就往下递归
在刚刚建树的基础上输入一个k,查询输入的第k个数是啥
来分析一下,在线段树中查询一个元素,就需要从根节点开始,哪边儿子包含这个元素就往他那里走,当这个节点就是原来要查询的节点时,输出值,返回
先来看看当这个节点完全等于要查询的元素下标时候
1 void search(int l,int r,int num,int res)//目前区间,目前编号,目标位置
2 {
3 if(r==res&&l==res)
4 {
5 cout<<a[num].sum;
6 return ;
7 }
8 }
直接输出,然后立刻返回
为了方便后边调用,我们设置两个变量
int mid=(l+r)/2;
tree an1=a[num*2];
然后就是十分重要的判断----判断左右儿子哪一个的区间包含查询的目的位置
判断条件很容易写,即儿子的左区间小于等于这个下标,右区间大于等于这个下标,如果不满足则就往另一儿子处走
即
1 if(an1.l<=res&&an1.r>=res)
2 {
3 search(l,mid,num*2,res);
4 }
5 else
6 {
7 search(mid+1,r,num*2+1,res);
8 }
这里可以看到我们只判断一边的儿子是否包含下标是因为,开始的时候,根节点包含所有录入的数,可以保证能包含此下标,然后后面的每一步都是在包含这个下标的区域去递归,所以就形成了一个非黑即白的场面
看看完整代码
#include<bits/stdc++.h>
using namespace std;
const int mm=100005;
int in[mm];
int n,m;
struct tree{
int l,r;
int sum;
}a[mm*4];
void build(int l,int r,int num)
{
a[num].l=l;
a[num].r=r;
if(l==r)
{
a[num].sum=in[l];
return ;
}
int mid=(l+r)/2;
build(l,mid,num*2);
build(mid+1,r,num*2+1);
a[num].sum=a[num*2].sum+a[num*2+1].sum;
}
void search(int l,int r,int num,int res)//目前区间,目前编号,目标位置
{
if(r==res&&l==res)
{
cout<<a[num].sum;
return ;
}
int mid=(l+r)/2;
tree an1=a[num*2];
if(an1.l<=res&&an1.r>=res)
{
search(l,mid,num*2,res);
}
else
{
search(mid+1,r,num*2+1,res);
}
return ;
}
int main(){
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>in[i];
}
build(1,n,1);
cout<<a[1].sum<<endl;
cin>>m;
search(1,n,1,m);
return 0;
}
n是你要输入的元素个数,in是输入的元素,m是要查询的下标,输出的第一行是你输入的元素之和,第二行是你所查询的下标的元素
很多人可能看到这里会像,这不是多此一举吗?直接cout<<in[n];不好吗?那么,接下来的单点查询就要用到这个思路了
三,单点修改
设想一下,对线段树底部的一个元素进行修改,是不是他的爸爸,爷爷,祖先都要发生更改?
因此,所有区间内包含了这个元素的节点都要进行改变。
如果从根节点开始递归,那就按照“单点查询”路线将走过的所有节点都改一遍。
比如我们此时在输入一个数s,表示在输入数组in中的下标,把他改成d,那么沿途所有节点的sum都得增加(d-in[s])
1 void reload(int l,int r,int num,int res,int exc)//新增的一个元素是要改成的数值
2 {
3 if(r==res&&l==res)
4 {
5 return ;
6 }
7 int mid=(l+r)/2;
8 tree an1=a[num*2];
9 if(an1.l<=res&&an1.r>=res)
10 {
11 reload(l,mid,num*2,res,exc);
12 }
13 else
14 {
15 reload(mid+1,r,num*2+1,res,exc);
16 }
17 return ;
18 }
首先大体上与查询一样,新增了一个变量exc,然后去掉了查询时的输出
接下来再加上一句灵性的
a[num].sum+=exc-in[res];
看看完整效果
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int mm=100005;
4 int in[mm];
5 int n,m,s,d;
6 struct tree{
7 int l,r;
8 int sum;
9 }a[mm*4];
10 void build(int l,int r,int num)
11 {
12 a[num].l=l;
13 a[num].r=r;
14 if(l==r)
15 {
16 a[num].sum=in[l];
17 return ;
18 }
19 int mid=(l+r)/2;
20 build(l,mid,num*2);
21 build(mid+1,r,num*2+1);
22 a[num].sum=a[num*2].sum+a[num*2+1].sum;
23 }
24 void search(int l,int r,int num,int res)//目前区间,目前编号,目标位置
25 {
26 if(r==res&&l==res)
27 {
28 cout<<a[num].sum<<endl;
29 return ;
30 }
31 int mid=(l+r)/2;
32 tree an1=a[num*2];
33 if(an1.l<=res&&an1.r>=res)
34 {
35 search(l,mid,num*2,res);
36 }
37 else
38 {
39 search(mid+1,r,num*2+1,res);
40 }
41 return ;
42 }
43 void reload(int l,int r,int num,int res,int exc)//新增的一个元素是要改成的数值
44 {
45 a[num].sum+=exc-in[res];
46 if(r==res&&l==res)
47 {
48 return ;
49 }
50 int mid=(l+r)/2;
51 tree an1=a[num*2];
52 if(an1.l<=res&&an1.r>=res)
53 {
54 reload(l,mid,num*2,res,exc);
55 }
56 else
57 {
58 reload(mid+1,r,num*2+1,res,exc);
59 }
60 return ;
61 }
62 int main(){
63 freopen("in.txt","r",stdin);
64 freopen("out.txt","w",stdout);
65 cin>>n;
66 for(int i=1;i<=n;i++)
67 {
68 cin>>in[i];
69 }
70 build(1,n,1);
71 cout<<a[1].sum<<endl;
72 cin>>m;
73 search(1,n,1,m);
74 cin>>s>>d;
75 reload(1,n,1,s,d);
76 cout<<a[1].sum<<endl;
77 return 0;
78 }
以上代码就是关于建树,单点查改的全部内容了😵
四,区间查询
区间查询和单点查询是有点相似的,只不过这里并不需要一查到底,倘若某节点的区间在查询区间以内,就将该节点的区间拿出来累加,如果只有一部分在,那就继续向下走
区间查询要求:输入两个数,求出in中下标为两个数间的和;
我们来重新搬出这幅图
比如,我们要查询区间为[3,7]内的数字之和
[1,10]的范围大了,完全包含了[3,7],而且要查询的区间的左边界比[1,10]区间的中点小,因此向左儿子递归,而要查询的区间的右边界比[1,10]区间的中点大,所以还可以向右儿子递归
再看到左儿子那里,查询区间的左边界比它的中点大因此不用往左儿子递归了,往右儿子递归;至于右儿子那边,左边界比mid小,右边界也比mid小所以往左儿子继续递归
如图
因为[3,5],[6,7]节点都完全被查询区间盖满了,所以就不必再往下递归,最后的结果便是这两的sum之和
来看看代码怎么写
这次查找我们带入5个变量,分别是该节点编号,查询区间,目前节点区间
void found(int num,int l,int r,int ll,int rr)
当该节点区间被查询区间完全覆盖时,就说明这是所需要查询的一部分,用ans累加
当该节点左右的区间不足触碰到查询区间边界时,说明走过头了,得回去
回溯部分
1 void found(int num,int l,int r,int ll,int rr)
2 {
3 if(a[num].l>=l&&a[num].r<=r)
4 {
5 ans+=a[num].sum;
6 return ;
7 }
8 if(a[num].r<l||a[num].l>r)
9 {
10 return ;
11 }
12 }
就像我之前一样,为了方便我们再定义一个mid
int mid=(a[num].l+a[num].r)/2;
按照我们前文讨论的思路,当l<=mid时就往左儿子递归,当r>mid时就往右儿子递归
看看整体效果
1 void found(int num,int l,int r,int ll,int rr)
2 {
3 if(a[num].l>=l&&a[num].r<=r)
4 {
5 ans+=a[num].sum;
6 return ;
7 }
8 if(a[num].r<l||a[num].l>r)
9 {
10 return ;
11 }
12 int mid=(a[num].l+a[num].r)/2;
13 if(l<=mid)
14 {
15 found(2*num,l,r,ll,mid);
16 }
17 if(r>mid)
18 {
19 found(2*num+1,l,r,mid+1,rr);
20 }
21 return ;
22 }
这样就可以实现区间查询了,最后再带上主函数以及前面讲的内容
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int mm=100005;
4 int in[mm];
5 int n,m,s,d,a1,b1;
6 int ans;
7 struct tree{
8 int l,r;
9 int sum;
10 }a[mm*4];
11 void build(int l,int r,int num)
12 {
13 a[num].l=l;
14 a[num].r=r;
15 if(l==r)
16 {
17 a[num].sum=in[l];
18 return ;
19 }
20 int mid=(l+r)/2;
21 build(l,mid,num*2);
22 build(mid+1,r,num*2+1);
23 a[num].sum=a[num*2].sum+a[num*2+1].sum;
24 }
25 void search(int l,int r,int num,int res)//目前区间,目前编号,目标位置
26 {
27 if(r==res&&l==res)
28 {
29 cout<<a[num].sum<<endl;
30 return ;
31 }
32 int mid=(l+r)/2;
33 tree an1=a[num*2];
34 if(an1.l<=res&&an1.r>=res)
35 {
36 search(l,mid,num*2,res);
37 }
38 else
39 {
40 search(mid+1,r,num*2+1,res);
41 }
42 return ;
43 }
44 void reload(int l,int r,int num,int res,int exc)//新增的一个元素是要改成的数值
45 {
46 a[num].sum+=exc-in[res];
47 if(r==res&&l==res)
48 {
49 return ;
50 }
51 int mid=(l+r)/2;
52 tree an1=a[num*2];
53 if(an1.l<=res&&an1.r>=res)
54 {
55 reload(l,mid,num*2,res,exc);
56 }
57 else
58 {
59 reload(mid+1,r,num*2+1,res,exc);
60 }
61 return ;
62 }
63 void found(int num,int l,int r,int ll,int rr)
64 {
65 if(a[num].l>=l&&a[num].r<=r)
66 {
67 ans+=a[num].sum;
68 return ;
69 }
70 if(a[num].r<l||a[num].l>r)
71 {
72 return ;
73 }
74 int mid=(a[num].l+a[num].r)/2;
75 if(l<=mid)
76 {
77 found(2*num,l,r,ll,mid);
78 }
79 if(r>mid)
80 {
81 found(2*num+1,l,r,mid+1,rr);
82 }
83 return ;
84 }
85 int main(){
86 //freopen("in.txt","r",stdin);
87 //freopen("out.txt","w",stdout);
88 cin>>n;
89 for(int i=1;i<=n;i++)
90 {
91 cin>>in[i];
92 }
93 build(1,n,1);
94 cout<<a[1].sum<<endl;
95 cin>>m;
96 search(1,n,1,m);
97 cin>>s>>d;
98 reload(1,n,1,s,d);
99 cout<<a[1].sum<<endl;
100 /*reload(1,n,1,s,d);
101 cout<<a[1].sum<<endl;*///可以用作把改过的改回来
102 cin>>a1>>b1;
103 found(1,a1,b1,1,n);
104 cout<<ans<<endl;
105 return 0;
106 }
以上就是本讲的建树,单点查询,单点修改和区间查询的全部内容了,代码在这↑,很快我应该就会更新带懒操作的线段树查改了,敬请期待!
如果您感觉本文对您有帮助,千万不要吝啬手上的赞和关注,最后,感谢您的访问,再会!