线段树是一种二叉搜索树 ,与区间树 相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点 ---- 百度百科
说真的,线段树真的是个超级超级棒的数据结构(๑•̀ㅂ•́)و✧真的相当好用,理解难度低应用广泛还代码好写,初期可能代码上有点难度,但是熟练后就会发现她的美!
进入正题,本期重点:
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 }
以上就是本讲的建树,单点查询,单点修改和区间查询的全部内容了,代码在这↑,很快我应该就会更新带懒操作的线段树查改了,敬请期待!
如果您感觉本文对您有帮助,千万不要吝啬手上的赞和关注,最后,感谢您的访问,再会!