Solution: 题解 洛谷 P2572 [SCOI2010]序列操作(浅谈区间操作线段树的代码简化)
本文从 洛谷 P2572 [SCOI2010]序列操作 为例,讲一讲本蒟蒻对常用区间操作线段树的一些小技巧,可以减少码量,降低出错概率
这道题可以说是常规的线段树区间操作题,但是有3个区间操作(都需要懒标记)和2个区间查询,而且第二个区间查询需要处理区间边界的连接问题,所以并不是一个容易的问题。其他题解里的线段树方法码量进100行,代码长度进5K,但代码有很多重复或相似内容。本蒟蒻使用了一些技巧包括但不限于压行,在除快读等 default source 以外仅使用了50多行,总量2K多的代码
本文假设读者已经基本掌握普通线段树和懒标记的使用
首先讨论一下为什么这道题可以使用线段树
因为可以通过维护区间的一些信息使得:
-
对于一个区间 \([l,r](l<r)\),可以通过该区间的两个子区间 \([l,m],[m+1,r](l\leqslant m<r)\) 的信息快速得到该区间的信息 (e.g. 一个区间里1的数量就是两个子区间1的数量的和),这样才可以利用线段树得到查询区间的信息
-
可以通过信息快速得到查询的答案
-
对于一个区间 \([l,r](l<r)\) 的修改等价于对该区间的两个子区间 \([l,m],[m+1,r](l\leqslant m<r)\) 的相同修改 (e.g. 一个区间全部变成1等价于两个子区间各自全部变成1),而且可以通过快速修改区间的信息实现对该区间的给定修改
对于本题,我们先找到需要的区间信息
\(ld=0/1\) 表示序列左端的数字,\(rd\) 同理
\(lc\) 表示序列左端的数字的连续长度,\(rc\) 同理(这是为了处理区间边界)
\(m0\) 表示序列内0的最长连续序列,\(m1\) 同理(这是为了方便区间反转)
\(s\) 表示序列内1的数量(不需存0的数量,因为可以直接由区间长度和\(s\)得到)
以下引出第一个技巧:
使用结构体线段树,对于普通线段树,保存左右端点于结构体内
这是本文使用的结构体定义:
struct T{int l,r,lc,rc,m0,m1,s,t;bool ld,rd;}
其中\(l,r,t\)分别为左右端点位置和懒标记(之后会讲)
然后,对于每两个连续区间,都可以通过各自的信息计算出合并的信息,所以以下引出第二个技巧:
将两个区间合并写为结构体加法
(当然也可以写成其他运算或函数)
首先,\(l,r,ld,rd,s\) 不必多说
对于 \(lc\),需要判断左子区间是否全都一样并且左子区间的数字和右子区间的左端数字相同。是则 \(lc=\) 左子区间的长度+右子区间的 \(lc\),否则仅为左子区间的 \(lc\)(\(rc\) 类似)
而对于 \(m0\),除了用两子区间 \(m0\) 的最大值,还要考虑中间的区间合并,即是否左子区间的右端和右子区间的左端是否都为0,是则还要考虑左子区间的 \(rc+\) 右子区间的 \(lc\)(\(m1\) 类似)
对于\(t\),我们待会再讲就设为0,表示没有懒标记(没见过懒标记上传的对吧)
以下是结构体定义和加法的参考实现:
#define N (100010)
#define Ln(u) ((u).r-(u).l+1)
struct T{int l,r,lc,rc,m0,m1,s,t;bool ld,rd;
T operator+(T b)const{
return {l,b.r,
lc==Ln(*this)&&ld==b.ld?lc+b.lc:lc,
b.rc==Ln(b)&&b.rd==rd?b.rc+rc:b.rc,
max(max(m0,b.m0),!rd&&!b.ld?rc+b.lc:0),
max(max(m1,b.m1),rd&&b.ld?rc+b.lc:0),
s+b.s,0,ld,b.rd
};
}
}t[N<<2];
之后所有区间合并的操作都可以用加法完成
接下来就是
建树
对于普通线段树,以1为根,存储区间 \([0,n-1]\),\(u\) 的左右儿子分别为 \(2u,2u+1\),并对区间做折半,然后依次对左右儿子递归建树
如果节点 \(l=r\) 就说明到达了叶子,此时可以直接读入数据并修改信息(\(ld=rd=\) 读入值,\(lc=rc=1\) 等等),因为叶子是依次从左到右搜索到的
当左右儿子递归完时,用结构体加法直接获得自己的信息值
一个小技巧:
固定节点编号的变量名(比如 \(u\)),然后 #define 掉节点和左右儿子的结构体名(比如 \(U,Ls,Rs\))
以下是建树的参考实现:
int rd(){
int x;char c(getchar());bool k;
while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
c^'-'?(k=1,x=c&15):k=x=0;
while(isdigit(Gc(c)))x=x*10+(c&15);
return k?x:-x;
}
#define U (t[u])
#define Ls (t[u<<1])
#define Rs (t[u<<1|1])
void bld(int u){
if(U.l==U.r){U.lc=U.rc=1,U.m1=U.s=U.ld=U.rd=rd(),U.m0=!U.ld;return;}
Ls.l=U.l,Rs.r=U.r,Rs.l=(Ls.r=(U.l+U.r)>>1)+1,bld(u<<1),bld(u<<1|1),U=Ls+Rs;
}
下一步是
修改
而必须使用的就是
懒标记
在此引出下一个技巧:
将放置懒标记写为一个函数,并且在放置懒标记的同时做区间修改
(即拥有懒标记的结点是已经进行过操作的)
这样的话,下传就可以写成是对左右儿子放置自己的懒标记,然后删掉自己的懒标记
这里有3个操作,为了方便起见,我们把操作的编号+1,即操作1是全变0,操作2是全变1,操作3是反转,然后再定义操作0是不操作
经过观察发现:
-
如果对一个区间先进行任意操作再进行操作1或2,那么先进行的操作会直接无效
-
如果对一个区间先进行操作 \(x\),再进行操作3,那么就等价于进行操作 \(3-x\)(可笑的巧合)
这样,操作之间是可以连接的,所以可以用一个编号(如 \(t\))来表示结点 \(u\) 身上的懒标记,然后当进行修改操作时,对区间信息进行修改,并且把 \(t\) 变为原来 \(t\) 和修改操作的连接
以下是放置懒标记 \(pt()\) 和懒标记下传 \(pd()\) 的参考实现:
void pt(int u,int x){
switch(x){
case 1:U.m0=U.lc=U.rc=Ln(U),U.m1=U.s=U.ld=U.rd=0;break;
case 2:U.m1=U.lc=U.rc=U.s=Ln(U),U.m0=0,U.ld=U.rd=1;break;
case 3:swap(U.m0,U.m1),U.s=Ln(U)-U.s,U.ld=!U.ld,U.rd=!U.rd;
}
U.t=x==3?U.t=3-U.t:x;
}
void pd(int u){if(U.l<U.r&&U.t)pt(u<<1,U.t),pt(u<<1|1,U.t),U.t=0;}
然后修改就很简单了,只需要写一个函数(\(opt\) 是操作编号,注意修改时为输入值+1,\(l,r\)是输入的边界):
void mdf(int u){
if(l<=U.l&&U.r<=r){pt(u,opt);return;}
if(r<U.l||U.r<l)return;
pd(u),mdf(u<<1),mdf(u<<1|1),U=Ls+Rs;
}
最后是
查询
于是有了最后一个技巧
查询函数返回查询区间的结构体信息
一般的查询函数可以轻易解决查询3(因为就是返回\(s\)的和),但是对于查询4,我们又要处理区间合并的边界问题
而这个问题,我们在写结构体加法的时候早就解决了
只要通过结构体加法得到查询区间的结构体信息,就可以完成任何查询
对于查询3就是输出\(s\),对于查询4就是输出\(m1\)
以下是查询 \(qry()\) 和主函数的参考实现(在结构体加法中没有定义零元,所以递归时要考虑儿子结点是否与给定区间有交集):
T qry(int u){
if(l<=U.l&&U.r<=r)return U;
pd(u);
return l<=Ls.r&&Rs.l<=r?qry(u<<1)+qry(u<<1|1):l<=Ls.r?qry(u<<1):qry(u<<1|1);
}
signed main(){
t[1]={0,rd()-1},Rd(m),bld(1);
while(m--){
Rd(opt),Rd(l),Rd(r);
if(opt<=2)++opt,mdf(1);
else wr(opt==3?qry(1).s:qry(1).m1),Pe;
}
exit(0);
}
然后所有内容都完成了(撒花)
Time complexity: \(O(m\log n)\)
Memory complexity: \(O(n)\)
参考代码(952ms / 3.41MB):
//This program is written by Brian Peng.
#include<bits/stdc++.h>
using namespace std;
#define Rd(a) (a=rd())
#define Gc(a) (a=getchar())
#define Pc(a) putchar(a)
int rd(){
int x;char c(getchar());bool k;
while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
c^'-'?(k=1,x=c&15):k=x=0;
while(isdigit(Gc(c)))x=x*10+(c&15);
return k?x:-x;
}
void wr(int a){
if(a<0)Pc('-'),a=-a;
if(a<=9)Pc(a|'0');
else wr(a/10),Pc((a%10)|'0');
}
signed const INF(0x3f3f3f3f),NINF(0xc3c3c3c3);
long long const LINF(0x3f3f3f3f3f3f3f3fLL),LNINF(0xc3c3c3c3c3c3c3c3LL);
#define Ps Pc(' ')
#define Pe Pc('\n')
#define Frn0(i,a,b) for(int i(a);i<(b);++i)
#define Frn1(i,a,b) for(int i(a);i<=(b);++i)
#define Frn_(i,a,b) for(int i(a);i>=(b);--i)
#define Mst(a,b) memset(a,b,sizeof(a))
#define File(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define N (100010)
#define U (t[u])
#define Ls (t[u<<1])
#define Rs (t[u<<1|1])
#define Ln(u) ((u).r-(u).l+1)
int m,opt,l,r;
struct T{int l,r,lc,rc,m0,m1,s,t;bool ld,rd;
T operator+(T b)const{
return {l,b.r,
lc==Ln(*this)&&ld==b.ld?lc+b.lc:lc,
b.rc==Ln(b)&&b.rd==rd?b.rc+rc:b.rc,
max(max(m0,b.m0),!rd&&!b.ld?rc+b.lc:0),
max(max(m1,b.m1),rd&&b.ld?rc+b.lc:0),
s+b.s,0,ld,b.rd
};
}
}t[N<<2];
void bld(int u);
void pt(int u,int x);
void pd(int u){if(U.l<U.r&&U.t)pt(u<<1,U.t),pt(u<<1|1,U.t),U.t=0;}
void mdf(int u);
T qry(int u);
signed main(){
t[1]={0,rd()-1},Rd(m),bld(1);
while(m--){
Rd(opt),Rd(l),Rd(r);
if(opt<=2)++opt,mdf(1);
else wr(opt==3?qry(1).s:qry(1).m1),Pe;
}
exit(0);
}
void bld(int u){
if(U.l==U.r){U.lc=U.rc=1,U.m1=U.s=U.ld=U.rd=rd(),U.m0=!U.ld;return;}
Ls.l=U.l,Rs.r=U.r,Rs.l=(Ls.r=(U.l+U.r)>>1)+1,bld(u<<1),bld(u<<1|1),U=Ls+Rs;
}
void pt(int u,int x){
switch(x){
case 1:U.m0=U.lc=U.rc=Ln(U),U.m1=U.s=U.ld=U.rd=0;break;
case 2:U.m1=U.lc=U.rc=U.s=Ln(U),U.m0=0,U.ld=U.rd=1;break;
case 3:swap(U.m0,U.m1),U.s=Ln(U)-U.s,U.ld=!U.ld,U.rd=!U.rd;
}
U.t=x==3?U.t=3-U.t:x;
}
void mdf(int u){
if(l<=U.l&&U.r<=r){pt(u,opt);return;}
if(r<U.l||U.r<l)return;
pd(u),mdf(u<<1),mdf(u<<1|1),U=Ls+Rs;
}
T qry(int u){
if(l<=U.l&&U.r<=r)return U;
pd(u);
return l<=Ls.r&&Rs.l<=r?qry(u<<1)+qry(u<<1|1):l<=Ls.r?qry(u<<1):qry(u<<1|1);
}