【数据结构】 线段树详解

本文是个人通过部分博文学习线段树的笔记,并且写一下自己的理解。文首标明参考文章出处。本文部分内容借鉴自于:

目录

一. 背景概述

二. 实现思路

1. 关系划分

2. 区间统计

3. 点修改

三. 实现代码

1. 建树

2. 查询区间

3. 点修改

4. 总板子(HDU - 1166 排兵布阵)

四. 例题分析

1. HDU - 1754 I Hate It

2. HDU - 2795   Billboard

五. 线段树的区间修改

1. 问题背景

2. 实现代码

3. 示例分析

1. HDU - 1698   Just a Hook

2. Can you answer these queries? HDU - 4027


一. 背景概述

        听名字,线段树,是来处理一段一段的区间的。我们来看一个例题:

题意

给出n个数字序列,给出m个操作,每个操作输入a , b, c

  • 若a == 1 :求出 [ b, c ] 区间内数字的和是多少
  • 若a == 2:修改b处数字+c

        这个题应该怎么来解决?普通的我们求和可能先预处理一下前缀和,但是修改某一点的话,我们所有的前缀和都要修改一遍,这里求和快修改慢;如果每次是一个个求和,求和太慢但是修改只修改一个点就行了,这里求和慢修改快。

        那有什么办法求和也快修改也快呢?这里就有了线段树 :把前缀和建在树上,前缀的是子节点的和,修改时只修改与修改点有联系的父节点,这就是线段树在该题的思路,也是线段树产生的背景。

二. 实现思路

1. 关系划分

        假设我们区间是  [ 1 , 13 ] , 首先我们来划分区间父子节点关系如下图:

