其他分享
首页 > 其他分享> > P3391 【模板】文艺平衡树 题解

P3391 【模板】文艺平衡树 题解

作者:互联网

描述:

给定 \(1\) 个长度为 \(n\) 的区间,有 \(m\) 次操作,每次反转区间 \([l,r]\) ,

求最后的序列。

思路:

区间操作,可以用平衡树中的 Splay 来解决。

不会平衡树的同学可以点这里:

  1. \(\mathtt{BST}\) 学习笔记

  2. \(\mathtt{Splay}\)

  3. \(\mathtt{Treap}\)

    下文默认会 Splay 的基础操作,Rotate , Splay ,\(0\) 表示左儿子, \(1\) 表示右儿子。

建树的时候,可以按照权值排序,也可以按照序列排序。

由于这里是区间反转,那么就要按照区间排序,也就是原序列排序。

Build

int Build(int l,int r,int fa){
	int mid=(l+r)>>1,now=++cnt;//cnt节点编号+1
	f[now]=fa;Val(now)=d[mid];
	Son(now,0)=Son(now,1)=0;
	if(l<=mid-1){Son(now,0)=Build(l,mid-1,now);}//递归左儿子
	if(r>=mid+1){Son(now,1)=Build(mid+1,r,now);}//递归右儿子
	Update(now);//更新
	return now;
}

考虑如何 \(O(\log n)\) 进行区间反转操作。可以仿照线段树,进行标记。

由于 Splay 节点代表的不是区间,那问题就是如何把区间 \([l,r]\) 变到 \(1\) 棵子树上呢?

将 \(l-1\) 旋转到 \(rt\) ,将 \(r+1\) 旋转到 \(l-1\) 的右儿子,那么区间 \([l,r]\) 就是 \(r+1\) 的左儿子。

证明:

将 \(l-1\) 旋转到 \(rt\) ,那么 \([l,r]\) 必然在 \(l-1\) 的右儿子上。

又因为将 \(r+1\) 旋转到 \(rt\) 的右儿子,那么 \([l,r]\) 只可能是 \(r+1\) 的左儿子。

对应的操作:

Splay(l-1,0) , Splay(r+1,0)

\(\mathtt{Ps:}\) 由于下文的代码会插入两个无用的节点,那么 l-1r+1 会变成 lr+2

\(r+1\) 是沿着祖先旋转的,那么如何保证 \(r+1\) 刚好旋转到 \(l-1\) 的右儿子呢?

因为在原树上, \(r+1\) 比 \(l-1\) 大,那么必定在 \(l-1\) 的右子树上,那么旋转就必然在 \(l-1\) 的右儿子了。

那将 \([l,r]\) 移到 \(1\) 棵子树上后, 就可以在子树的根上的 Tag 打上标记,如果后面有操作会改变该子树及其子孙的形态,那么就要先下传标记,这就在 Splay 中实现。

Update

void Update(int p){
	Siz(p)=1;//当前节点
	if(Son(p,0)){Siz(p)+=Siz(Son(p,0));}//加入左子树的节点
	if(Son(p,1)){Siz(p)+=Siz(Son(p,1));}//加入右子树的节点
}

更新节点的信息。

P_d

void P_d(int p){
	if(Tag(p)){
		Tag(Son(p,0))^=1;//下传标记
		Tag(Son(p,1))^=1;//下传标记
		Tag(p)=0;//清空当前节点的标记
		swap(Son(p,0),Son(p,1));//交换左右子树
	}
}

意为 Push_down ,表示下传标记,将 Tag 当中的翻转标记下传给儿子,记住交换左右儿子且清空 Tag

Get

int Get(int p){
	return Son(f[p],1)==p;//返回当前节点是其父亲的左儿子还是右儿子
}

Rotate

void Rotate(int p){
	int fa=f[p],ffa=f[fa],m=Get(p),n=Get(fa);
	Con(p,ffa,n);
	Con(Son(p,m^1),fa,m);
	Con(fa,p,m^1);
	Update(fa);Update(p);//更新旋转后的当前节点和父亲节点
}

旋转操作。

Splay

void Splay(int x,int goal){
	int len=0;
	for(int i=x;i;i=f[i]){q[++len]=i;}
	for(int i=len;i;--i){P_d(q[i]);}//下传标记
	while(f[x]!=goal){
		int fa=f[x];
		if(f[fa]!=goal){
			Rotate(Get(x)==Get(fa)?fa:x);
		}
		Rotate(x);
	}
	if(!goal){rt=x;}
}

