平衡樹 學習筆記

卡布叻_周深發表於2024-05-28

BST

又稱二分查詢樹,\(BST\) 性質指其左子樹所有節點全職均小於該點,其右子樹所有節點全職均大於該點;同時若對該棵樹進行中序遍歷,所產生的序列為從小到大排序的序列。

利用該性質,從而在 \(O(\log(n))\) 的複雜度內實現查詢排名、第 \(k\) 小(大)值、前驅、後繼等。

當每次插入的資料呈單調性時,其內部將形成一條鏈,從而使複雜度退化為 \(O(n)\)

Treap

當資料隨機時,其內部時趨於平衡的,\(Treap\) 就是基於隨機化與二叉堆使其實現平衡。

對於如下的幾種基本操作單次複雜度均為 \(O(\log(n))\)

旋轉

對於內一個點賦予其一個隨機值,使這棵樹是該隨機值的二叉堆。

具體如何旋轉操作在 \(splay\) 那裡會有更詳細的解釋。

旋轉操作是使 \(Treap\) 達到平衡的核心。

旋轉操作
void rotate(int &p,bool d)
{
    int son=f.ch[d];
    f.ch[d]=t[son].ch[d^1];
    t[son].ch[d^1]=p;
    pushup(p);
    pushup(p=son);
}

插入

找到其應插入的位置,若書中已存在該值則 \(cnt++\) ,否則建立新點,同時根據其隨機值旋轉,使達到平衡。

插入操作
void insert(int &p,int x)
{
    if(p==0)
    {
        p=++tot;
        f.cnt=f.size=1;
        f.val=x;
        f.dat=rand();
        return ;
    }
    f.size++;
    if(f.val==x) 
    {
        f.cnt++;
        return ;
    }
    bool d=f.val<x;
    insert(f.ch[d],x);
    if(f.dat>t[f.ch[d]].dat) rotate(p,d);
}

刪除

找到所要刪除點所在位置,如果其 \(cnt>1\) ,直接 \(cnt--\) 即可。

否則,如果該節點只有一個子節點,那麼直接讓子節點替換其位置即可;若該點為葉子節點,那麼直接刪掉它不會產生任何其他的影響。

因為 \(Treap\) 支援旋轉操作,將其直接旋轉到滿足上面兩種情況即可。

刪除操作
void remove(int &p,int x)
{
    if(p==0) return ;
    if(f.val==x)
    {
        if(f.cnt>1)
        {
            f.cnt--;
            f.size--;
            return ;
        }
        bool d=ls.dat>rs.dat;
        if(f.l==0||f.r==0) p=f.l+f.r;
        else rotate(p,d),remove(p,x);
    }
    else f.size--,remove(f.ch[f.val<x],x);
}

查詢排名

\(x\) 的排名定義為比 \(x\) 小的個數 \(+1\)

因為其滿足 \(BST\) 性質,從根節點開始向下尋找,設當前點權值為 \(val\)\(ls、rs\) 分別指左右兒子,\(cnt\) 指該節點重複插入次數,\(size\) 指對應節點子樹的大小。

  • \(val==x\) ,即已經找到答案,返回 \(ls.size+1\)

  • \(val>x\) ,繼續向其左兒子方向尋找。

  • \(val<x\) ,繼續向右兒子方向尋找,同時答案加上 \(ls.size+cnt\)

在很多時候要查詢的數並不在樹裡,但同時根據比他小的個數加一仍可以求出其排名,常見解決辦法有兩種,不會對複雜度產生較大影響。

  • 在查詢前先插入該點,查詢後再將該點刪除。

  • 當尋找到一個不存在的點時,顯然只有所查詢數不在樹中時才可能出現這種情況,此時令他返回 \(1\) 而不是 \(0\) ,即比他小的個數 \(+1\)

查詢排名
int ask_rk(int p,int x)
{
    if(p==0) return 1;
    if(f.val==x) return ls.size+1;
    if(f.val>x) return ask_rk(f.l,x);
    return ask_rk(f.r,x)+ls.size+f.cnt;
}

查詢第 \(k\) 小(大)值

以查詢第 \(k\) 小值為例,因為其滿足 \(BST\) 性質,設 \(ls、rs\) 分別指左右兒子,\(size\) 指對應節點子樹的大小,\(cnt\) 指該節點重複插入次數。

  • \(ls.size>=k\) ,繼續向其左兒子方向尋找。

  • \(ls.size<k\&\&ls.size+cnt>=k\) ,該節點即為答案。

  • \(ls.size+cnt<k\) ,另 \(k\) 減去 \(ls.size+cnt\) ,繼續向其右兒子方向尋找。