.

        怎么划分?即自上而下二分:给定区间[L,R],只要L < R ,线段树就会把它继续分裂成两个区间。

        首先计算 M = (L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就递归分解。

2. 区间统计

        接下来如何来进行区间统计呢?这个以数组 [ 1 , 13 ] = { 1 , 2 , 3 , 4  , 1 , 2 , 4 , 1 , 3 , 4 } 为例,,其父节点前缀和如图:

        每一个父节点都是其子节点的和,这样递归合并下去。如果我们要求[ 4 , 11 ] 的和:首先自上而下:sum = 0

(1)二分 [ 1 , 13 ] 为 [ 1,7] + [8 , 13] :4<=7&&11>7说明要求的区间有两部分分别在 [ 1,7 ]和 [ 8 , 13 ]里面,继续递归

(2) 二分 [ 1 , 7] 为 [ 1 , 4 ] + [5 , 7 ]:在 [ 1,7 ]里面的那部分又分为了 [ 1 , 4 ] 和 [5 , 7 ]两部分,而4<=4&&11>4,继续递归

(3)二分 [ 8 , 13]为 [ 8 ,10 ] + [11 , 13] :在 [ 8,13 ]里面的那部分又分为了 [ 8 , 10 ] 和 [11 , 13 ]两部分,而4<=10&&11>10,继续递归

(4)二分 [ 1 , 4] 为 [ 1 , 2 ] + [3 , 4 ]:在 [ 1,4 ]里面的那部分又分为了 [ 1 , 2 ] 和 [3 , 4 ]两部分,而11>2但4也>2,只递归右子树,说明[1,2]并不在[ 4 ,11 ]里面

(5)现在递归到[5 , 7 ] ,发现[5 , 7 ]全部都在 [ 4, 11 ]里面 ,sum+=ans[ 5 ,7 ] ;return;

(6)...................

        这样一直递归下去,最后求出的就是要查询的区间和。结束Over。

3. 点修改

        接下来如何进行点修改呢?假设我们这里要修改的点是 这个位置:

        我们发现要修改的地方只是该节点的父节点即可,仍是递归实现即可。

三. 实现代码

        思考对于每个区间节点我们怎么表示呢?可以采用传统的 n 的左子节点是 n<<1 , 右子节点是 n<<1|1来用数组保存。所以这里保存节点的数组大小最好开到长度的四倍。

const int maxn = 50000 + 10;
int num[maxn];
int sum[maxn<<2];//保存区间节点
int n;

1. 建树

void BuildTree(int l,int r,int root)//当前区间左边界l , 右边界 r , 当前节点
{
    if(l==r){//递归到叶子赋值
        sum[root] = man[l];
        return;
    }
    int m = (l+r)>>1;
    BuildTree(l,m,root<<1);//递归左右子树
    BuildTree(m+1,r,root<<1|1);
    sum[root] = sum[root<<1] + sum[root<<1|1];//更新该节点
}

2. 查询区间

int Query(int L,int R,int l,int r,int root)//要查询的区间 [ L,R ],当前区间[l,r],当前节点
{
    if(L<=l&&R>=r){//如果完全包含了这个区间,直接返回
        return sum[root];
    }
    int m = (l+r)>>1;
    int ANS = 0;//记录左右子树和
    if(L<=m)ANS+=Query(L,R,l,m,root<<1);//判断能否继续递归
    if(R>m)ANS+=Query(L,R,m+1,r,root<<1|1);
    return ANS;
}

3. 点修改

void Update(int p,int c,int l,int r,int root)//要修改的点和+c ,当前区间 [l,r],当前节点
{
    if(l==r){//递归到叶子就是要修改的子位置
        sum[root]+=c;
        return;
    }
    int m = (l+r)>>1;
    if(p<=m)Update(p,c,l,m,root<<1);//判断要修改点的位置
    else if(p>m)Update(p,c,m+1,r,root<<1|1);
    sum[root] = sum[root<<1]+sum[root<<1|1];//更新节点
}

4. 总板子(HDU - 1166 排兵布阵)

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn = 50000 + 10;
int num[maxn];
int sum[maxn<<2],n;
void BuildTree(int l,int r,int root)
{
    if(l==r){
        sum[root] = man[l];
        return;
    }
    int m = (l+r)>>1;
    BuildTree(l,m,root<<1);
    BuildTree(m+1,r,root<<1|1);
    sum[root] = sum[root<<1] + sum[root<<1|1];
}
int Query(int L,int R,int l,int r,int root)
{
    if(L<=l&&R>=r){
        return sum[root];
    }
    int m = (l+r)>>1;
    int ANS = 0;
    if(L<=m)ANS+=Query(L,R,l,m,root<<1);
    if(R>m)ANS+=Query(L,R,m+1,r,root<<1|1);
    return ANS;
}
void Update(int p,int c,int l,int r,int root)
{
    if(l==r){
        sum[root]+=c;
        return;
    }
    int m = (l+r)>>1;
    if(p<=m)Update(p,c,l,m,root<<1);
    else if(p>m)Update(p,c,m+1,r,root<<1|1);
    sum[root] = sum[root<<1]+sum[root<<1|1];
}
int main()
{
    int T;
    int t = 0;
    scanf("%d",&T);
    while(T--){
        t++;
        scanf("%d",&n);
        for(int i = 1;i<=n;i++){
            scanf("%d",&man[i]);
        }
        BuildTree(1,n,1);
        string op;
        printf("Case %d:\n",t);
        while(cin>>op&&op!="End"){
            int a,b;
            if(op=="Query"){
                scanf("%d%d",&a,&b);
                int ans = Query(a,b,1,n,1);
                printf("%d\n",ans);
            }
            else if(op=="Add"){
                scanf("%d%d",&a,&b);
                man[a]+=b;
                Update(a,b,1,n,1);
            }
            else if(op=="Sub"){
                scanf("%d%d",&a,&b);
                man[a]-=b;
                Update(a,-b,1,n,1);
            }
        }
    }
    return 0;
}

四. 例题分析

1. HDU - 1754 I Hate It

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn  = 200000 + 10;
int stu[maxn];
int great[maxn<<2],n,m;//求区间最值
void BuildTree(int l,int r,int root)
{
    if(l==r){
        great[root] = stu[l];
        return;
    }
    int m = (l+r)/2;
    BuildTree(l,m,root<<1);
    BuildTree(m+1,r,root<<1|1);
    great[root] = max(great[root<<1],great[root<<1|1]);

}
int Query(int L,int R,int l,int r,int root)
{
    if(L<=l&&R>=r){
        return great[root];
    }
    int m = (l+r)/2;
    int lm = -1,rm = -1;
    if(L<=m)lm = Query(L,R,l,m,root<<1);
    if(R>m)rm = Query(L,R,m+1,r,root<<1|1);
    return max(lm,rm);
}
void Update(int p,int c,int l,int r,int root)
{
    if(l==r){
        great[root] = c;
        return;
    }
    int m = (l+r)/2;
    if(p<=m)Update(p,c,l,m,root<<1);
    else if(p>m)Update(p,c,m+1,r,root<<1|1);
    great[root] = max(great[root<<1],great[root<<1|1]);
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF){
        for(int i = 1;i<=n;i++){
            scanf("%d",&stu[i]);
        }
        BuildTree(1,n,1);
        for(int i = 1;i<=m;i++){
            char op;
            int a,b;
            getchar();
            scanf("%c%d%d",&op,&a,&b);
            if(op=='Q'){
                int ans = Query(a,b,1,n,1);
                printf("%d\n",ans);
            }
            else if(op=='U'){
                stu[a] = b;
                Update(a,b,1,n,1);
            }
        }
    }
    return 0;
}

2. HDU - 2795   Billboard