第 \(2\) ~ \(4\) 行就是在有区间操作的时候,需要在 Splay 之前下传标记。

Fip

void Fip(int x,int y){
	int l=Rank(x),r=Rank(y+2);
	Splay(l,0);Splay(r,l);
	Tag(Son(r,0))^=1;
}

由于插入了两个无用节点,kInf-kInf,保证 l-1r+1 的存在,那么就要改变成 lr+2

代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <ctime>

#define PR pair<int,int>
#define LL long long

using namespace std;

const int kN=1e5+5;
const int kInf=1e9;

struct Splay{
	int son[2],val,siz;
	bool tag;
	#define Son(p,k) spy[p].son[k]
	#define Tag(p) spy[p].tag
	#define Val(p) spy[p].val
	#define Siz(p) spy[p].siz
}spy[kN];

int n,m;
int d[kN],f[kN],q[kN];
int rt,cnt;

void Update(int p){
	Siz(p)=1;//当前节点
	if(Son(p,0)){Siz(p)+=Siz(Son(p,0));}//加入左子树的节点
	if(Son(p,1)){Siz(p)+=Siz(Son(p,1));}//加入右子树的节点
}

int Build(int l,int r,int fa){
	int mid=(l+r)>>1,now=++cnt;//cnt节点编号+1
	f[now]=fa;Val(now)=d[mid];
	Son(now,0)=Son(now,1)=0;
	if(l<=mid-1){Son(now,0)=Build(l,mid-1,now);}//递归左儿子
	if(r>=mid+1){Son(now,1)=Build(mid+1,r,now);}//递归右儿子
	Update(now);//更新
	return now;
}

void P_d(int p){
	if(Tag(p)){
		Tag(Son(p,0))^=1;//下传标记
		Tag(Son(p,1))^=1;//下传标记
		Tag(p)=0;//清空当前节点的标记
		swap(Son(p,0),Son(p,1));//交换左右子树
	}
}

int Get(int p){
	return Son(f[p],1)==p;//返回当前节点是其父亲的左儿子还是右儿子
}

void Con(int x,int y,int z){
	f[x]=y;
	Son(y,z)=x;
}

void Rotate(int p){
	int fa=f[p],ffa=f[fa],m=Get(p),n=Get(fa);
	Con(p,ffa,n);
	Con(Son(p,m^1),fa,m);
	Con(fa,p,m^1);
	Update(fa);Update(p);//更新旋转后的当前节点和父亲节点
}

void Splay(int x,int goal){
	int len=0;
	for(int i=x;i;i=f[i]){q[++len]=i;}
	for(int i=len;i;--i){P_d(q[i]);}//下传标记
	while(f[x]!=goal){
		int fa=f[x];
		if(f[fa]!=goal){
			Rotate(Get(x)==Get(fa)?fa:x);
		}
		Rotate(x);
	}
	if(!goal){rt=x;}
}

int Rank(int k){
	int now=rt;
	while(1){
		P_d(now);
		if(Siz(Son(now,0))>=k){now=Son(now,0);}
		else if(Siz(Son(now,0))+1==k){return now;}
		else{
			k-=Siz(Son(now,0))+1;
			now=Son(now,1);
		}
	}
}

void Fip(int x,int y){
	int l=Rank(x),r=Rank(y+2);
	Splay(l,0);Splay(r,l);
	Tag(Son(r,0))^=1;
}

void Out(int p){
	P_d(p);
	if(Son(p,0)){Out(Son(p,0));}
	if(Val(p)>0&&Val(p)<n+1){printf("%d ",Val(p));}
	if(Son(p,1)){Out(Son(p,1));}
}

int main(){
	scanf("%d%d",&n,&m);
	d[1]=-kInf;d[n+2]=kInf;
	for(int i=2;i<=n+1;++i){d[i]=i-1;}
	rt=Build(1,n+2,0);
	for(int i=1;i<=m;++i){
		int x,y;
		scanf("%d%d",&x,&y);
		Fip(x,y);
	}
	Out(rt);
	return 0;
}

\(\mathtt{Ps:}\) 想学 Treap 的同学,可以点开上文的连接。

标签:fa,int,题解,Son,Splay,P3391,now,节点,模板
来源: https://www.cnblogs.com/Godzillad/p/15095585.html