查詢第 k 小值
int ask_val(int p,int x)
{
    if(p==0) return inf;
    if(ls.size>=x) return ask_val(f.l,x);
    if(ls.size+f.cnt>=x) return f.val;
    return ask_val(f.r,x-ls.size-f.cnt);
}

查詢前驅、後繼

以前驅為例。

根據 \(BST\) 性質,找到第一個小於其的位置,之後不停向右兒子方向尋找。

查詢前驅
int pre(int p,int x)
{
    if(p==0) return -inf;
    if(f.val>=x) return pre(f.l,x);
    return max(pre(f.r,x),f.val);
}
查詢後繼
int nxt(int p,int x)
{
    if(p==0) return inf;
    if(f.val<=x) return nxt(f.r,x);
    return min(nxt(f.l,x),f.val);
}

fhq_Treap

又名無旋 \(Treap\) ,以分裂與合併為核心操作代替旋轉使其實現平衡。

其分裂與合併分為按照權值合併與按照樹的大小合併兩種,當維護序列時按照大小分則更加方便,此處以按照權值分為例,另一種將在文藝平衡樹中講解。

分裂

\(p\) 分為 \(x、y\) 兩部分,\(x\) 中節點全部 \(\leq k\)\(y\) 中節點全部 \(>k\)

  • \(val>k\) ,說明其左子樹全部在 \(x\) 內,故 \(y=p\) ,向左兒子方向繼續分裂。

  • 反之,其右子樹全部在 \(y\) 內,則 \(x=p\) ,繼續向右兒子方向分裂。

分裂操作
void split(int p,int k,int &x,int &y)
{
    if(!p) {x=y=0; return ;}
    if(f.val>k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k,t[x].r,y);
    pushup(p);
}

合併

通常是將被分開的兩部分再合併起來,將 \(x、y\) 合併成 \(p\) ,滿足 \(x\) 中節點全部小於 \(y\) 中節點。

此時就可以按照其隨機值合併,使其平衡。

合併操作
int merge(int x,int y)
{
    if(!x||!y) return x+y;
    int p;
    if(t[x].dat>t[y].dat)t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
}

插入

\(k\) 插入。

\(root\) 按照 \(k-1\) 分裂為 \(x、y\) ,再按照 \(x、new、y\) 合併起來。

插入操作
void insert(int &p,int k)
{
    t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
    int x,y;
    split(p,k-1,x,y);
    p=merge(merge(x,tot),y);
}

刪除

\(k\) 刪除。

依舊將 \(root\) 按照 \(k-1\) 分裂成 \(x、y\) ,再將 \(y\) 按照 \(k\) 分裂成 \(is、y\) ,將其按照 \(x、is.ls,is.rs,y\) 合併。

刪除操作
void remove(int &p,int k)
{
    int x,y,is;
    split(p,k-1,x,y),split(y,k,is,y);
    p=merge(merge(x,merge(t[is].l,t[is].r)),y);
}

查詢排名

\(k\) 的排名。

\(root\) 按照 \(k-1\) 分裂為 \(x、y\)\(x.size+1\) 即為所求。

查詢排名
int ask_rk(int p,int k)
{
    int x,y,is;
    split(p,k-1,x,y);
    is=t[x].size+1;
    p=merge(x,y);
    return is;
}

其餘操作與 \(Treap\) 基本一致。

優點

  1. 為所有平衡樹中最好打,最容易理解的一種。

  2. 因為不需要旋轉,所以可以可持久化。

  3. 允許有權值一樣的不同節點,因為其是按照權值分裂的。

  4. 因為按照權值分裂,所以不在樹中的數也可以直接查。

  5. 當其按照樹的大小分裂時,可以實現維護序列,這是 \(Treap\) 做不到的,大多數 \(splay\) 能做的他都能做。

splay

\(splay\) 的核心在於其雙旋操作,使其不依賴於隨機值。

在每次操作後,將該點進行雙旋轉到根節點的位置,從而使複雜度達到均攤單次 \(O(\log(n))\) ,其複雜度分析詳見 oi-wiki

哨兵節點

即新增 \(-inf、inf\) 節點,為極小值和極大值,使其避免查詢出界,會對一些操作產生需要 \(±1\) 的操作。

單旋操作

image

\(x\)\(y\)\(k\) 方向上的兒子.

旋轉後 \(y\)\(x\)\(k\bigoplus 1\) 方向上的兒子。

\(x\)\(k\bigoplus 1\) 方向上的兒子成為 \(y\)\(k\) 方向上的兒子。

其餘不變。