(1)题意:给你一块公告板的长 h ,宽 w。给你  n  块公告依次输入他们的宽度,高度都为单位高度,输入顺序代表张贴时间,现在让你输出每块公告张贴的高度,要求张贴位置尽量靠上靠左。(1 <= h,w <= 10^9; 1 <= n <= 200,000)

(2)分析:给你一块公告,我们肯定先从上往下找某个高度的宽度能>=他的地方来张贴这个公告 。 所以我们可以用sum来保存每个区间的当前的最大宽度。如果sum[1](树根)>=公告宽度就说明树里面肯定有能放下这个公告的位置,如果<公告宽度则一定没有。那怎么表示从上到下呢?我们知道小区间是在左子树,所以我们递归的时候优先判断左子树是否可用即可。然后找到叶子修改。

(3)注意:本题目一大坑点,h的范围使用不到的,因为n最大是200000,每个公告是单位长度,就算我每个高度贴一个,最多也就用200000,所以h之取到n范围,大于的取n即可;

(4)代码实现:

#include <iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 200000 + 5;
int sum[maxn<<2];
int h,w,n;
void CreatTree(int l,int r,int root)
{
    if(l==r){
        sum[root] = w;//初始化宽度都为w
        return;
    }
    int m = (l+r)/2;
    CreatTree(l,m,root<<1);
    CreatTree(m+1,r,root<<1|1);
    sum[root] = max(sum[root<<1],sum[root<<1|1]);
}
void Update(int l,int r,int root,int k)
{
    if(l==r){
        sum[root]-=k;//更新同时打印高度
        printf("%d\n",l);
        return;
    }
    int m = (l+r)/2;
    if(k<=sum[root<<1]){//优先判断左子树
        Update(l,m,root<<1,k);
    }
    else Update(m+1,r,root<<1|1,k);
    sum[root] = max(sum[root<<1],sum[root<<1|1]);
}
int main()
{
    while(scanf("%d%d%d",&h,&w,&n)!=EOF){
        if(h>n)h = n;//坑点
        CreatTree(1,h,1);
        for(int i = 1;i<=n;i++){
            int len;
            scanf("%d",&len);
            if(len>sum[1]){
                printf("-1\n");
            }
            else Update(1,h,1,len);
        }
    }
    return 0;
}

五. 线段树的区间修改

1. 问题背景

        在上述问题的基础上,如果我要是给你把区间 [ L , R] 的数字都加上 c,你会怎么去修改?

  • 做法一:枚举区间,进行R - L + 1次点修改
  • 做法二:暴力递归,在修改时递归到每一个子节点进行修改(这里只给出做法二的代码):
void Update(int L,int R,int l,int r,int root,int k)
{
      if(l==r){
         sum[root] += k;
         return;
      }
      int m = (l+r)/2 ;
      if(L<=m)Update(L,R,l,m,root<<1,k);
      if(R>m)Update(L,R,m+1,r,root<<1|1,k);
      sum[root] = sum[root<<1] + sum[root<<1|1];
}

        上面做法是可行的,但是花费时间太长了,修改所有子节点和他们的父节点。那有什么办法加快速度呢?我们发现:

        如果我们要让[ 8, 13 ]里面的都加上c ,那我们直接让 [ 8, 13 ] 这个节点加上 (13 - 8 + 1)*c ,不就得了嘛!

        但是如果我们下面还要修改查询呢?子节点还是要修改的!但是我们万一不查询呢。所以这里利用这个特点,引入一个"延时标记"表示这个区间是要修改的,但是子节点还没修改。我们在查询或者修改时遇到这个标记就先更新他的子节点。但是我们在最初修改时只递归到这一层就可以返回了:

  • [ 8, 13 ] 这个节点加上 (13 - 8 + 1)*c 保证上层结果正确;
  • [ 8, 13 ] 这个节点标记上延时标记,保证下层可被修改结果正确;

        加快速度保证结果两全其美何乐而不为!

2. 实现代码

void PushDown(int l,int r,int root)
{
    if(Change[root]){//如果被标记说明该节点下的子节点还没有更新
        //下推标记
        Change[root<<1] += Change[root];//子节点标记更新+=
        Change[root<<1|1] += Change[root];
        //更新子节点
        int m = (l+r)/2;
        sum[root<<1] += (m - l + 1)*Change[root];
        sum[root<<1|1] += (r - m)*Change[root];
        //清零标记
        Change[root] = 0;
    }
    return;
}
void Update(int L,int R,int l,int r,int root,int k)
{
      if(L<=l&&R>=r){
          sum[root] += (r - l + 1)*k;//直接修改大区间保证上层结果正确
          Change[root] += k;//叠加延迟标记,保证下层结果
          return;
      }
      int m = (l+r)/2 ;
      PushDown(l,r,root);//下推标记先更新,再递归
      if(L<=m)Update(L,R,l,m,root<<1,k);
      if(R>m)Update(L,R,m+1,r,root<<1|1,k);
      sum[root] = sum[root<<1] + sum[root<<1|1];
}

