转载声明
本文为了弥补自己打的代码不知道为什么过不了luogu而作
只摘录重要的内容(个人认为)
原出处: Senior Data Structure · 浅谈线段树(Segment Tree)
简介
ps : _此处以询问区间和为例。实际上线段树可以处理很多符合结合律的操作。(比如说加法,a[1]+a[2]+a[3]+a[4]=(a[1]+a[2])+(a[3]+a[4]))
线段树之所以称为“树”,是因为其具有树的结构特性。线段树由于本身是专门用来处理区间问题的(包括 RMQ 、 RSQ 问题等。
思想:
线段树就是分块思想的树化,或者说是对于信息处理的二进制化
通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成 k 个所分块与 m 个单个元素的信息的并 (0<=k,m<=logn)(0<=k,m<=logn)
建树和维护
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操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。(这其实是正确性的证明啦)
呐,我们在这儿就能看出来,实际上 push_up是在合并两个子节点的信息,所以需要信息满足结合律!
对于建树: 我们应该维护父子节点的关系
区间修改
- 思想详见出处
- 懒标记
首先,懒标记的作用是记录每次、每个节点要更新的值,也就是 delta,但线段树的优点不在于全记录(全记录依然很慢qwq),而在于传递式记录:
整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。
如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时 pushdown一次,以免重复或者冲突或者爆炸 qwq
那么对于 pushdown而言,其实就是纯粹的 push up的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导 lazy tag
那么问题来了:怎么传导 pushdown呢?这里很有意思,开始回溯时执行 push up ,因为是向上传导信息;那我们如果要让它向下更新,就调整顺序,在向下递归的时候 pushdown不就好惹~ qwq:
注: ans[]为需要维护的
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);
//回溯之后
}
查询区间
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;
}
标程
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
inline ll ls(ll x)
{
return x<<1;
}
inline ll rs(ll x)
{
return x<<1|1;
}
void scan()
{
cin>>n>>m;
for(ll i=1;i<=n;i++)
scanf("%lld",&a[i]);
}
inline void push_up(ll p)
{
ans[p]=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 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;
}
int main()
{
ll a1,b,c,d,e,f;
scan();
build(1,1,n);
while(m--)
{
scanf("%lld",&a1);
switch(a1)
{
case 1:{
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,1,n,1,d);
break;
}
case 2:{
scanf("%lld%lld",&e,&f);
printf("%lld
",query(e,f,1,n,1));
break;
}
}
}
return 0;
}
以下为luogu听课时的代码
如果看懂了上面的,就完全不用看下面的了,博主只是觉得下面的码风比较亲民...
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,a[N],m;
int sumv[N<<2],addv[N<<2];
int pushup(int o){sumv[o]=sumv[o<<1]+sumv[o<<1|1];}
void build(int o,int l,int r){
addv[o]=0;//注意初始化
if(l==r){sumv[o]=a[l];return;}
int mid=(l+r)>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
pushup(o);
}
inline void puttag(int o,int l,int r,int v){//puttag的意思是到了该修改信息的结点
addv[o]+=v;sumv[o]+=(r-l+1)*v;//先记录一下tag,然后算出本节点增加的sum(注;增加的是v,这里的addv只是标记
}
void pushdown(int o,int l,int r){
if(addv[o]==0)return;//注意addv可能为0
addv[o<<1]+=addv[o];
addv[o<<1|1]+=addv[o];
int mid=(l+r)>>1;
sumv[o<<1]+=addv[o]*(mid-l+1);//注意这是 ..'+'= addv['o']...
sumv[o<<1|1]+=addv[o]*(r-mid);//r-(mid+1)+1
addv[o]=0;
}
void optadd(int o,int l,int r,int ql,int qr,int v){
if(ql<=l&&r<=qr){puttag(o,l,r,v);return;}//到了结点
int mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid)optadd(o<<1,l,mid,ql,qr,v);
if(qr>mid)optadd(o<<1|1,mid+1,r,ql,qr,v);
pushup(o);
}
int querysum(int o,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr)return sumv[o];//到了结点
int ans=0;int mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid)ans+=querysum(o<<1,l,mid,ql,qr);
if(qr>mid)ans+=querysum(o<<1|1,mid+1,r,ql,qr);//注:是mid “<” qr
return ans;
}
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 opt;scanf("%d",&opt);
if(opt==1){int l,r,v;scanf("%d%d%d",&l,&r,&v);optadd(1,1,n,l,r,v);}
if(opt==2){int l,r;scanf("%d%d",&l,&r);printf("%d
",querysum(1,1,n,l,r));}
}
}
几句有用的话:
需要修改某点,但是却不访问它,这样就没必要执行修改操作,所以线段树的区间修改有了lazytag
修改了就需要push_up
记得addv和sumv一起改
不要老是想着非叶子结点的lazytag,它会在修改和访问的时候下传,而在下传之前pushtag操作和pushdown操作就把sumv等附加信息更新好了(即下传的时候更新),所以在query的时候直接返回了sumv,所以记得addv和sumv一起改
修改和查询时没有完全覆盖的话,都需要push_down(其实这个理解了上面的原理就都懂了,不懂的看下面...(原谅语文不好...0.0
(修改时没有全覆盖的话,就需要下传addv了,这时用子节点的信息更新本节点的信息,然后这时候就不用操心本节点的addv啦(如果覆盖了就直接修改嘛,注意:一定要更新本节点的信息,不然在本节点的信息就是错的,万一在查询的时候刚好覆盖了本节点,那么返回的就是个错误的信息
(查询没有被完全覆盖的区间时,只有先push_down, 算出子节点的信息了,才能算本节点在[ql,qr]范围内的附加信息(如果覆盖了就直接返回嘛,原因如上