單旋操作
void rotate(int x)
{
    int y=t[x].ff,z=t[y].ff,k=fson(y,x);
    t[z].ch[fson(z,y)]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    pushup(y),pushup(x);
}

splay 操作(雙旋操作)

將點 \(x\) 旋轉到 \(goal\) 的子節點位置。

定義 \(fa_x=y,fa_y=z\) ,當 \(x、y、z\) 滿足在同一條鏈時,先旋轉 \(y\) ,再旋轉 \(x\) ,使其滿足平衡。

如圖:

imageimageimage

\(x\) 不停進行單旋操作,左圖中最大深度為 \(4\) ,右圖最大深度仍為 \(4\) ,隨意其實沒有意義的。

對於上面所說的情況,如果先旋轉 \(y\) ,再旋轉 \(x\) ,最後為:

image

從而達到平衡。

splay 操作
void splay(int x,int goal)
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff,z=t[y].ff;
        if(z!=goal) 
            fson(z,y)^fson(y,x)?rotate(x):rotate(y);
        rotate(x);
    }
    if(goal==0) root=x;
}

另外為了方便,有一個 \(find\) 操作,即找到某點位置,再將其轉到根節點。

find 操作
void find(int x)
{
    int p=root;
    if(!p) return ;
    while(f.ch[x>f.val]&&x!=f.val) p=f.ch[x>f.val];
    splay(p,0);
}

插入操作

找到離 \(x\) 最近的點,若該點權值就是 \(x\) ,則 \(cnt++\) ,否則建一個新的點,最後都要將其旋轉到根節點。

插入操作
void insert(int x)
{
    int p=root,ff=0;
    while(p&&f.val!=x)
        ff=p,
        p=f.ch[x>f.val];
    if(p) {f.cnt++; splay(p,0); return ;}
    p=++tot;
    if(ff) t[ff].ch[x>t[ff].val]=p;
    f.ff=ff,f.val=x,f.size=f.cnt=1;
    splay(p,0);
}

刪除操作

刪除操作需要先找到其前驅與後繼對應的位置,將前驅轉到根節點,再將後繼轉到前驅的右兒子位置,那麼此時他們倆中間夾著的,即後繼的左兒子,即為 \(x\) 的位置,刪除即可。

刪除操作
void remove(int x)
{
    int pre=pre_nxt(x,0),nxt=pre_nxt(x,1);
    splay(pre,0),splay(nxt,pre);
    int p=t[nxt].l;
    if(f.cnt>1) f.cnt--,splay(p,0);
    else t[nxt].l=0;
}

查詢前驅、後繼

以前驅為例。

先找到離 \(x\) 最近的位置,如果該點權值就是 \(x\) ,先到其左兒子,然後向右不斷地找,即為所求。

如果該點值不是 \(x\) ,若其恰好滿足比 \(x\) 小,則該點即為所求,否則和上述一樣即可。

注意此處找到的是其前驅的位置,不是數值,為了方便刪除操作。

查詢前驅、後繼
int pre_nxt(int x,bool d)
{
    find(x);
    int p=root;
    if((f.val>x&&d)||(f.val<x&&!d)) return p;
    p=f.ch[d];
    while(f.ch[d^1]) p=f.ch[d^1];
    return p;
}

查詢排名

此時需要滿足 \(x\) 一定在樹內,所以可以先插入,待查詢後再刪除。

先找到 \(x\) 所在位置,將其轉到根節點位置,則此時其左兒子個數 \(+1\) 即為所求。

由於新增了哨兵節點,實際還需要 \(-1\) ,所以 \(ls.size\) 即為所求。

查詢排名
int ask_rk(int x)
{
    find(x);
    int p=root;
    return ls.size;
}

查詢第 \(k\) 小(大)值

\(Treap\) 一致。

優點

因為其 \(splay\) 操作,所以其能夠進行維護序列等各種操作,完成所有 \(fhq\) 能支援甚至不支援的各種操作,但是常熟更大,碼量更長,不如 \(fhq\) 好理解。

例題

基本操作

前三道為最基本的查詢前驅後繼、第 \(k\) 大值等問題。

luogu P2234 營業額統計

luogu P2286 寵物收養場

luogu P1486 鬱悶的出納員

luogu P3369 普通平衡樹

