洛谷 P3372 【模板】线段树 1

题目描述

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

  1. 将某区间每一个数加上 k。
  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

前缀和,差分,树状数组1,树状数组2,都无法解决这道题,最多70分,所以我们要学习线段树 

什么是线段树

线段树是一种二叉搜索树,故线段树满足所有二叉树的规律。一般来说,我们对二叉树从上往下,做左到右的方式进行编号,故线段树的左儿子的编号总为2*i,右儿子的编号总为2*i+1。所以我们得到儿子就非常的方便

因为这种关系在数组上非常容易实现,所以我们把这种关系的数组用树的结构来维护。

取儿子函数

inline int lc(int i){//取左儿子
    return i<<1;
}

inline int rc(int i){//取右儿子
    return i<<1|1;
}

注意,取出来的是下标,因为是下标满足规律

1.
什么是inline
有inline前置的函数,都会在使用函数的地方具体展开
例如调用了 lc(1)  会自动变成 return 1<<1

当代码体量很小的时候,可以减少栈的开销,节约时间和空间。

2.
i<<1是位运算,把i的二进制往左移动1位,缺少的用0补全,即乘2操作
故i<<1|1,的|1就可以实现+1操作,因为是用0补全的,最后一位必定是0

儿子你都会取了,怎么建树呢,赶快端上来罢!,没有学过二叉树的,自行百度二叉树的递归建树。

递归建树

递归之前首先我们要有存放和线段树相关信息的数组。

const int MAXN=1e+5;
int st[MAXN<<2];//segmentTree 直译 线段树

MAXN<<2就是上面提到过的位运算,不过是移动两位,所以相当于乘以4,
MAXN的大小取决于给定序列的长度。

为什么要给线段树开四倍的空间?
我们将原序列的MAXN个数据全部放在二叉树的叶子节点中。

故最底层的叶子节点数量有MAXN个
易得二叉树得高度为log2MAXN,然后计算一下这棵树的所有节点数量,近似取得节点数量为4MAXN。

递归建树

inline void update(int i){
    //区间和
    st[i]=st[lc(i)]+st[rc(i)];

    //如果要最小值可以写成
    //st[i]=min(st[lc(i)],st[rc(i)])
}
void build(int l,int r,int i){
    if(l==r){//两个儿子都没了,即叶子节点
        st[i]=a[l];//给叶子节点赋值,l和r用哪个都一样,反正相等
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid, lc(i));
    build(mid+1,r,rc(i));
    update(i);//更新父节点
}

update是什么东西?你不会自己看函数吗,这一步用来让叶子节点的父亲们都拥有自己的值,为什么update放在这个地方,百度什么是二叉树的后序遍历,不然赋出来的就不是我们想要的结果了。

完成了建树之后,接下来我们就根据树的特性来进行区间修改和区间查询,你说为什么没有单点修改和单点查询?区间修改和区间查询的长度为1不就是单点了吗?,在此之前我们还要知道什么是懒标记。

懒标记:如果对a[1]修改,那么他的父类节点控制的区间都需要都需要加上对a[1]修改的值,所以我们对公共祖先节点打上修改的值,代表他的儿子们都需要被修改,当修改的时候从上往下传递懒标记,让儿子们加上这个值,因为都加过了,所以使用之后清零。

因此我们在对区间进行修改或者查询的时候,都要先下放懒标记。

void push_down(int l,int r,int i){
    if(tag[i]){//有tag的话
        int mid=(l+r)>>1;
        //儿子们的tag加上父的tag
        tag[lc(i)]+=tag[i];
        tag[rc(i)]+=tag[i];
        //儿子们加上父tag传递下来的值
        st[lc(i)]+=tag[i]*(mid-l+1);
        st[rc(i)]+=tag[i]*(r-mid);//注意此处不用加1,因为右孩子掌管的内容不包含mid,具体看build函数
        //父亲的tag归零
        tag[i]=0;
    }
}

区间修改

