线段树基础模板

你以为我是树状数组?
其实我是线段树哒!

这两个东西作为同是数据结构又同是树状的东西,很让人迷糊,
你看ta们的题号:


然而看代码长度就容易区分多了

那么ta们之间到底有什么联系(废话,都是树状呗)与不同呢(名称)
先讲完线段树再归纳整理吧
线段树可维护的东西可比上面的辣鸡ST和树状数组多(好像后面就不用归纳整理了...)
ta是这样一种结构(这好像还是个二叉搜索树...):

当然这是将区间以数字形式表现,也可以直观表现,像这样:

也是像树状数组一样的,将大区间拆分为小的,查询时选取相应区间做指定运算就是了

区间查询的具体原理是这样的:
设立查询区间和目前区间几个递归函数参数,当然需要一个变量\(k\)来储存与传递当前线段树点的编号(线段树当然要用树来存啦,\(k\)节点的子节点编号为\(2*k\)\(2*k+1\))
每次判断当前区间与目标区间的关系,如果包含就意味着要返回当前节点的总和,如果只包含一部分,先设立变量\(mid\),判断其左右区间是否在其中,如果在就继续递归,如果不在就不进行任何操作,所以在函数递归过程中不会出现任何完全不包含情况,
主要就是设立一个要进行\(return\)语句的变量,将左右区间(在范围内的值)加入,最后返回

并且与树状数组不同的是,线段树支持区间更新,
蓝书上举了一堆关于朴素算法和前缀和的东西,来印证线段树多么美好...
并不想喷蓝书...
但如果像树状数组一样,每次都将维护进行到底,也就是说把涉及更改区间的所有节点更新一遍,无疑是非常浪费时间的算法,
因为更新到的节点并不一定会用到,要么就是用到的时候又加上了好几层元素,就是说在使用之前经历了好几次更新,然而我只访问了一次,这无疑非常浪费,毕竟花了时间去更新其值多次,然而只有一次使用

那么能不能在保存值的基础上,尽量的少用或是不用那些被更新但是不访问的变量呢?
现在考虑给每个点多设一个变量(每个与节点对应),表示这个点的更新的量,然而并没有加进每个线段树的节点内,等到这个点被访问再进行调用处理,
这么一想发明这东西的人好懒...拖延症哦,
这不就是我嘛

那么我们称这个东西为懒标记(\(Lazy-Tag\)),
对于每个节点,可以有这样一个懒标记(程序中是\(laz\),\(r\)\(l\)分别指区间右端点和左端点,下文一样用),储存当前这个\(k\)节点的每个元素加了\(laz[k]\)的值,再访问时,给当前节点加上\(laz[k]*(r-l+1)\),这样就是这个区间总的变化量,
那么有人要问,如果这个节点的元素并不是加的一个统一量,也就是说其中的元素分不同批次不同区间进行了值的更改,再简而言之,就是再多次操作中同一个节点区间的元素加了不同的值,如何处理,其正确性如何?
其实不用担心,每次在值的更新时,要先进行一步判断,看当前区间是否被更新(是否在更新的目标区间内),道理同上面的查询(突然发现我打了好多字哦...)
当前节点值的更新必不可少的就是标记的\(pushdown\)操作,就是把标记传给后代,再清除自身标记
每次该函数先判断是否包含,如果全包含就把该节点对应值加上\(laz[k]*(r-l+1)\),再将标记\(pushdown\)
如果不,就把标记\(pushdown\),再进行值的修改,递归子区间,反正最终的节点值是左区间和加上右区间和,不管是否包含,
如此一来,就把所有的区间值高效处理好了,

当然还有算法是将标记永久化,就是把初始值作一个数组,更新值是另一个数组,就是将更新值永远储存在节点中,并不会将标记消除,
个人不喜欢,所以不用,毕竟消除标记方便多种运算的使用,
这就是理论,现在放代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=100005;
int n,m;
ll sum[4*N];
ll laz[4*N];
ll a[N];
inline void build(const int &k,const int &l,const int &r){
    if(l==r){
        sum[k]=a[l];
        return ;
    }
    int mid=l+r>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    sum[k]=sum[k<<1]+sum[k<<1|1];
}
inline void add(const int &k,const int &l,const int &r,const ll &v){
    laz[k]+=v;
    sum[k]+=(r-l+1)*v;
    return ;
}
inline void pushdown(const int &k,const int &l,const int &r){
    if(!laz[k]) return ;
    int mid=l+r>>1;
    add(k<<1,l,mid,laz[k]);
    add(k<<1|1,mid+1,r,laz[k]);
    laz[k]=0;
}
inline void insert(const int &k,const int &l,const int &r,const int &x,const int &y,const ll &v){
    if(x<=l&&r<=y){
        add(k,l,r,v);
        return ;
    }
    pushdown(k,l,r);
    int mid=l+r>>1;
    if(x<=mid) insert(k<<1,l,mid,x,y,v);
    if(mid<y) insert(k<<1|1,mid+1,r,x,y,v);
    sum[k]=sum[k<<1]+sum[k<<1|1];
}
inline ll find(const int &k,const int &l,const int &r,const int &x,const int &y){
    if(x<=l&&r<=y) return sum[k];
    pushdown(k,l,r);
    int mid=l+r>>1;
    ll rtn=0;
    if(x<=mid) rtn+=find(k<<1,l,mid,x,y);
    if(mid<y) rtn+=find(k<<1|1,mid+1,r,x,y);
    return rtn;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    build(1,1,n);
    while(m--){
        int n1,n2,n3;
        scanf("%d",&n1);
        if(n1==1){
            ll n4;
            scanf("%d%d%lld",&n2,&n3,&n4);
            insert(1,1,n,n2,n3,n4);
        }
        else{
            scanf("%d%d",&n2,&n3);
            printf("%lld\n",find(1,1,n,n2,n3));
        }
    }
    return 0;
}

1.好长的模板题呀(虽然有更长的)
2.函数加上\(const\)和引用符可以更快,正确性喜闻乐见的自己证明

那么重点来了,前面整理的\(ST\)表,树状数组,线段树到底联系在哪,区别在哪?
先说\(ST\),其由于结构较简单,只能维护较简单的信息,也就求求最大值最小值
那么树状数组就要稍微好些,可以维护前缀和,前缀最大最小值,前缀积,但是其缺陷就是在于只能维护"前缀",求

猜你喜欢

转载自www.cnblogs.com/648-233/p/11130534.html