Treap
#include<bits/stdc++.h>
// #define int long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,tot,root;
struct treap
{
    int ch[2],cnt,size,val,dat;
}t[N];
void pushup(int p)
{
    f.size=ls.size+rs.size+f.cnt;
}
void rotate(int &p,bool d)
{
    int son=f.ch[d];
    f.ch[d]=t[son].ch[d^1];
    t[son].ch[d^1]=p;
    pushup(p);
    pushup(p=son);
}
void insert(int &p,int x)
{
    if(p==0)
    {
        p=++tot;
        f.cnt=f.size=1;
        f.val=x;
        f.dat=rand();
        return ;
    }
    f.size++;
    if(f.val==x) 
    {
        f.cnt++;
        return ;
    }
    bool d=f.val<x;
    insert(f.ch[d],x);
    if(f.dat>t[f.ch[d]].dat) rotate(p,d);
}
void remove(int &p,int x)
{
    if(p==0) return ;
    if(f.val==x)
    {
        if(f.cnt>1)
        {
            f.cnt--;
            f.size--;
            return ;
        }
        bool d=ls.dat>rs.dat;
        if(f.l==0||f.r==0) p=f.l+f.r;
        else rotate(p,d),remove(p,x);
    }
    else f.size--,remove(f.ch[f.val<x],x);
}
int ask_rk(int p,int x)
{
    if(p==0) return 1;
    if(f.val==x) return ls.size+1;
    if(f.val>x) return ask_rk(f.l,x);
    return ask_rk(f.r,x)+ls.size+f.cnt;
}
int ask_val(int p,int x)
{
    if(p==0) return inf;
    if(ls.size>=x) return ask_val(f.l,x);
    if(ls.size+f.cnt>=x) return f.val;
    return ask_val(f.r,x-ls.size-f.cnt);
}
int pre(int p,int x)
{
    if(p==0) return -inf;
    if(f.val>=x) return pre(f.l,x);
    return max(pre(f.r,x),f.val);
}
int nxt(int p,int x)
{
    if(p==0) return inf;
    if(f.val<=x) return nxt(f.r,x);
    return min(nxt(f.l,x),f.val);
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n);
    while(n--)
    {
        int op,x;
        read(op),read(x);
        switch(op)
        {
            case 1: insert(root,x); break;
            case 2: remove(root,x); break;
            case 3: write(ask_rk(root,x)),puts(""); break;
            case 4: write(ask_val(root,x)),puts(""); break;
            case 5: write(pre(root,x)),puts(""); break;
            case 6: write(nxt(root,x)),puts(""); break;
        }
    }
}
fhq_Treap
#include<bits/stdc++.h>
// #define int long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct fhq_treap
{
    int ch[2],val,dat,size;
}t[N];
void pushup(int p) {f.size=ls.size+rs.size+1;}
void split(int p,int k,int &x,int &y)
{
    if(!p) {x=y=0; return ;}
    if(f.val>k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k,t[x].r,y);
    pushup(p);
}
int merge(int x,int y)
{
    if(!x||!y) return x+y;
    int p;
    if(t[x].dat>t[y].dat)t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
}
void insert(int &p,int k)
{
    t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
    int x,y;
    split(p,k-1,x,y);
    p=merge(merge(x,tot),y);
}
void remove(int &p,int k)
{
    int x,y,is;
    split(p,k-1,x,y),split(y,k,is,y);
    p=merge(merge(x,merge(t[is].l,t[is].r)),y);
}
int ask_rk(int p,int k)
{
    int x,y,is;
    split(p,k-1,x,y);
    is=t[x].size+1;
    p=merge(x,y);
    return is;
}
int ask_val(int p,int k)
{
    if(!p) return inf;
    if(ls.size>=k) return ask_val(f.l,k);
    if(ls.size+1>=k) return f.val;
    return ask_val(f.r,k-ls.size-1);
}
int pre(int p,int k)
{
    if(!p) return -inf;
    if(f.val>=k) return pre(f.l,k);
    return max(pre(f.r,k),f.val);
}
int nxt(int p,int k)
{
    if(!p) return inf;
    if(f.val<=k) return nxt(f.r,k);
    return min(nxt(f.l,k),f.val);
}
signed main()
{
    read(n);
    for(int i=1,op,x;i<=n;i++)
    {
        read(op),read(x);
        switch(op)
        {
            case 1: insert(root,x); break;
            case 2: remove(root,x); break;
            case 3: write(ask_rk(root,x)),puts(""); break;
            case 4: write(ask_val(root,x)),puts(""); break;
            case 5: write(pre(root,x)),puts(""); break;
            case 6: write(nxt(root,x)),puts(""); break;
        }
    }
}
splay
#include<bits/stdc++.h>
// #define int long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct splay
{
    int ch[2],ff,size,cnt,val;
}t[N];
bool fson(int x,int y) {return (t[x].ch[1]==y);}
void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
void rotate(int x)
{
    int y=t[x].ff,z=t[y].ff,k=fson(y,x);
    t[z].ch[fson(z,y)]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    pushup(y),pushup(x);
}
void splay(int x,int goal)
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff,z=t[y].ff;
        if(z!=goal) 
            fson(z,y)^fson(y,x)?rotate(x):rotate(y);
        rotate(x);
    }
    if(goal==0) root=x;
}
void find(int x)
{
    int p=root;
    if(!p) return ;
    while(f.ch[x>f.val]&&x!=f.val) p=f.ch[x>f.val];
    splay(p,0);
}
void insert(int x)
{
    int p=root,ff=0;
    while(p&&f.val!=x)
        ff=p,
        p=f.ch[x>f.val];
    if(p) {f.cnt++; splay(p,0); return ;}
    p=++tot;
    if(ff) t[ff].ch[x>t[ff].val]=p;
    f.ff=ff,f.val=x,f.size=f.cnt=1;
    splay(p,0);
}
int pre_nxt(int x,bool d)
{
    find(x);
    int p=root;
    if((f.val>x&&d)||(f.val<x&&!d)) return p;
    p=f.ch[d];
    while(f.ch[d^1]) p=f.ch[d^1];
    return p;
}
void remove(int x)
{
    int pre=pre_nxt(x,0),nxt=pre_nxt(x,1);
    splay(pre,0),splay(nxt,pre);
    int p=t[nxt].l;
    if(f.cnt>1) f.cnt--,splay(p,0);
    else t[nxt].l=0;
}
int ask_rk(int x)
{
    find(x);
    int p=root;
    return ls.size;
}
int ask_val(int p,int x)
{
    if(!p) return inf;
    if(ls.size>=x) return ask_val(f.l,x);
    if(ls.size+f.cnt>=x) return f.val;
    return ask_val(f.r,x-ls.size-f.cnt);
}
signed main()
{
    insert(-inf),insert(inf);
    read(n);
    for(int i=1,op,x;i<=n;i++)
    {
        read(op),read(x);
        switch(op)
        {
            case 1: insert(x); break;
            case 2: remove(x); break;
            case 3: 
                insert(x);
                write(ask_rk(x)),puts(""); 
                remove(x);
                break;
            case 4: write(ask_val(root,x+1)),puts(""); break;
            case 5: write(t[pre_nxt(x,0)].val),puts(""); break;
            case 6: write(t[pre_nxt(x,1)].val),puts(""); break;
        }
    }
}

