今天博主所在机房的数据结构之王ldxoi神仙于百忙之中抽空给蒟蒻博主和博主的一些神仙同学们讲了一些线段树的操作,只会那几道模板题的博主觉得受益匪浅(ldxoi:这些不都是基本,哦不,底层操作吗? )
ldxoi的数据结构真的讲的特别棒,使得弱如博主都能听懂啊。在此衷心感谢ldxoi在OI和其它学习中对博主的帮助。
ldxoi的各种树型数据结构锦集友链奉上
一.线段树维护区间最大子段和(CDOJ644||校内局域网WOJ2088 程序设计竞赛)
描述
“你动规无力,图论不稳,数据结构松散,贪心迟钝,没一样像样的,就你还想和我同台竞技,做你的美梦!今天这场比赛,就是要让你知道你是多么的无能!!”
不训练,无以为战。有n项能力是ACM竞赛要求的,训练则能提升,忽略则会荒废。
这m天,你能做到如何。
输入
第一行两个整数n,m,分别表示有n项能力要求,共有m天。
第二行n个整数,第i个整数ai表示第i项能力的数值。
接下来m行,每行开始先读入一个整数si,表明这是一次询问还是一次能力变化。
si=0,表明这是一次询问,然后读入两个整数li,ri,表示询问在[li,ri]区间中任选一段连续序列,这段序列中所有能力值之和最大能是多少。
si=1,表明这是一次能力变化,然后读入两个整数xi,wi,表示第xi项能力变为了wi。
1≤n,m≤100000,−10000≤ai≤10000,1≤li≤ri≤n,1≤xi≤n,−10000≤wi≤10000
输出
有多少询问就输出多少行,每行输出一个整数,作为对该询问的回答。
样例输入
4 4
1 2 3 4
0 1 3
1 3 -3
0 2 4
0 3 3
样例输出
6
4
-3
分析:对于一个区间,由于其中可能存在负数,所以其最大子段和很可能不是其区间和。但是,一段序列的最大子段和无非就是三种情况:
然后我们发现,前两种情况可能有重合,但是无所谓;第三种情况通过递归总能转化为前两种情况。
所以我们对于区间[l,r],需要维护4个值:
1.区间总和s;
2.从左端向右拓展得到的最大区间和ls
3.从右端向左拓展得到的最大区间和rs
4.区间最大子段和dat
维护操作,很好理解(很像一个DP啊)
1.t[p].s=t[p* 2].s+t[p* 2+1].s
2.t[p].ls=max(t[p* 2].ls, t[p* 2].s+t[p* 2+1].ls)
3.t[p].rs=max(t[p* 2+1].rs, t[p* 2+1].s+t[p* 2].rs)
4.t[p].dat=max(t[p* 2].dat,max(t[p* 2+1].dat,t[p* 2].rs+t[p* 2+1].ls))
在神仙ldxoi的指点下,博主爱上了重载加法运算符的操作,所以大家在博主之后的代码里会经常看见一个结构体直接加上另一个结构体的操作,果然很爽啊!!!!!
贴代码:
#include
using namespace std;
const int INF=1e9+7;
inline int read(){
char ch;
int flag=1;
while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
int ans=ch-48;
while((ch=getchar())>='0'&&ch<='9') ans=ans*10+ch-48;
return ans*flag;
}
inline void write(int x){
if(x<0) putchar('-') ,x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
int n,m;
int a[100001];
struct node{
int l,r;
int ls,rs,sum,dat;
}tree[400001];
inline void pre1(node &a){
a.ls=a.rs=a.dat=a.sum=-INF;
}
inline node merge(node l,node r){
node ans;
ans.l=l.l,ans.r=r.r;
ans.ls=max(l.ls,l.sum+r.ls);
ans.rs=max(r.rs,r.sum+l.rs);
ans.sum=l.sum+r.sum;
ans.dat=max(l.dat,max(r.dat,l.rs+r.ls));
return ans;
}
inline void build(int p,int l,int r){
tree[p].l=l,tree[p].r=r;
if(l==r){
tree[p].ls=tree[p].rs=tree[p].dat=a[l];
tree[p].sum=a[l];
return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);build((p<<1)+1,mid+1,r);
tree[p]=merge(tree[p<<1],tree[(p<<1)|1]);
}
inline void update(int p,int x,int val){
if(tree[p].l==tree[p].r){
tree[p].sum=val;
tree[p].ls=tree[p].rs=tree[p].dat=val;
return;
}
int mid=(tree[p].l+tree[p].r)>>1;
if(x<=mid) update(p<<1,x,val);
else update((p<<1)|1,x,val);
tree[p]=merge(tree[p<<1],tree[(p<<1)|1]);
}
inline node query(int p,int x,int y){
int l=tree[p].l,r=tree[p].r;
if(x<=l&&y>=r){
return tree[p];
}
int mid=(l+r)>>1;
if(x<=mid&&y>mid){
return merge(query(p<<1,x,mid),query((p<<1)|1,mid+1,y));
}
else if(x<=mid) return query(p<<1,x,y);
else if(y>mid) return query((p<<1)|1,x,y);
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++) a[i]=read();
build(1,1,n);
int f,x,y;
for(int i=1;i<=m;i++) {
f=read(),x=read(),y=read();
if(f==1) update(1,x,y);
else write(query(1,x,y).dat),putchar('\n');
}
return 0;
}
二.线段树维护gcd(待更新TBC)
典例:校内WOJ4351:ldx的sgt水题3
由于ldx大佬过于fake 强大,他的水题我都不会做啊,我是真的菜啊。
由于ldx实在是太菜了,因此他现在需要你的帮助: 现在ldx手上有一个凌乱的序列,有两种操作:
0 l r 表示询问区间[l,r]的gcd
1 l r v 表示给区间[l,r]所有数加上v
请你帮忙回答一下所有0操作的答案,这样的话菜鸡ldx将会感激不尽。
输入
第一行一个数n。 第二行n个数表示初始序列 第二行一个整数m。 接下来m行每行三个0,l,r或者四个整数1,l,r,v。
输出
对于每一个0操作输出一行答案
样例输入 [复制]
10
275480 1115680 151240 679640 1038560 274920 341400 1177800 1153680 194320
10
1 4 10 20
1 3 8 20
1 4 6 40
0 3 8
1 4 5 20
1 3 10 20
0 7 8
1 1 4 40
1 1 6 20
1 6 9 40
样例输出 [复制]
20
60
提示
n≤100000,m≤100000
分析
首先我们明确一点,就是gcd(a,b,c)=gcd(a,gcd(b,c)).
相信欧几里得辗转相除法求gcd的代码大家应该都会写,这里再多说一下:gcd(a,b)=gcd(b,a-b)=gcd(b,b-2*a)…=gcd(b,a%b)。哦,注意加上绝对值函数。所以仔细分析一下我们会发现,对于一个给定的序列,它的gcd值就等于它的差分序列的gcd,也等于它的前缀和序列的gcd值。
这道题的第一个操作是比较简单的,直接用gcd来合并左右儿子的gcd;主要是第二个操作,有点烦人。但是当我们发现了上述性质后,我们可以类比一下用差分数组来维护区间修改时的操作,差分数组的妙处就是将区间修改操作改为了两个单点修改操作,这里也一样,先求出原序列的差分序列,对差分序列建树、修改、查询,反正求出差分序列后的一切基本上就和原序列基本上没关系了。但是有一点需要注意一下,询问区间gcd的公式推出来后是gcd(a1,b2,b3,b4…),所以为了找到a1我们还要维护一个sum,来方便找a1.
代码(压了行,有点丑qwq):
#include
using namespace std;
inline int read(){
char ch;
int flag=1;
while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
int ans=ch-48;
while((ch=getchar())>='0'&&ch<='9') ans=ans*10+ch-48;
return ch*flag;
}
inline void write(int x){
if(x<0) putchar('-'),x=-x;if(x>9) write(x/10);putchar('0'+x%10);return;
}
#define N 200001
int n,m;
inline int gcd(int a,int b){if(!b) return a;return gcd(b,a%b);}
struct node{
int l,r,g,s;
friend node operator + (node A,node B);
}tree[N<<2];
node operator + (node A,node B){
node ans;ans.l=A.l,ans.r=B.r;
ans.g=gcd(A.g,B.g);ans.s=A.s+B.s;
return ans;
}
int cf[N];
inline void build(int p,int l,int r){
tree[p].l=l,tree[p].r=r;
if(l==r){
tree[p].g=cf[l],tree[p].s=cf[l];return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);build(p<<1|1,mid+1,r);
tree[p]=tree[p<<1]+tree[p<<1|1];return;
}
inline void update(int p,int pos,int v){
int l=tree[p].l,r=tree[p].r;
if(v>n)return;
if(l==r){tree[p].s+=v;tree[p].g+=v;return;}
int mid=(l+r)>>1;
if(pos<=mid) update(p<<1,pos,v);
else update(p<<1|1,pos,v);
tree[p]=tree[p<<1]+tree[p<<1|1];
}
inline node query(int p,int ql,int qr){
int l=tree[p].l,r=tree[p].r;
if(ql>tree[p].r||qr<tree[p].l)return (node){0,0,0,0};
if(ql<=l&&qr>=r) return tree[p];
int mid=(l+r)>>1;
if(ql<=mid&&qr>mid) return query(p<<1,ql,mid)+query(p<<1|1,mid+1,qr);
else if(qr<=mid) return query(p<<1,ql,qr);
else if(ql>mid) return query(p<<1|1,ql,qr);
}
int main(){
// freopen("rand.in","r",stdin);
scanf("%d",&n);
int x;
for(int i=1;i<=n;i++)scanf("%d",&cf[i]);
for(int i=n;i;--i)cf[i]-=cf[i-1];
build(1,1,n);
scanf("%d",&m);
int flag,l,r,v;
for(int i=1;i<=m;i++){
scanf("%d",&flag);
if(flag){scanf("%d%d%d",&l,&r,&v);update(1,l,v);update(1,r+1,-v);}
else{
scanf("%d%d",&l,&r);
int x1=query(1,1,l).s;int x2=query(1,l+1,r).g;
write(abs(gcd(x1,x2))); putchar('\n');
}
}
return 0;
}
三.线段树维护01串
典例:SCOI2010 序列操作
传送门1
传送门2
lxhgww最近收到了一个01序列,序列里面包含了n个数,这些数要么是0,要么是1,现在对于这个序列有五种变换操作和询问操作:
0 a b 把[a, b]区间内的所有数全变成0
1 a b 把[a, b]区间内的所有数全变成1
2 a b 把[a,b]区间内的所有数全部取反,也就是说把所有的0变成1,把所有的1变成0
3 a b 询问[a, b]区间内总共有多少个1
4 a b 询问[a, b]区间内最多有多少个连续的1
对于每一种询问操作,lxhgww都需要给出回答,聪明的程序员们,你们能帮助他吗?
输入 输出 样例输入 [复制] 分析:
输入数据第一行包括2个数,n和m,分别表示序列的长度和操作数目 第二行包括n个数,表示序列的初始状态 接下来m行,每行3个数,op, a, b,(0<=op<=4,0<=a<=b
对于每一个询问操作,输出一行,包括1个数,表示其对应的答案
10 10
0 0 0 1 1 0 1 0 1 1
1 0 2
3 0 5
2 2 2
4 0 4
0 3 6
2 3 7
4 2 8
1 0 5
0 5 6
3 3 9
样例输出 [复制]
5
2
6
5
提示
【数据范围】 对于30%的数据,1<=n, m<=1000 对于100%的数据,1<=n, m<=100000
操作0和操作1就是简单的区间覆盖,打一个cover标记就可以了,操作2需要一个反转标记,操作3是模板操作,维护无脑区间和就可以了,操作4其实跟上一道题程序设计竞赛差不多,但是由于操作2的存在,所以不仅要维护l1,r1,dat1,还要维护l0,r0,dat0,来实现反转的操作。
在下传标记的时候要注意的是,有标记表示的是这一个节点的信息已经维护好了,询问时可以直接调用,但是它的子孙后代并没有维护,标记就是用来储存它的左右儿子应该如何如何。这个就是lazy标记的本质,正是因为询问时常常不需要将信息维护到线段树的最底层的叶子结点,所以lazy标记可以节省很多时间。这一点博主个人认为有点像阴阳账簿,当上面检查部门的人想查你水表的时候,如果他查的不那么细的话,你作为一个贪污分子也许可以把公费出差等事大账本记得粗略一点,只用把表面做的没问题,然后在大账本一个隐秘的角落写下这本大账本所管的小账本里有哪些地方的账是没有结清的,然后只给他看大账本,小账本先不慌着填坑,由于他只关心账目如何,并不会去看你有没有在隐秘的角落记下什么神奇的东西,所以你暂时非常安全;如果他想查的严一些,你可以在他还在查大账本时赶紧改一改小账本,把出差时在哪吃了什么等等小事记下来,当然小账也要记得看似没有问题才行;如果他查的再细一点,你就再翻一翻你的小小账本,赶紧改一改填填坑比如把吃的每道菜多少钱都记上再把有出入的地方补一补,然后把小小账本递给那位秉公执法认真查表的大人。
(以上内容纯属为了方便解释,不带任何目的,并非个人社会经验之谈)
至于本题具体的标记下传操作,有几点还是要注意一下:
代码:#include