洛谷 P3372 【模板】线段树 1

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

将某区间每一个数加上 k。
求出某区间每一个数的和。
输入格式
第一行包含两个整数 n, m,分别表示该数列数字的个数和操作的总个数。

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

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

1 x y k:将区间 [x, y] 内每个数加上 kk。
2 x y:输出区间[x, y]内每个数的和。

#include <bits/stdc++.h>
#define inf 1000000009
#define db printf("where!\n");
#define pb push_back
using namespace std;
#define ll long long
#define MP std::make_pair
ll gcd(ll x,ll y){return y ? gcd(y,x%y) : x;}
template<class T>inline void read(T &res){
char c;T flag=1;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
while((c=getchar())>='0'&&c<='9')res=res*10+c-'0';res*=flag;
}
#define maxn 100005
int n;
ll ans[maxn*4],a[maxn*4],tag[maxn*4];
inline ll ls(ll p){return p<<1;}
inline ll rs(ll p){return p<<1|1;}
void push_up(int p)
{
    ans[p]=ans[ls(p)]+ans[rs(p)];
}
void push_up_min(int p)
{
    ans[p]=min(ans[ls(p)],ans[rs(p)]);
}
void build(ll p,ll l,ll r)
{
    tag[p]=0;
    if(l==r) {ans[p]=a[l];return;}
    ll mid=(l+r)>>1;
    build(ls(p),l,mid);
    build(rs(p),mid+1,r);
    push_up(p);
}
inline void f(ll p,ll l,ll r,ll k)
{
    tag[p]=tag[p]+k;
    ans[p]=ans[p]+k*(r-l+1);
}
inline void push_down(ll p,ll l,ll r)
{
    ll mid=(l+r)>>1;
    f(ls(p),l,mid,tag[p]);
    f(rs(p),mid+1,r,tag[p]);
    tag[p]=0;
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
{
    if(nl<=l&&r<=nr){
        ans[p]+=k*(r-l+1);
        tag[p]+=k;
        return;
    }
    push_down(p,l,r);
    ll mid=(l+r)>>1;
    if(nl<=mid) update(nl,nr,l,mid,ls(p),k);
    if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
    push_up(p);
}
ll query(ll qx,ll qy,ll l,ll r,ll p)
{
    ll res=0;
    if(qx<=l&&r<=qy) return ans[p];
    ll mid=(l+r)>>1;
    push_down(p,l,r);
    if(qx<=mid) res+=query(qx,qy,l,mid,ls(p));
    if(qy>mid) res+=query(qx,qy,mid+1,r,rs(p));
    return res;
}
int main()
{
    int m;read(n),read(m);
    for(int i=1;i<=n;i++) read(a[i]);
    build(1,1,n);
    while(m--){
        int l,r,d;
        int a1;read(a1);
        if(a1==1){
            read(l),read(r),read(d);
            update(l,r,1,n,1,d);
        }
        else{
            read(l),read(r);
            cout<<query(l,r,1,n,1)<<endl;
        }
    }
	return 0;
}

1、建树与维护
由于二叉树的自身特性,对于每个父亲节点的编号i,他的两个儿子的编号分别是2i2i+1,所以我们考虑写两个O(1)的取儿子函数:

int n;
int ans[MAXN*4];

inline int ls(int p){return p<<1;}//左儿子 
inline int rs(int p){return p<<1|1;}//右儿子 

那么根据线段树的服务对象,可以得到线段树的维护:

void push_up_sum(int p){
	t[p]=t[lc(p)]+t[rc(p)];
}//	向上不断维护区间操作 

void push_up_min(int p){//max and min
 t[p]=min(t[lc(p)],t[rc(p)]);
 //t[p]=max(t[lc(p)],t[rc(p)]);             
}

此处一定要注意,push up操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。

那么对于建树,由于二叉树自身的父子节点之间的可传递关系,所以可以考虑递归建树,并且在建树的同时,我们应该维护父子节点的关系:

void build(ll p,ll l,ll r)
{
  if(l==r){ans[p]=a[l];return ;}
  //如果左右区间相同,那么必然是叶子节点啦,只有叶子节点是被真实赋值的
  ll mid=(l+r)>>1;
  build(ls(p),l,mid);
  build(rs(p),mid+1,r);
//此处由于我们采用的是二叉树,所以对于整个结构来说,可以用二分来降低复杂度,
//否则树形结构则没有什么明显的优化
  push_up(p);
//此处由于我们是要通过子节点来维护父亲节点,所以pushup的位置应当是在回溯时。
} 

2、区间修改
为什么不讨论单点修改呢?因为其实很显然,单点修改就是区间修改的一个子问题而已,即区间长度为1时进行的区间修改操作罢了

那么对于区间操作,我们考虑引入一个名叫“lazy tag”(懒标记)的东西——之所以称其“lazy”,是因为原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达O(nlogn)的级别。但当我们引入了懒标记之后,区间更新的期望复杂度就降到了O(logn)的级别且甚至会更低.

(1)首先先来从分块思想上解释如何区间修改:
分块的思想是通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成k个所分块与m个单个元素的信息的并(0<=k,m<=logn)

那么我们可以反过来思考这个问题:对于一个要修改的、长度为l的区间来说,总是可以看做由一个长度为2logn和剩下的元素(或者小区间组成)。那么我们就可以先将其拆分成线段树上节点所示的区间,之后分开处理:

如果单个元素被包含就只改变自己,如果整个区间被包含就修改整个区间

其实好像这个在分块里不是特别简单地实现,但是在线段树里,无论是元素还是区间都是线段树上的一个节点,所以我们不需要区分区间还是元素,加个判断就好。

(2)懒标记的正确打开方式
首先,懒标记的作用是记录每次、每个节点要更新的值,也就是delta,但线段树的优点不在于全记录(全记录依然很慢),而在于传递式记录:

整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。 如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时pushdown一次,以免重复或者冲突

那么对于pushdown而言,其实就是纯粹的pushup的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazy tag

那么问题来了:怎么传导pushdown呢?这里很有意思,开始回溯时执行pushup,因为是向上传导信息;那我们如果要让它向下更新,就调整顺序,在向下递归的时候pushdown

inline void f(ll p,ll l,ll r,ll k)
{
   tag[p]=tag[p]+k;
   ans[p]=ans[p]+k*(r-l+1);
   //由于是这个区间统一改变,所以ans数组要加元素个数次啦 
}
//我们可以认识到,f函数的唯一目的,就是记录当前节点所代表的区间 
inline void push_down(ll p,ll l,ll r)
{
   ll mid=(l+r)>>1;
   f(ls(p),l,mid,tag[p]);
   f(rs(p),mid+1,r,tag[p]);
   tag[p]=0;
   //每次更新两个儿子节点。以此不断向下传递 
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
{
   //nl,nr为要修改的区间
   //l,r,p为当前节点所存储的区间以及节点的编号 
   if(nl<=l&&r<=nr)
   {
   	ans[p]+=k*(r-l+1);
   	tag[p]+=k;
   	return ;
   }
   push_down(p,l,r);
   //回溯之前(也可以说是下一次递归之前,因为没有递归就没有回溯) 
   //由于是在回溯之前不断向下传递,所以自然每个节点都可以更新到 
   ll mid=(l+r)>>1;
   if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
   if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
   push_up(p);
   //回溯之后 
}

对于复杂度而言,由于完全二叉树的深度不超过logn,那么单点修改显然是O(logn)的,区间修改的话,由于我们的这个区间至多分logn个子区间,对于每个子区间的查询是O(1)的,所以复杂度自然是O(logn)不过带一点常数

3、对于区间查询

ll query(ll q_x,ll q_y,ll l,ll r,ll p)
{
	ll res=0;
	if(q_x<=l&&r<=q_y)return ans[p];
	ll mid=(l+r)>>1;
	push_down(p,l,r);
	if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
	if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
	return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值