維護序列操作

luogu P3391 文藝平衡樹

利用平衡樹實現維護序列操作,可以使用 \(fhq\)\(splay\)

  • \(fhq\)

    對於每次旋轉,將其按照樹的大小分裂,先按照 \(r\) 分裂, 再將左半部分按照 \(l-1\) 分裂,則中間一部分即為所求區間,向下打標記即可。

    翻轉即左右兒子互換。

    最後中序遍歷輸出。

    文藝平衡樹(fhq)
    #include<bits/stdc++.h>
    // #define int long long 
    #define endl '\n'
    #define sort stable_sort
    #define f t[p]
    #define ls t[t[p].ch[0]]
    #define rs t[t[p].ch[1]]
    #define l ch[0]
    #define r ch[1]
    using namespace std;
    const int N=1e5+10,inf=0x7f7f7f7f;
    template<typename Tp> inline void read(Tp&x)
    {
        x=0;register bool z=true;
        register char c=getchar();
        for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
        for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
        x=(z?x:~x+1);
    }
    void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
    void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
    int n,m,tot,root;
    struct fhq_treap
    {
        int ch[2],size,dat,val;
        bool add;
    }t[N];
    void pushup(int p) {f.size=ls.size+rs.size+1;}
    void spread(int p)
    {
        if(!f.add||!p) return ;
        swap(f.l,f.r);
        ls.add^=1,rs.add^=1;
        f.add=0;
    }
    void split(int p,int k,int &x,int &y)
    {
        if(!p) {x=y=0; return ;}
        spread(p);
        if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
        else x=p,split(f.r,k-ls.size-1,t[x].r,y);
        pushup(p);
    }
    int merge(int x,int y)
    {
        if(!x||!y) return x+y;
        spread(x),spread(y);
        int p;
        if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
        else t[y].l=merge(x,t[y].l),pushup(p=y);
        return p;
    }
    void insert(int &p,int k)
    {
        t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
        p=merge(p,tot);
    }
    void solve(int &p,int ll,int rr)
    {
        int x,y,is;
        split(p,rr,x,y),split(x,ll-1,x,is);
        t[is].add^=1;
        p=merge(merge(x,is),y);
    }
    void dfs(int p)
    {
        if(!p) return ;
        spread(p);
        dfs(f.l);
        write(f.val),putchar(' ');
        dfs(f.r);
    }
    signed main()
    {
        read(n),read(m);
        for(int i=1;i<=n;i++)
            insert(root,i);
        for(int i=1,ll,rr;i<=m;i++)
            read(ll),read(rr),
            solve(root,ll,rr);
        dfs(root);
    }
    
  • \(splay\)

    找到 \(l-1\) 的位置,定義為 \(x\)\(r+1\) 的位置,定義為 \(y\) ,將 \(x\) 轉到根節點,再將 \(y\) 轉為 \(x\) 的子節點,那麼根節點的右兒子的左兒子即對應所求區間,其餘操作與 \(fhq\) 一致。

    文藝平衡樹(splay)
    #include<bits/stdc++.h>
    // #define int long long 
    #define endl '\n'
    #define sort stable_sort
    #define f t[p]
    #define ls t[t[p].ch[0]]
    #define rs t[t[p].ch[1]]
    #define l ch[0]
    #define r ch[1]
    using namespace std;
    const int N=1e5+10,inf=0x7f7f7f7f;
    template<typename Tp> inline void read(Tp&x)
    {
        x=0;register bool z=true;
        register char c=getchar();
        for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
        for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
        x=(z?x:~x+1);
    }
    void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
    void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
    int n,m,root,tot;
    struct splay
    {
        int ch[2],ff,size,cnt,val;
        bool add;
    }t[N];
    bool fson(int x,int y) {return t[x].ch[1]==y;}
    void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
    void spread(int p)
    {
        if(!p||!f.add) return ;
        swap(f.l,f.r);
        ls.add^=1,rs.add^=1;
        f.add=0;
    }
    void rotate(int x)
    {
        int y=t[x].ff,z=t[y].ff,k=fson(y,x);
        t[z].ch[fson(z,y)]=x;
        t[x].ff=z;
        t[y].ch[k]=t[x].ch[k^1];
        t[t[x].ch[k^1]].ff=y;
        t[x].ch[k^1]=y;
        t[y].ff=x;
        pushup(y),pushup(x);
    }
    void splay(int x,int goal)
    {
        while(t[x].ff!=goal)
        {
            int y=t[x].ff,z=t[y].ff;
            if(z!=goal) 
                fson(z,y)^fson(y,x)?rotate(x):rotate(y);
            rotate(x);
        }
        if(goal==0) root=x;
    }
    void insert(int x)
    {
        int p=root,ff=0;
        while(p&&f.val!=x)
            ff=p,
            p=f.ch[x>f.val];
        if(p) {f.cnt++; splay(p,0); return ;}
        p=++tot;
        if(ff) t[ff].ch[x>t[ff].val]=p;
        f.ff=ff,f.val=x,f.size=f.cnt=1;
        splay(p,0);
    }
    int find(int p,int x)
    {
        spread(p);
        if(ls.size>=x) return find(f.l,x);
        if(ls.size+f.cnt>=x) return p;
        return find(f.r,x-ls.size-f.cnt);
    }
    void solve(int x,int y)
    {
        x=find(root,x),y=find(root,y);
        splay(x,0),splay(y,x);
        t[t[t[root].r].l].add^=1;
    }
    void dfs(int p)
    {
        if(!p) return ;
        spread(p);
        dfs(f.l);
        if(f.val!=1&&f.val!=n+2) write(f.val-1),putchar(' ');
        dfs(f.r);
    }
    signed main()
    {
        read(n),read(m);
        for(int i=1;i<=n+2;i++) insert(i);
        for(int i=1,ll,rr;i<=m;i++)
            read(ll),read(rr),
            solve(ll,rr+2);
        dfs(root);
    }
    

