基(là)础(jī)线段树详解

本文详细介绍了一种高效的数据结构——线段树,通过实例讲解如何利用线段树处理区间加法和区间求和的问题,并提供了完整的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目来源:https://www.luogu.org/problemnew/show/3372,侵删。

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和

输入输出格式

输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式:
输出包含若干行整数,即为所有操作2的结果。

输入输出样例

输入样例#1:
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出样例#1:
11
8
20
说明

时空限制:1000ms,128M

数据规模:

对于30%的数据:N<=8,M<=10

对于70%的数据:N<=1000,M<=10000

对于100%的数据:N<=100000,M<=100000

(数据保证在int64/long long数据范围内)

样例说明:
这里写图片描述
 
  对于较小的数据,当然可以用数组模拟,代码简单人见人爱。但针对较大数据就会果断GG,所以需要使用线段树,线段树在处理较大数据时有奇效,时间复杂度为O(logn),但占用空间也相对较大,达到4n。
   
  (1)建树
  线段树的主要思想是二分,将一个大区间不停二分直到成为一个单点,如图:
  这里写图片描述
  在对区间进行操作时,只需要对大区间进行操作,不用下到每个叶结点,查询时同理。
  建树时,我们将线段树储存为线性结构,又因为该线段树为完全二叉树结构,所以对于每一个父节点x[v],x[v×2]和x[v×2+1]为其子节点。而对于每一个元素,需要记录的变量有:左区间(le),右区间(ri)。此外,根据题意,还应记录该区间的元素和(sum)以及区间操作的加数(ad),所以定义结构体如下:

struct sd{
    int le,ri;
    long long sum,ad;
    sd()
    {
        memset(this,0,sizeof(this));
    }
};

  其中,this指向该结构体中的元素。memset语句则将该结构体中的所有元素初始化为0。
  记录加数(ad)的目的是延后对区间加法的运算,在查询时再调用。另外,该加数针对的是其子节点的加数,而非自己的。
  然后就是建树操作了,将子节点的区间和合并到父节点上,如果该节点已经是最底层叶节点(即左右区间相等),那么就从读入的初始数据中赋值。建树时我们采用递归操作,每次都进行二分,遇到叶结点时回溯,合并区间。
  注意:对二分点(mid)所属区间的划分,否则会莫名RE,在这里博主定义左区间为[le,mid],右区间为(mid,ri]。在后面进行区间加/查询时也要遵守建树时的定义。
  代码如下:

void up(int v)
{
    tree[v].sum=tree[v*2].sum+tree[v*2+1].sum;//合并自己子区间的和
}
void build(int v,int le,int ri)
{
    tree[v].le=le;
    tree[v].ri=ri;//确定区间范围
    if(le==ri)
    {
        tree[v].sum=x[le];
        return;//当递归到叶结点时,回溯
    }
    int mid=(le+ri)/2;
    build(v*2,le,mid);
    build(v*2+1,mid+1,ri);//递归二分
    up(v);//合并子区间
}

  在进行区间加的操作时,同样采用递归二分进行赋值,遇到被完全包含的区间时就停止递归,将该区间的区间和进行修改,然后将加数传递给子区间

void pushdown(int t,int le,int ri)
{//将加数传递给子区间
    int mid=(le+ri)/2;
    tree[t*2].ad+=tree[t].ad;
    tree[t*2+1].ad+=tree[t].ad;//传递加数
    tree[t*2].sum+=(mid-le+1)*tree[t].ad;
    tree[t*2+1].sum+=(ri-mid)*tree[t].ad;//修改区间和
    tree[t].ad=0;//传递完毕后将父区间的加数归零
}
void add(int t,int le,int ri,int ll,int rr,int ad)
{
    if(ll<=le&&ri<=rr)
    {//如果遇到完全包含的区间,更改其加数与区间和后回溯
        tree[t].ad+=ad;
        tree[t].sum+=(ri-le+1)*ad;
        return;
    }
    if(tree[t].ad!=0)
    {//如果该区间有加数,将加数传递给子区间,为后面的二分递归做准备
        pushdown(t,le,ri);
    }
    int mid=(le+ri)/2;
    if(ll<=mid)
    {//没有完全包含则二分递归继续进行区间加
        add(t*2,le,mid,ll,rr,ad);
    }
    if(rr>mid)
    {
        add(t*2+1,mid+1,ri,ll,rr,ad);
    }
    up(t);//更改父节点
}

  然后就是查询操作,同样递归二分。参数从左到右依次为:当前查询的节点编号,当前查询到的左、右区间以及要求查询的左、右区间。

