线段树初步:建树、单点查改、区间查询

  线段树是一种二叉搜索树 ,与区间树 相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点 ---- 百度百科

  说真的,线段树真的是个超级超级棒的数据结构(๑•̀ㅂ•́)و✧真的相当好用,理解难度低应用广泛还代码好写,初期可能代码上有点难度,但是熟练后就会发现她的美!

  进入正题,本期重点:

    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 }

   以上就是本讲的建树,单点查询,单点修改和区间查询的全部内容了,代码在这↑,很快我应该就会更新带懒操作的线段树查改了,敬请期待!

  如果您感觉本文对您有帮助,千万不要吝啬手上的赞和关注,最后,感谢您的访问,再会! 

posted @ 2021-08-27 22:19  球君  阅读(630)  评论(0编辑  收藏  举报
View Code