[题解]P2042 [NOI2005] 维护数列 - Splay解法
一道思路不难,但实现细节很多的平衡树题,调了一天半终于做出来了w。
对于初始序列,我们可以直接构建一条链(毕竟一个一个调用插入函数也可能形成一条链)。题解有递归直接构建成一棵严格平衡的二叉树的,这样也可以,常数可能会小一点。
区间修改&求和与文艺平衡树的原理相同,只需要多加\(1\)个赋值的懒标记即可。
删除都是区间操作。我们受文艺平衡树的启发,找到区间的左右端点\(l,r\),然后再找到\(l\)在中序遍历中的前驱节点\(L\),\(r\)在中序遍历中的后继节点\(R\),splay(L,0)
,在splay(R,L)
,这样\(R\)的左子树就是要操作的区间\([l,r]\)了。删除\([l,r]\)区间,直接断开\(R\)与其左子树的连接即可。
插入同样是区间操作,我们可以把输入的值连成一条链(或构建成一棵严格平衡的二叉树),然后找我们要插入的位置是哪一个节点,设要插入的位置为\(pos\)之后,那么找到的节点\(u\)就是中序遍历第\(pos\)个节点。我们再寻找\(u\)的后继节点,把这条链接到后继它上面就可以了(别忘了无右儿子的情况)。
平衡树维护最大子列和其实不难,但这道题有个很毒的坑点:子列不能为空,然而题面却没有给出!
每个节点\(u\)额外维护\(3\)个信息:
- \(lmax\):子树\(u\)的中序遍历的最大前缀(可以为空)。
- \(rmax\):子树\(u\)的中序遍历的最大后缀(可以为空)。
- \(maxx\):子树\(u\)的中序遍历的最大子列和(不能为空)。
初始化(\(sum\)表示子树权值和):
\(maxx(u)=v(u)\\lmax(u)=rmax(u)=\max(v(u),0)\)
转移:
- \(\color{green}lmax(u)=\max\{lmax(lc(u)),sum(lc(u))+v(u)+lmax(rc(u)),0\}\)
(如果不选\(u\)就是\(lmax(lc(u))\),如果选\(u\)就是\(sum(lc(u))+v(u)+lmax(rc(u))\),根据定义可以为空,所以还要和\(0\)取一个\(\max\)) - \(\color{darkcyan}rmax(u)=\max\{rmax(rc(u)),sum(rc(u))+v(u)+rmax(lc(u)),0\}\)
(同理) - \(\color{mediumblue}maxx(u)=\max\{rmax(lc(u))+v(u)+lmax(rc(u)),maxx(lc(u)),maxx(rc(u))\}\)
(如果不选\(u\)就直接等于左边的答案/右边的答案,如果选\(u\)就是左边的后缀最大+\(u\)的权值+右边的前缀最大)
pushdown
中的转移(set_sum
)见代码。
注意到转移的第\(3\)个式子中,\(rmax(lc(u))+v(u)+lmax(rc(u))\)是选\(u\)的情况,所以前后缀可以为空。
其实就这些了,思路不难,不过代码实现有很多需要注意。如果遇到问题可以查一下:
- 子列不能为空。
- 两个哨兵值要置为\(0\),否则计算\(sum\)可能会出问题。
- 一定一定要
pushdown
,在查找第\(k\)名、插入节点的过程中都要pushdown
! - 插入次数是\(4*10^6\),如果开这么大的数组会MLE。但题目保证任何时候平衡树里不超过\(5*10^5\)个元素。我们考虑把删掉的空间重复利用:每删除一个子树,就把这个子树的根节点入栈,为插入的节点分配空间时,先看栈中有没有节点,如果有,则把该节点拿出来,把它的左右子节点(如果有)入栈。没有必要删除后一次性把删掉的节点都入栈。
- 区间翻转需要
lmax
和rmax
交换位置。 - 和线段树相似地,下放标记时对左儿子、右儿子操作,而非自己。拿计算\(sum\)来距离,
pushdown(x)
应该是更新左右孩子的\(sum\),而自己的\(sum\)应该在主函数中完成设置。其他同理。这应该作为写pushdown
的一个习惯。因为代码中,很有可能调用pushdown(x)
后就去获得\(x\)子节点的各种信息(比如ins
函数中pushdown(u)
后就去获取\(lc(rc(u))\)),所以必须保证子节点状态得到更新。
点击查看代码
#include<bits/stdc++.h> #define int long long #define N 500010 #define lc(x) tr[x].ch[0] #define rc(x) tr[x].ch[1] #define ch(x,y) tr[x].ch[y] #define fa(x) tr[x].fa #define v(x) tr[x].v #define siz(x) tr[x].siz #define tag(x) tr[x].tag #define tag2(x) tr[x].tag2 #define sum(x) tr[x].sum #define lmax(x) tr[x].lmax #define rmax(x) tr[x].rmax #define maxx(x) tr[x].maxx using namespace std; struct tree{ int ch[2],siz,fa,v,tag2,sum; int lmax,rmax,maxx; bool tag; void init(){ch[0]=ch[1]=siz=fa=v=lmax=rmax=maxx=tag=0;tag2=LLONG_MIN;} }tr[N]; int n,m,cnt,root,nodecnt; stack<int> fr; void init(int u){tr[u].init();} int newnode(int v,int siz=1){ int u; if(fr.empty()) u=++cnt; else{ u=fr.top(),fr.pop(); if(lc(u)) fr.push(lc(u)); if(rc(u)) fr.push(rc(u)); } init(u); sum(u)=v(u)=maxx(u)=v,siz(u)=siz; lmax(u)=rmax(u)=max(v,0ll); return u; } void update(int u){ siz(u)=siz(lc(u))+siz(rc(u))+1; sum(u)=sum(lc(u))+v(u)+sum(rc(u)); lmax(u)=max(max(lmax(lc(u)),sum(lc(u))+v(u)+lmax(rc(u))),0ll); rmax(u)=max(max(rmax(rc(u)),sum(rc(u))+v(u)+rmax(lc(u))),0ll); maxx(u)=rmax(lc(u))+v(u)+lmax(rc(u)); if(lc(u)) maxx(u)=max(maxx(u),maxx(lc(u))); if(rc(u)) maxx(u)=max(maxx(u),maxx(rc(u))); } bool get(int u){return u==rc(fa(u));} void rot(int x){ int y=fa(x),z=fa(y);//保证y!=0 bool dir=get(x),tdir=get(y); if(ch(x,!dir)) fa(ch(x,!dir))=y; ch(y,dir)=ch(x,!dir); ch(x,!dir)=y; fa(y)=x,fa(x)=z; if(z) ch(z,tdir)=x; update(y),update(x); } void splay(int x,int y){ for(int f;(f=fa(x))!=y;rot(x)) if(fa(f)!=y) rot(get(f)==get(x)?f:x); if(!y) root=x; } void set_sum(int x,int v){//用于pushdown和主函数 tag2(x)=v,v(x)=v,sum(x)=v*siz(x); if(v>0) lmax(x)=rmax(x)=maxx(x)=sum(x); else lmax(x)=rmax(x)=0,maxx(x)=v; } //用于pushdown和主函数 void set_rev(int x){swap(lc(x),rc(x)),swap(lmax(x),rmax(x)),tag(x)^=1;} void pushdown(int x){ if(tag2(x)!=LLONG_MIN){ if(lc(x)) set_sum(lc(x),tag2(x)); if(rc(x)) set_sum(rc(x),tag2(x)); tag(x)=0;//相当于一个小优化,如果已经区间赋值了,那么翻转不翻转无所谓了 tag2(x)=LLONG_MIN; } if(tag(x)){ if(lc(x)) set_rev(lc(x)); if(rc(x)) set_rev(rc(x)); tag(x)=0; } } int find(int num){//找中序遍历第num个是哪个节点 int u=root; while(u){ pushdown(u);//别忘了pushdown if(siz(lc(u))+1==num) break; else if(siz(lc(u))>=num) u=lc(u); else num-=siz(lc(u))+1,u=rc(u);//这两句顺序别反了 } return u; } void ins(int num,int v){//在中序第num后插入子树v int u=find(num+1); pushdown(u);//pushdown不要忘记 int nex=rc(u); if(!nex) rc(u)=v,fa(v)=u,splay(u,0); else{ while(lc(nex)){ pushdown(nex);//一定记得写pushdown nex=lc(nex); } pushdown(nex);//这个也不要忘 lc(nex)=v,fa(v)=nex,splay(nex,0); } } void make_range(int& l,int& r){//取出[l,r]的区间,放在lc(r)中 l=find(l),r=find(r+2);//本来应该是l-1,r+1,但因为有哨兵节点所以需要+1 splay(l,0),splay(r,l);//lc(r)就是要操作的部分 } signed main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin>>n>>m; newnode(0,1);//两个哨兵的值都为0,否则sum计算会出问题 for(int i=1;i<=n+1;i++){//连成一条链 int v; if(i==n+1) v=0; else cin>>v; newnode(v); lc(cnt)=cnt-1; fa(cnt-1)=cnt; update(cnt); } root=nodecnt=n+2;//根节点不是1而是n+2 while(m--){ string op; cin>>op; if(op=="INSERT"){ int pos,tot,x; cin>>pos>>tot; if(tot==0) continue; int cur,last; for(int i=1;i<=tot;i++){//把输入连成一条链 cin>>x; cur=newnode(x,i); if(i>1) fa(last)=cur,lc(cur)=last; update(cur); last=cur; } ins(pos,cur);//cur是这条链的根节点 nodecnt+=tot; }else if(op=="DELETE"){ int l,tot,r; cin>>l>>tot; r=l+tot-1; make_range(l,r); if(lc(r)) fa(lc(r))=0,fr.push(lc(r)),lc(r)=0;//需要将子树根节点入栈 nodecnt-=tot; }else if(op=="MAKE-SAME"){ int l,tot,r,v; cin>>l>>tot>>v; r=l+tot-1; make_range(l,r); if(lc(r)) set_sum(lc(r),v); }else if(op=="REVERSE"){ int l,tot,r; cin>>l>>tot; r=l+tot-1; make_range(l,r); if(lc(r)) set_rev(lc(r)); }else if(op=="GET-SUM"){ int l,tot,r; cin>>l>>tot; r=l+tot-1; make_range(l,r); cout<<sum(lc(r))<<"\n"; }else if(op=="MAX-SUM"){ int l=1,r=nodecnt-2; make_range(l,r); cout<<maxx(lc(r))<<"\n"; } } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效