long long qsum(int t,int le,int ri,int ll,int rr)
{
    if(ll<=le&&ri<=rr)
    {//如果该区间被完全包含,返回该区间的和
        return tree[t].sum;
    }
    if(tree[t].ad!=0)
    {//和区间加一样,更改子结点的值为二分递归查找做准备
        pushdown(t,le,ri);
    }
    int mid=(le+ri)/2;long long s=0;
    if(ll<=mid)
    {//二分查找
        s=qsum(t*2,le,mid,ll,rr);
    }
    if(mid<rr)
    {
        s+=qsum(t*2+1,mid+1,ri,ll,rr);
    }
    //返回左右两个区间查询得到的结果之和
    return s;
}

  基本的函数都已经介绍完毕,在调用函数进行区间加/查询时,需要将当前查询的节点编号(t),当前查询到的左(le)、右(ri)区间都设为最大的区间(即t=1,le=1,ri=n),剩下的都交给递归解决。
  最后贴上博主丑陋的代码:

#include<cstdio>
#include<cstring>
using namespace std;
struct sd{
    int le,ri;
    long long sum,ad;
    sd()
    {
        memset(this,0,sizeof(this));
    }
};
long long x[100005];
sd tree[400005];
void up(int v)
{
    tree[v].sum=tree[v*2].sum+tree[v*2+1].sum;
}
void build(int v,int le,int ri)
{//建树
    tree[v].le=le;
    tree[v].ri=ri;
    if(le==ri)
    {
        tree[v].sum=x[le];
        return;
    }
    int mid=(le+ri)/2;
    build(v*2,le,mid);
    build(v*2+1,mid+1,ri);
    up(v);
}
void pushdown(int t,int le,int ri)
{
    int mid=(le+ri)/2;
    tree[t*2].ad+=tree[t].ad;
    tree[t*2+1].ad+=tree[t].ad;
    tree[t*2].sum+=(mid-le+1)*tree[t].ad;
    tree[t*2+1].sum+=(ri-mid)*tree[t].ad;
    tree[t].ad=0;
}
void add(int t,int le,int ri,int ll,int rr,int ad)
{//区间加
    if(ll<=le&&ri<=rr)
    {
        tree[t].ad+=ad;
        tree[t].sum+=(ri-le+1)*ad;
        return;
    }
    if(tree[t].ad!=0)
    {
        pushdown(t,le,ri);
    }
    int mid=(le+ri)/2;
    if(ll<=mid)
    {
        add(t*2,le,mid,ll,rr,ad);
    }
    if(rr>mid)
    {
        add(t*2+1,mid+1,ri,ll,rr,ad);
    }
    up(t);
}
long long qsum(int t,int le,int ri,int ll,int rr)
{//查询区间和
    if(ll<=le&&ri<=rr)
    {
        return tree[t].sum;
    }
    if(tree[t].ad!=0)
    {
        pushdown(t,le,ri);
    }
    int mid=(le+ri)/2;long long s=0;
    if(ll<=mid)
    {
        s=qsum(t*2,le,mid,ll,rr);
    }
    if(mid<rr)
    {
        s+=qsum(t*2+1,mid+1,ri,ll,rr);
    }
    return s;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x[i]);//读入初始数据
    }
    build(1,1,n);//建树
    for(int i=1;i<=m;i++)
    {
        int p;
        int x,y,k;
        scanf("%d",&p);
        if(p==1)
        {
            scanf("%d%d%d",&x,&y,&k);
            add(1,1,n,x,y,k);//参数都设为最大区间
        }
        else
        {
            scanf("%d%d",&x,&y);
            printf("%lld\n",qsum(1,1,n,x,y));//同上
        }
    }
    return 0;
}

  线段树作为一种高效的数据结构,需要4n的空间作为牺牲(ps:请一定把数组开的够大)但是相应的,面对更大的数据(>=5000000)时内存炸裂依然会GG,那么针对这种情况的优化以(lǎo)后(zi)再(bú)讲(huì)。另外,针对线段树的维护也是一件头疼的事,要维护的量往往不会像例题一样明显,同时也会更加复杂,需要靠大家自己的修为了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShadyPi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值