luogu P3165 排序機械臂

因為其不會插入新點,所以可以透過排序得知第 \(k\) 小值的編號。

對於查詢其所在位置,利用 \(splay\) 將其轉到根節點,則根節點的左兒子的 \(size+1\) 即為所求。

區間翻轉操作與文藝平衡樹類似,同時向樹中存的應為下標而不是權值。

注意審題與哨兵節點插入的順序。

機械臂排序
#include<bits/stdc++.h>
// #define int long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot,a[N];
struct aa {int h,id;}e[N];
bool cmp(aa a,aa b) {return a.h==b.h?a.id<b.id:a.h<b.h;}
struct splay
{
    int ch[2],ff,size,cnt,val;
    bool add;
}t[N];
bool fson(int x,int y) {return t[x].ch[1]==y;}
void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
void spread(int p)
{
    if(!p||!f.add) return ;
    swap(f.l,f.r);
    ls.add^=1,rs.add^=1;
    f.add=0;
}
void rotate(int x)
{
    int y=t[x].ff,z=t[y].ff,k=fson(y,x);
    t[z].ch[fson(z,y)]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    pushup(y),pushup(x);
}
void splay(int x,int goal)
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff,z=t[y].ff;
        spread(z),spread(y),spread(x);
        if(z!=goal) 
            fson(z,y)^fson(y,x)?rotate(x):rotate(y);
        rotate(x);
    }
    if(goal==0) root=x;
}
void insert(int x)
{
    int p=root,ff=0;
    while(p&&f.val!=x)
        ff=p,
        p=f.ch[x>f.val];
    if(p) {f.cnt++; splay(p,0); return ;}
    p=++tot;
    if(ff) t[ff].ch[x>t[ff].val]=p;
    f.ff=ff,f.val=x,f.size=f.cnt=1;
    splay(p,0);
}
int find(int p,int x)
{
    spread(p);
    if(ls.size>=x) return find(f.l,x);
    if(ls.size+f.cnt>=x) return p;
    return find(f.r,x-ls.size-f.cnt);
}
void solve(int x,int y)
{
    x--,y++;
    x=find(root,x),y=find(root,y);
    splay(x,0),splay(y,x);
    t[t[t[root].r].l].add^=1;
}
int ask(int x)
{
    splay(x,0);
    return t[t[root].l].size+1;
}
void solve(int i)
{
    int x=i,y=ask(e[i].id);
    solve(x,y);
}
signed main()
{
    srand(time(0));
    insert(1);
    read(n);
    for(int i=2;i<=n+1;i++)
        read(e[i].h),
        e[i].id=i,
        insert(i);
    insert(n+2);
    sort(e+2,e+2+n,cmp);
    for(int i=2;i<=n+1;i++)
        write(ask(e[i].id)-1),putchar(' '),
        solve(i);
}

