[题解]P3391 文艺平衡树 - Splay解法
给定序列\(1,2,\dots,n\),接下来\(m\)次操作,每次操作给定\(l,r\),你需要翻转\([l,r]\)。
所有操作结束后,请输出这个序列。
我们先从“普通平衡树”这一题出发,思考一下Splay操作的本质。
我们把一个节点Splay到根节点后,中序遍历在自己前面的节点都在左边,中序遍历在自己后面的都在右边。
发现了么?Splay操作和点权是无关的。如果我们将splay(x)
——将\(x\)转移到根节点,修改为splay(x,y)
——将\(x\)转移到\(y\)节点下方(\(y=0\)则表示转移到根节点)。那么splay(x,y)
所做的,就是将\(y\)的子树中,有\(x\)的那棵子树中所有的节点,按与\(x\)中序遍历的先后关系进行调整。
回归这道题。我们先构建一棵中序遍历是\(1,2,\dots,n\)的二叉树(你可以很简单地考虑一条链)。接下来我们思考怎样区间翻转。
我们先考虑一棵树,要想翻转其中序遍历,我们只需要把每个节点的左右子树都翻转一遍即可(不要在意破坏BST的性质,因为这棵树根本就不是BST)。当然这一操作如果实时更新会TLE,所以我们像线段树那样,给根节点打个懒标记,后期需要用到子节点就先下传标记(并交换根节点的左右子树)。
如果我们能把要翻转的\([l,r]\)的所有节点弄到一棵树上去就好了……
现在是Splay大放异彩的时候了。
我们先找到\(l\)在中序遍历中的前驱节点\(L\),\(r\)在中序遍历中的后继节点\(R\)(为了防止找不到,我们设置两个哨兵\(n+1\)和\(0\)一并加入二叉树)。splay(L,0)
,再splay(R,L)
。
我们先将\(L\)转移到根节点,再把\(R\)转移到\(L\)的下面(右子树)。
那么显然,此时\(R\)的左子树的中序遍历位置就都在\(l,r\)之间了。
我们给\(R\)打一个标记即可。
就酱。
受上面操作的启发,我们还可以设计出以下操作(一般这些操作都需要哨兵\(n+1\)和\(0\)的辅助,同时splay
需要接受\(2\)个参数):
- \([l,r]\)区间删除:和上边一样,
splay(l,0)
后splay(r,l)
,断开\(r\)与其左子树的连接。 - 合并以\(x,y\)为根的\(2\)颗Splay树(前提是两树都不为空,且\(x\)中的最大值\(<y\)中的最小值):把\(x\)中的最大值Splay到根节点,将其右子节点设为\(y\)并更新自己的信息即可。
- 区间赋值、区间加……一般线段树能维护的这个也能维护,只是可能与一些平衡树特有的功能冲突。
实现细节:
- 别忘了
find(k)
(找中序遍历第\(k\)个)和ltr(u)
(中序遍历输出结果)要调用pushdown
。
此题的代码
#include<bits/stdc++.h> #define int long long #define N 100010 #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 MAX LLONG_MAX #define MIN LLONG_MIN using namespace std; struct tree{ int ch[2],siz,fa,v; bool tag; }tr[N]; int n,m,cnt,root; int newnode(int v){v(++cnt)=v,siz(cnt)=1;return cnt;} void update(int u){siz(u)=siz(lc(u))+siz(rc(u))+1;} bool get(int u){return u==rc(fa(u));} void rot(int x){//通过旋转把x提升1层 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==0) root=x; } void pushdown(int x){//下放标记 if(tag(x)){ swap(lc(x),rc(x)); tag(lc(x))^=1; tag(rc(x))^=1; tag(x)=0; } } void ins(int v){//插入 int u=root,f=0; while(u) f=u,u=ch(u,v>v(u));//>在右,<=在左 u=newnode(v),fa(u)=f; if(f) ch(f,v>v(f))=u; splay(u,0); } int find(int num){//中序遍历第num个是多少 int u=root; while(u){ pushdown(u); if(num==siz(lc(u))+1) return u; if(num<=siz(lc(u))) u=lc(u); else num-=siz(lc(u))+1,u=rc(u); } return -1; } void ltr(int u){//中序遍历,输出 if(!u) return; pushdown(u); ltr(lc(u)); if(v(u)!=MIN&&v(u)!=MAX) cout<<v(u)<<" "; ltr(rc(u)); } signed main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin>>n>>m; ins(MIN),ins(MAX); for(int i=1;i<=n;i++) ins(i); while(m--){ int l,r; cin>>l>>r; l=find(l),r=find(r+2); //因为有极小值,所以查询的名次都要+1 splay(l,0); splay(r,l); if(lc(r)) tag(lc(r))^=1; } ltr(root); return 0; }
注意到,find
函数是没有Splay操作的。其实加上也可以,只不过find
后面已经有\(2\)次Splay操作了。加不加其实都差不多。
实际上,初始化完全不需要调用ins
,直接建成一条链即可,这样显然不会影响后面操作的复杂度(因为就算调用ins
也可能形成链嘛),而且把初始化的复杂度减少了一个\(\log\)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效