BZOJ1269 文本編輯器editor(伸展樹)
題意
https://www.lydsy.com/JudgeOnline/problem.php?id=1269
思路
伸展樹(\(\text{splay}\))功能比較齊全的模板,能較好的體現 \(\text{splay}\) 的功能,簡單介紹一下 \(\text{splay}\)。
基本的概念和函數
\(\text{splay}\) 是平衡樹的一種,能在均攤 \(\log n\) 的時間復雜度內完成很多序列操作(序列就是樹的中序遍歷),核心是以下兩個函數。
rotate
首先是旋轉函數,\(\text{rotate}(x)\) 表示旋轉 \(x\) 節點到它父親的位置,它的父親變成它的孩子,在旋轉函數的過程中,原樹的中序遍歷保持不變,代碼如下:
void rotate(int x)
{
int y=fa[x],z=fa[y];
//push_down(y),push_down(x); 有時需要
int k=chk(x);
ch[z][chk(y)]=x,fa[x]=z;
ch[y][k]=ch[x][!k],fa[ch[x][!k]]=y;
ch[x][!k]=y,fa[y]=x;
//push_up(y),push_up(x); //有時需要
}
網上有大量旋轉函數過程圖,這裏不再解釋。
splay
然後是伸展函數,\(\text{splay}(x,w)\) 表示將 \(x\)
有一點特別註意,當節點 \(x\),\(x\) 的父親 \(y\) ,\(y\) 的父親 \(z\) 三點共線,需要先轉 \(y\) 再轉 \(x\) ,否則轉兩次 \(x\) 。可以通過模擬一條鏈的旋轉發現這種方法的優越性。
代碼如下,可以寫的很短:
void splay(int x,int w) { push_down(x),push_up(x); while(fa[x]!=w) { int y=fa[x],z=fa[y]; if(z!=w)(x==ch[y][1])==(y==ch[z][1])?rotate(y):rotate(x); rotate(x); } if(!w)rt=x; }
由於我通過這個函數保證節點 \(x\) 已經被 \(\text{up down}\) 了,所以特地加了\(\text{up down}\) 防止不用進入循環體的情況。
基本的操作
構造
可以扔一個完美的 \(\text{splay}\) 上去,據說是對後面的操作在常數上有利。
void build(int &x,int f, int *arr,int l,int r)
{
if(l>r)return;
int mid=(l+r)>>1;
x=++tot;
ch[x][0]=ch[x][1]=0;
fa[x]=f;
pw[x]=arr[mid];
//sum[x]=1,tag[x]=0; 在構造時清空會比較方便,如果有點權、標記的話要清空
build(ch[x][0],x,arr,l,mid-1);
build(ch[x][1],x,arr,mid+1,r);
push_up(x);
}
前驅後繼
對於一個中序單調的 \(\text{splay}\) ,可以通過查找的方法獲得前驅後繼,這也就是找第一個大於/小於(等於)一個數 \(v\) 的方法,即在 \(\text{splay}\) 上二分。
int get_pre(ll v,bool inc) //inc表示是否包含v
{
int x=rt,tmp=-1;ll res=-1e18;
while(x)
{
if(inc&&val[x]<=v||!inc&&val[x]<v)if(chk_max(res,val[x]))tmp=x;
if(!ch[x][v>val[x])splay(x,0);
x=ch[x][v>val[x]];
}
return tmp;
}
int get_nxt(ll v,bool inc)
{
int x=rt,tmp=-1;ll res=1e18;
while(x)
{
if(inc&&val[x]>=v||!inc&&val[x]>v)if(chk_min(res,val[x]))tmp=x;
if(!ch[x][v>=val[x])splay(x,0);
x=ch[x][v>=val[x]];
}
return tmp;
}
當然對於中序不一定單調,維護一個普通序列的 \(\text{splay}\) ,可以從結構上分析,利用旋轉操作中序不變的性質。
int get_pre(int x)
{
splay(x,0);
if(!ch[x][0])return -1;
x=ch[x][0];
while(ch[x][1])x=ch[x][1];
splay(x,0);
return x;
}
int get_nxt(int x)
{
splay(x,0);
if(!ch[x][1])return -1;
x=ch[x][1];
while(ch[x][0])x=ch[x][0];
splay(x,0);
return x;
}
第K大值
與動點線段樹寫法類似,直接二分即可。
int get_Kth(int K)
{
//K++; 因為有些題目需要加上極小值和極大值,為了下面調用的方便起見加了這一句話
int x=rt;
while(K!=sum[ch[x][0]]+1)
{
push_down(x);
if(K<=sum[ch[x][0]])x=ch[x][0];
else K-=sum[ch[x][0]]+1,x=ch[x][1];
}
splay(x,0);
return x;
}
在這裏說一下 \(\text{splay}\) 調用的時機,復雜度的證明我目前看不懂,但我知道由於 \(\text{splay}\) 是均攤 \(\log n\) ,所以只要是經過若幹次循環叠代到的節點,都要上旋以防止復雜度堆積。
求一個點是第幾大
int get_rank(int x)
{
splay(x,0);
return sum[ch[x][0]]+1;
// return sum[ch[x][0]]; 同理,在有極小值時采用這種寫法
}
區間操作
我們需要的就是“取出”一個區間,一般采取這樣的方法:
l=get_Kth(l-1),r=get_Kth(r+1);
splay(l,0),splay(r,l);
這樣之後,\(\text{ch}[r][0]\) 就是我們需要的區間了,區間打標記,求和,挪位什麽的就很好實現了。
\(\text{splay}\) 也被叫做分裂樹,它能實現區間的,挪位,這是它很顯著的優勢。
代碼
#include<bits/stdc++.h>
#define FOR(i,x,y) for(int i=(x),i##END=(y);i<=i##END;++i)
#define DOR(i,x,y) for(int i=(x),i##END=(y);i>=i##END;--i)
template<typename T,typename _T>inline bool chk_min(T &x,const _T y){return y<x?x=y,1:0;}
template<typename T,typename _T>inline bool chk_max(T &x,const _T y){return x<y?x=y,1:0;}
typedef long long ll;
const int N=3e6+5;
int ch[N][2],fa[N];char pw[N];
int sum[N];bool tag[N];
int rt,tot;
int n,mouse;
char str[N];
void init(){rt=tot=0;}
void tag_up(int x)
{
tag[x]^=1;
std::swap(ch[x][0],ch[x][1]);
}
void push_up(int x){sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+1;}
void push_down(int x)
{
if(!tag[x])return;
if(ch[x][0])tag_up(ch[x][0]);
if(ch[x][1])tag_up(ch[x][1]);
tag[x]=0;
}
void rotate(int x)
{
int y=fa[x],z=fa[y];
push_down(y),push_down(x);
int k=(x==ch[y][1]);
ch[z][y==ch[z][1]]=x,fa[x]=z;
ch[y][k]=ch[x][!k],fa[ch[x][!k]]=y;
ch[x][!k]=y,fa[y]=x;
push_up(y),push_up(x);
}
void splay(int x,int w)
{
push_down(x),push_up(x);
while(fa[x]!=w)
{
int y=fa[x],z=fa[y];
if(z!=w)(x==ch[y][1])==(y==ch[z][1])?rotate(y):rotate(x);
rotate(x);
}
if(!w)rt=x;
}
void build(int &x,int f,char *arr,int l,int r)
{
if(l>r)return;
int mid=(l+r)>>1;
x=++tot;
ch[x][0]=ch[x][1]=0;
fa[x]=f;
pw[x]=arr[mid];
sum[x]=1,tag[x]=0;
build(ch[x][0],x,arr,l,mid-1);
build(ch[x][1],x,arr,mid+1,r);
push_up(x);
}
int get_Kth(int K)
{
K++;
int x=rt;
while(K!=sum[ch[x][0]]+1)
{
push_down(x);
if(K<=sum[ch[x][0]])x=ch[x][0];
else K-=sum[ch[x][0]]+1,x=ch[x][1];
}
splay(x,0);
return x;
}
int insert(int pos,char *str,int n)
{
int l=get_Kth(pos),r=get_Kth(pos+1);
splay(l,0),splay(r,l);
build(ch[r][0],r,str,0,n-1);
splay(r,0);
}
void flip(int l,int r)
{
l=get_Kth(l-1),r=get_Kth(r+1);
splay(l,0),splay(r,l);
tag_up(ch[r][0]);
}
void erase(int l,int r)
{
l=get_Kth(l-1),r=get_Kth(r+1);
splay(l,0),splay(r,l);
ch[r][0]=0;
splay(r,0);
}
int main()
{
init();
str[0]='0',str[1]='$';
build(rt,0,str,0,1);
mouse=0;
scanf("%d",&n);
while(n--)
{
int x;
scanf("%s",str);
if(str[0]=='M')
{
scanf("%d",&x);
mouse=x;
}
else if(str[0]=='I')
{
scanf("%d",&x);getchar();
FOR(i,0,x-1)str[i]=getchar();
str[x]='\0';
insert(mouse,str,x);
}
else if(str[0]=='D')
{
scanf("%d",&x);
erase(mouse+1,mouse+x);
}
else if(str[0]=='R')
{
scanf("%d",&x);
flip(mouse+1,mouse+x);
}
else if(str[0]=='G')
printf("%c\n",pw[get_Kth(mouse+1)]);
else if(str[0]=='P')mouse--;
else if(str[0]=='N')mouse++;
}
return 0;
}
BZOJ1269 文本編輯器editor(伸展樹)