void add(ll l,ll r,ll i,ll x,ll y,ll k){
    if(x<=l&&y>=r){//如果遍历区间在[x,y]内
        tag[i]+=k;//儿子们的懒标记
        st[i]+=k*(r-l+1);//节点增加值,修改值*区间长度
        return ;//懒标记存在则不需要接下去遍历
    }
    push_down(l,r,i);//下放懒标记
    //遍历所有区间
    ll mid=(l+r)>>1;
    if(x<=mid)add(l,mid, lc(i),x,y,k);
    if(y>mid)add(mid+1,r, rc(i),x,y,k);
    update(i);//更新父节点
}

区间修改和区间查询差不多,因此实际运用的时候完全可以复制粘贴,去掉赋值部分,删掉更新部分,增加返回值即可

区间查询

ll query(ll l,ll r,ll i,ll x,ll y){
    ll ans=0;//这个ans放在哪里都可以,只要在递归之前
    if(x<=l&&y>=r){
        return st[i];
    }
    //遍历所有区间
    ll mid=(l+r)>>1;
    push_down(l,r,i);//下放懒标记
    if(x<=mid)ans+=query(l,mid, lc(i),x,y);
    if(y>mid)ans+= query(mid+1,r, rc(i),x,y);
    //最终返回值
    return ans;
}

AC代码

#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const ll MAXN=1000001;

ll n,m,a[MAXN],st[MAXN<<2],tag[MAXN<<2];
inline ll lc(ll i){
    return i<<1;
}
inline ll rc(ll i){
    return i<<1|1;
}
void update(ll i){
    st[i]=st[lc(i)]+st[rc(i)];
}
void build(ll l,ll r,ll i){
    if(l==r){
        st[i]=a[l];
        return;
    }
    ll mid=(l+r)>>1;
    build(l,mid, lc(i));
    build(mid+1,r, rc(i));
    update(i);
}
void push_down(ll l,ll r,ll i){
    tag[lc(i)]+=tag[i];
    tag[rc(i)]+=tag[i];
    ll mid=(l+r)>>1;
    st[lc(i)]+=tag[i]*(mid-l+1);
    st[rc(i)]+=tag[i]*(r-mid);
    tag[i]=0;
}
void add(ll l,ll r,ll i,ll x,ll y,ll k){
    if(x<=l&&y>=r){
        tag[i]+=k;
        st[i]+=k*(r-l+1);
        return ;
    }
    push_down(l,r,i);
    ll mid=(l+r)>>1;
    if(x<=mid)add(l,mid, lc(i),x,y,k);
    if(y>mid)add(mid+1,r, rc(i),x,y,k);
    update(i);
}
ll query(ll l,ll r,ll i,ll x,ll y){
    ll ans=0;
    if(x<=l&&y>=r){
        return st[i];
    }
    ll mid=(l+r)>>1;
    push_down(l,r,i);
    if(x<=mid)ans+=query(l,mid, lc(i),x,y);
    if(y>mid)ans+= query(mid+1,r, rc(i),x,y);
    return ans;
}

int main(){
    cin>>n>>m;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    build(1,n,1);
    ll op,x,y,k;
    for(ll i=0;i<m;i++){
        scanf("%lld",&op);
        if(op==1){
            scanf("%lld%lld%lld",&x,&y,&k);
            add(1,n,1,x,y,k);
        }else{
            scanf("%lld%lld",&x,&y);
            printf("%lld\n", query(1,n,1,x,y));
        }
    }
    return 0;
}

全部代码都有75行了,树状数组也才20多行,只要不涉及双区间修改的,为了更快做出题目,选谁显而易见。我才不用线段树! 

当然线段树是可以不使用懒标记的,使用懒标记是因为,如果一个控制区间2-4已经被选定了,那就不需要再去修改他的儿子了(大大降低复杂度),只要每次累计标记的值就可以让该用上的用上,不需要的不修改。

如果不加上懒标记,那么修改值时,需要遍历所有修改区间的叶子节点。

无懒标记的修改

void add(ll l,ll r,ll i,ll x,ll y,ll k){
    if(l==r){
        st[i]+=k*(r-l+1);
        return ;
    }
    ll mid=(l+r)>>1;
    if(x<=mid)add(l,mid, lc(i),x,y,k);
    if(y>mid)add(mid+1,r, rc(i),x,y,k);
    update(i);
}

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值