luogu P4309 最長上升子序列

回想最長上升子序列的 \(DP\) 式子,\(dp_i=\max\limits_{0<j<i\&\&a_j<a_i}\{dp_j\}+1\)

此處由於每次插入的數一定大於之前插入的所有數,所以直接在 \(0<j<i\) 中找到最大的 \(dp_j+1\) 即為 \(dp_i\)

因為其需要動態插入,可以離線後線段樹維護,也可以平衡樹動態維護。

對於沒一個節點定義 \(val\) 表示其子樹中最大的 \(dp\) 值,\(dp\) 表示其自身的 \(dp\) 值,由此將子樹的資訊傳遞給根節點以便查詢。

void pushup(int p) 
{
    f.val=max({ls.val,rs.val,f.dp});
}

插入操作可以用 \(fhq\)\(splay\) 支援。

最長上升子序列
#include<bits/stdc++.h>
// #define int long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct fhq_treap
{
    int ch[2],size,dat,ans,len;
}t[N];
void pushup(int p) 
{
    f.size=ls.size+rs.size+1;
    f.ans=max({ls.ans,rs.ans,f.len});
}
void split(int p,int k,int &x,int &y)
{
    if(!p) {x=y=0; return ;}
    if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k-ls.size-1,t[x].r,y);
    pushup(p);
}
int merge(int x,int y)
{
    if(!x||!y) return x+y;
    int p;
    if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
}
void insert(int &p,int k)
{
    t[++tot].size=1,t[tot].dat=rand();
    int x,y;
    split(p,k,x,y);
    t[tot].len=t[tot].ans=t[x].ans+1;
    p=merge(merge(x,tot),y);
}
signed main()
{
    srand(time(0));
    read(n);
    for(int i=1,x;i<=n;i++)
        read(x),
        insert(root,x),
        write(t[root].ans),puts("");
}

樹上雜湊

對於每個節點,儲存的是其子樹的 \(hash\) 值。

void pushup(int p) 
{
    f.hash=ls.hash*b[rs.size+1]+f.val*b[rs.size]+rs.hash;
}

luogu P4036 火星人

利用樹上雜湊,對於每次詢問,二分答案即可。

查詢一個區間的 \(hash\) 詢問,與文藝平衡樹類似的,找到這個區間,此時的 \(hash\) 值即為所求。

