1. 程式人生 > >BZOJ1269 文本編輯器editor(伸展樹)

BZOJ1269 文本編輯器editor(伸展樹)

二分 tor ble 進入 define 什麽 平衡 一點 防止

題意

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\)

通過 \(\text{rotate}\) 函數上旋至成為 \(w\) 的兒子,特別的,當 \(w=0\) 時,表示將 \(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(伸展樹)