[模版]线段树

(欢迎咨询QQ3228648095)

线段树

线段树是个啥呀

线段树是个毛???在前面我只能告诉你又长又骚。线段树的代码,板子题都有将近百行,稍微有点思想的题目估计就要上一百五十行了。然后差错的时候哦查到你心累,这就是线段树……

算了还是来点正能量的吧。

线段树有着不可比拟的有点,它是可以快捷地对一段数据进行在线维护、查询的数据结构,查询和维护的时间复杂度都是(log2 n)这就很舒服了。

基本原理

线段树是一种二叉搜索树,它将一个区间放在根结点里面,然后不断把区间进行分割,直到分割成一个单位区间(可以理解为一个单结点区间,或者一个不可分割的区间),然后把所有单位区间都放在叶子结点里面,我们查找的时候就可以通过树枝进行搜,如果遇到一个区间和需要查找的区间是包含关系,那就不用查找了,直接返回这个区间的相应参数,如果交叉关系,就继续分割递归,如果没有关系,那就直接return结束本层递归。

对比

和RMQ的对比
RMQ求区间最大值运用了倍增的思想,在预处理后,可以在常数时间内完成查询,也就是查询的时间复杂度是O(1),而预处理的时间复杂的是O(nlog2 n),看起来是不是比这个线段树要好很多?但是——RMQ算法是局限于离线查询,不支持更改,如果要更改的话,时间复杂度蹭蹭蹭上去,而且代码复杂度也很高,我试过,头都大了。但是线段树是支持更改的在线数据结构。
和数组直接存储的对比
数组直接存储不需要预处理,修改需要O(1)的时间,但是查找单点要O(n),查找区间的话要O(n^2)的时间。太慢了(我曹。

线段树的实现

线段树真是个好玩意儿,我们怎么实现它呢?

存储

线段树在本质上是一棵完全二叉树(想一想,为什么)
线段树的存储有很多种办法,但是最省空间的方法是:采用堆结构。对于每一个结点i,a[i]就来存储这个结点,a[2*i]来存储这个点的左孩子,a[2 *i+1]来存储这个点的右孩子。没有优化的空间复杂度是O(2n),但是数组往往需要开到4n,否则会溢出空间哦。
(待更新)

查找和更新

线段树本来就是一种二叉搜索树,查找和更新的时候就可以从根结点一层一层向下递归就ok了。

一个极好的优化

我们会发现,如果对于一棵线段树,我们每次都进行到底的搜索或修改,我们会浪费很多时间,导致tle很多点。我们会发现对于同一个元素,我们在修改维护的时候会经过一次,而且在询问的时候又会经过一次。这样对于多询问题来说就会重复判断操作。我们这里有一个解决办法——延迟修改。
我们一般称之为lazy标记(很懒,只能留到下次经过的时候再处理),然后做法就很显然了:如果当前遇到的区间被要操作的区间所覆盖,那就直接对这个根结点进行标记,以下的子结点就不用再找了,直接return掉。

各种模板

我们的模板是基于一个很好的板子题进行的:

题目

题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.将某区间每一个数乘上x
3.求出某区间每一个数的和

输入输出格式
输入格式:
第一行包含三个整数N、M、P,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数乘上k
操作2: 格式:2 x y k 含义:将区间[x,y]内每个数加上k
操作3: 格式:3 x y 含义:输出区间[x,y]内每个数的和对P取模所得的结果
输出格式:
输出包含若干行整数,即为所有操作3的结果。

一些这道题必备的思路

简单来说,如果要修改一个区间的值,如果这个区间刚好被完全包含了就直接返回,打上标记(要改成多少),那么下次要用子节点的时候将标记下传即可,显然,对于本题要打两个tag,那么sum[o] = sum[o] * mul[o] + add[o],sum为和,mul为乘的数,add为加的数,那么如果我们乘一个数c,sum[o] = sum[o] * mul[o] * c + add[o] * c,将mul[o] * c和add[o] * c看作两个整体,那么可以发现如果乘一个数c的话,add数组要*c,mul数组也要*c,同理,如果加一个数c,那么只需要add数组+c即可,因为在下传标记的时候既有加,又有减(可能为0),所以add和mul数组的计算一定要将这两种情况都计算到.如果在线段树上几个区间操作的性质相同的,先把要求的数用操作需要的量给表示出来,然后对于每一种操作看看需要维护的标记的变化,最后合并一下即可.

声明

在上模板代码之前,我们先用了这个头文件:

#include<bits/stdc++.h>

这两个宏定义:

#define le l,mid,o*2
#define re mid+1,r,o*2+1

n用来存储原数列的个数,m用来存储操作的个数,p用来存储需要mod的数,a数组用来存原数列,add数组用来存加法的lazy标记,mul用来存乘法的lazy标记,sum用来存累加和(或对p取模后的值);x和y代表需要更改的区间的两个端点。

建树

void build(long long l,long long r,long long o)
{
    mul[o]=1;//乘法基数1
    add[o]=0;//加法基数0
    if(l==r)//如果这个区间两端点重合啦,就是锁定单点了,这个区间只有一个元素
    {
        sum[o]=a[l];//这个区间的总和就是元素自身了
        return;
    }
    int mid=(l+r)>>1;//获取中间点
    build(le);//继续造树
    build(re);//十年树木百年树人
    sum[o]=(sum[o*2]+sum[o*2+1])%p;//第o个点的累加和是两个孩子的累加和之和
}

传递lazy标记

void pushdown(int o,int k)
{
    sum[o*2]=(sum[o*2]*mul[o]+add[o]*(k-(k>>1)))%p;//左子树的累加和更新
    sum[o*2+1]=(sum[o*2+1]*mul[o]+add[o]*(k>>1))%p;//右子树的累加和更新
    //更新的顺序:先乘法标记的更新,再加上累加标记的更新。并且累加标记的更新还要注意累加的次数,一个是(k-(k>>1)),另一个是(k>>1)。
    mul[o*2]=mul[o*2]*mul[o]%p
    mul[o*2+1]=mul[o*2+1]*mul[o]%p;
    //乘法标记也要更新的。千万别忘记
    add[o*2]=(add[o*2]*mul[o]+add[o])%p;
    add[o*2+1]=(add[o*2+1]*mul[o]+add[o])%p;
    //加法标记更新的时候要先考虑乘法标记的更新。
    mul[o]=1;
    add[o]=0;
    //标记清空。
}

区间自加

void jia(int l,int r,int o)//三个参数,l和r代表当前在查找的区间的两个端点,o代表当前查找的区间的根结点
{
    if(x<=l&&r<=y)//如果区间完全被覆盖
    {
        add[o]=(add[o]+v)%p;//对lazy标记直接累加v,并且模p(一步一模可以避免最后long long溢出的问题)
        sum[o]=(sum[o]+v*(r-l+1))%p;//对于该点的累加和也要相应加上每一个单点的改变量
        return;//lazy标记的初衷,直接退出
    }
    pushdown(o,r-l+1);//传递lazy标记
    int mid=(l+r)>>1;//获取区间中点
    if(x<=mid)
    jia(le);//如果x在中点左边,就说明有必要搜索左子树
    if(y>mid)
    jia(re);//如果y在中点右边,就说明有必要搜索右子树
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
//是时候更新根结点的累加和了。

区间乘法

void cheng(int l,int r,int o)
{
    if(x<=l&&r<=y)
    //如果区间完全包含
    {
        add[o]=(add[o]*v)%p;
        mul[o]=(mul[o]*v)%p;
        sum[o]=(sum[o]*v)%p;
            //累加和、乘法标记、加法标记全部更新。
        return;//更新完就直接返回,不用多说话。
    }
    pushdown(o,r-l+1);//传递标记
    int mid=(l+r)>>1;
    if(x<=mid)
    cheng(le);
    if(y>mid)
    cheng(re);
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
    //这一段同上文加法。
}

区间查找

long long query(int l,int r,int o)
{
    if(x<=l&&r<=y)
    return sum[o]%p;
    //如果完全包含,直接返回值(lazy标记已经被pushdown了)
    int mid=(l+r)>>1;//取中点
    pushdown(o,r-l+1);//传递标记
    long long temp=0;
    if(x<=mid)
    temp=(temp+query(le))%p;
    //左子树的累加和
    if(y>mid)
    temp=(temp+query(re))%p;
    //右子树的累加和
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
    //更新根结点
    return temp%p;
    //返回最后值
}

主函数的一些边缘操作

int main()
{
    n=read();
    m=read();
    p=read();
    //快读(见下)
    for(int i=1;i<=n;i++)
    a[i]=read();
    //读入数据
    build(1,n,1);
    //构造树
    for(int i=1;i<=m;i++)
    {
        pattern=read();
        //读入操作类型
        if(pattern==1)
        {
            x=read();
            y=read();
            v=read();
            cheng(1,n,1);
        }//1就是乘法
        if(pattern==2)
        {
            x=read();
            y=read();
            v=read();
            jia(1,n,1);
        }//2就是加法
        if(pattern==3)
        {
            x=read();
            y=read();
            printf("%lld\n",query(1,n,1)%p);
        }//3就是查找区间累加和并取模
    }
    return 0;
}

快读

这是个好技能。希望大家get到

inline long long read()
{
    long long num=0;
    char c=getchar();
    for(;c<'0'||c>'9';c=getchar());
    for(;c>='0'&&c<='9';c=getchar())num=num*10+c-'0';
    return num;
}
以下是acwing模板线段树的示例代码: ```cpp const int N = 100010; int n, m; int a[N]; struct Node { int l, r; int v, lazy; } tree[N * 4]; void pushup(int x) { tree[x].v = tree[x * 2].v + tree[x * 2 + 1].v; } void pushdown(int x) { if (tree[x].lazy) { int l = tree[x].l, r = tree[x].r; int mid = (l + r) >> 1; tree[x * 2].v += tree[x].lazy * (mid - l + 1); tree[x * 2 + 1].v += tree[x].lazy * (r - mid); tree[x * 2].lazy += tree[x].lazy; tree[x * 2 + 1].lazy += tree[x].lazy; tree[x].lazy = 0; } } void build(int x, int l, int r) { tree[x].l = l, tree[x].r = r; if (l == r) { tree[x].v = a[l]; return; } int mid = (l + r) >> 1; build(x * 2 l, mid); build(x * 2 + 1, mid +1, r); pushup(x); } void modify(int x, int l, int r, int val) { if (tree[x].l >= l && tree[x].r <= r) { tree[x].v += val * (tree[x].r - tree[x].l + 1); tree[x].lazy += val; return; } pushdown(x); int mid = (tree[x].l + tree[x].r) >> 1; if (l <= mid) modify(x * 2, l, r, val); if (r > mid) modify(x * 2 + 1, l, r, val); pushup(x); } int query(int x, int l, int r) { if (tree[x].l >= l && tree[x].r <= r) { return tree[x].v; } pushdown(x); int mid = (tree[x].l + tree[x].r) >> 1; int sum = 0; if (l <= mid) sum += query(x * 2, l, r); if (r > mid) sum += query(x * 2 + 1, l, r); return sum; } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; } build(1, 1, n); while (m--) { int op, l, r, val; cin >> op; if (op == 1) { cin >> l >> r >> val; modify(1, l, r, val); } else if (op == 2) { cin >> l >> r; cout << query(1, l, r) << endl; } } return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值