火星人
#include<bits/stdc++.h>
// #define int long long 
#define ull unsigned long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=3e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,tot,root;
ull b[N];
char s[N];
struct fhq_treap
{
    int ch[2],size,dat;
    ull val,hash;
}t[N];
void pushup(int p) 
{
    f.size=ls.size+rs.size+1;
    f.hash=ls.hash*b[rs.size+1]+f.val*b[rs.size]+rs.hash;
}
void split(int p,int k,int &x,int &y)
{
    if(!p) {x=y=0; return ;}
    if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k-ls.size-1,t[x].r,y);
    pushup(p);
}
int merge(int x,int y)
{
    if(!x||!y) return x+y;
    int p;
    if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
}
void insert(int &p,int k,int d)
{
    t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash=d;
    int x,y;
    split(p,k,x,y);
    p=merge(merge(x,tot),y);
}
void change(int &p,int k,int d)
{
    t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash=d;
    int x,y,is;
    split(p,k,x,y),split(x,k-1,x,is);
    p=merge(merge(x,tot),y);
}
int ask(int p,int ll,int rr)
{
    int x,y,is;
    ull ans;
    split(p,rr,x,y),split(x,ll-1,x,is);
    ans=t[is].hash;
    p=merge(merge(x,is),y);
    return ans;
}
bool check(int x,int y,int mid)
{
    return ask(root,x,x+mid-1)==ask(root,y,y+mid-1);
}
int ask(int x,int y)
{
    int ll=0,rr=min(n-x+1,n-y+1),mid,ans=0;
    while(ll<=rr)
    {
        mid=(ll+rr)>>1;
        if(check(x,y,mid)) ans=mid,ll=mid+1;
        else rr=mid-1;
    }
    return ans;
}
void pre()
{
    b[0]=1,b[1]=233;
    for(int i=2;i<=N-1;i++) b[i]=b[i-1]*b[1];
}
signed main()
{
    srand(time(0));
    cin>>s+1; read(m);
    n=strlen(s+1);
    pre();
    for(int i=1;i<=n;i++)
        insert(root,i-1,s[i]-'a'+1);
    while(m--)
    {
        char op,d; int x,y;
        cin>>op;
        if(op=='Q') 
        {
            read(x),read(y);
            if(x>y) swap(x,y);
            write(ask(x,y)),puts("");
        }
        if(op=='R')
            read(x),cin>>d,
            change(root,x,d-'a'+1);
        if(op=='I')
            read(x),cin>>d,
            insert(root,x,d-'a'+1),
            n++;
    }
}

AtCoder abc331_f Palindrome Query

類似的二分答案即可,需要存正串和反串,同時存線上段樹解法,不過多展開。

Palindrome Query
#include<bits/stdc++.h>
// #define int long long 
#define ull unsigned long long 
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=2e6+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,root,tot;
ull b[N];
char s[N];
struct fhq_treap
{
    int ch[2],val,size,dat;
    ull hash[2];
}t[N];
void pushup(int p)
{
    f.size=ls.size+rs.size+1;
    f.hash[0]=ls.hash[0]*b[rs.size+1]+f.val*b[rs.size]+rs.hash[0];
    f.hash[1]=rs.hash[1]*b[ls.size+1]+f.val*b[ls.size]+ls.hash[1];
}
void split(int p,int k,int &x,int &y)
{
    if(!p) {x=y=0; return ;}
    if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k-ls.size-1,t[x].r,y);
    pushup(p);
}
int merge(int x,int y)
{
    if(!x||!y) return x+y;
    int p;
    if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
}
void insert(int &p,int k,int d)
{
    t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash[0]=t[tot].hash[1]=d;
    int x,y;
    split(p,k,x,y);
    p=merge(merge(x,tot),y);
}
void change(int &p,int k,int d)
{
    t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash[0]=t[tot].hash[1]=d;
    int x,y,is;
    split(p,k,x,y),split(x,k-1,x,is);
    p=merge(merge(x,tot),y);
}
int ask(int p,int ll,int rr,bool d)
{
    int x,y,is;
    ull ans;
    split(p,rr,x,y),split(x,ll-1,x,is);
    ans=t[is].hash[d];
    p=merge(merge(x,is),y);
    return ans;
}
void ask(int ll,int rr)
{
    if(ll==rr) {puts("Yes"); return ;}
    int mid=(rr-ll+1)>>1;
    puts(ask(root,ll,ll+mid-1,0)==ask(root,rr-mid+1,rr,1)?"Yes":"No");
}
void pre()
{
    b[0]=1,b[1]=233;
    for(int i=2;i<=N-1;i++) b[i]=b[i-1]*b[1];
}
signed main()
{
    srand(time(0));
    read(n),read(m);
    pre();
    cin>>s+1;
    for(int i=1;i<=n;i++) insert(root,i-1,s[i]-'a'+1);
    while(m--)
    {
        int op,x,y; char d;
        read(op);
        if(op==1) read(x),cin>>d,change(root,x,d-'a'+1);
        else read(x),read(y),ask(x,y);
    }
}

相關文章