int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
	if(L <= l && r <= R){
		//在区间内,直接返回 
		return Sum[rt];
	}
	int m=(l+r)>>1;
	//下推标记,否则Sum可能不正确
	PushDown(rt,m-l+1,r-m); 
	
	//累计答案
	int ANS=0;
	if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
	if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1);
	return ANS;
}

3. 示例分析

1. HDU - 1698   Just a Hook

        注意:要看清题目是区间累加x,还是区间都修改为x;一个是+=x,一个是=x,区别还是很大的。这里就是区间修改为x

#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 100000 + 10;
int sum[maxn<<2];
int Change[maxn<<2];
int len,q;
void PushDown(int l,int r,int root)
{
    if(Change[root]){
        //下推标记
        Change[root<<1] = Change[root];
        Change[root<<1|1] = Change[root];
        //更新子节点
        int m = (l+r)/2;
        sum[root<<1] = (m - l + 1)*Change[root];
        sum[root<<1|1] = (r - m)*Change[root];
        //清零标记
        Change[root] = 0;
    }
    return;
}
void CreatTree(int l,int r,int root)
{
    if(l==r){
        sum[root] = 1;
        return;
    }
    int m = (l+r)/2;
    CreatTree(l,m,root<<1);
    CreatTree(m+1,r,root<<1|1);
    sum[root] = sum[root<<1] + sum[root<<1|1];
}
void Update(int L,int R,int l,int r,int root,int k)
{
      if(L<=l&&R>=r){
          sum[root] = (r - l + 1)*k;
          Change[root] = k;//延迟标记
          return;
      }
      int m = (l+r)/2 ;
      PushDown(l,r,root);
      if(L<=m)Update(L,R,l,m,root<<1,k);
      if(R>m)Update(L,R,m+1,r,root<<1|1,k);
      sum[root] = sum[root<<1] + sum[root<<1|1];
}
int main()
{
    int T;
    scanf("%d",&T);
    int t = 0;
    while(T--){
        t++;
        memset(Change,0,sizeof(Change));
        scanf("%d",&len);
        CreatTree(1,len,1);
        scanf("%d",&q);
        for(int i = 1;i<=q;i++){
            int x,y,op;
            scanf("%d%d%d",&x,&y,&op);
            Update(x,y,1,len,1,op);
        }
        printf("Case %d: The total value of the hook is %d.\n",t,sum[1]);
    }
    return 0;
}

2. Can you answer these queries? HDU - 4027

(1)题意:修改区间 [ L, R] 的数字开方,查询区间和

(2)分析:woc?区间修改是对每个数字不同处理?区间修改代码怎么做。这时我们可以暴力递归来解决区间修改但是不同处理的情况,但是当一个区间sum[l,r] = l - r + 1时,说明这里的所有数字都变成1了,不用修改了,可以用这个来加快速度,否则超时。

(3)代码实现:

#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
const int maxn = 100000 + 10;
LL sum[maxn<<2];
LL num[maxn];
int n;
void BuildTree(int l,int r,int root)
{
    if(l==r){
        sum[root] = num[l];
        return;
    }
    int m = (l+r)/2;
    BuildTree(l,m,root<<1);
    BuildTree(m+1,r,root<<1|1);
    sum[root] = sum[root<<1] + sum[root<<1|1];
}
LL Query(int L,int R,int l,int r,int root)
{
    if(L<=l&&R>=r){
        return sum[root];
    }
    int m = (l+r)/2;
    LL ans = 0;
    if(L<=m)ans+=Query(L,R,l,m,root<<1);
    if(R>m)ans+=Query(L,R,m+1,r,root<<1|1);
    return ans;
}
void Update(int L,int R,int l,int r,int root)
{
    if(l==r){
        sum[root] = (LL)sqrt(sum[root]);
        return;
    }
    if(L<=l&&R>=r&&sum[root]==r - l +1)return;
    int m = (l+r)/2;
    if(L<=m)Update(L,R,l,m,root<<1);
    if(R>m)Update(L,R,m+1,r,root<<1|1);
    sum[root] = sum[root<<1] + sum[root<<1|1];
}
int main()
{
    int t = 0;
    while(scanf("%d",&n)!=EOF){
        t++;
        for(int i = 1;i<=n;i++){
            scanf("%lld",&num[i]);
        }
        BuildTree(1,n,1);
        int m;
        scanf("%d",&m);
        printf("Case #%d:\n",t);
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            if(b>c)swap(b,c);
            if(a){
                LL k = Query(b,c,1,n,1);
                printf("%lld\n",k);
            }
            else{
                Update(b,c,1,n,1);
            }
        }
        printf("\n");
    }
    return 0;
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿阿阿安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值