FHQ Treap入門教程(含洛谷P3369 & LOJ#104 普通平衡樹題解qwq)

前置技能

二叉搜索樹

前置技能有旋轉treap?不存在的(我到現在仍然不會旋轉的treap qwq)

Treap簡介

引用維基百科上一句精闢的話:Treap=Tree+Heap
在Treap上需要維護兩個值:一個優先級pripri,一個節點權值valval
其中優先級取隨機數,滿足小根堆的性質。節點權值滿足二叉搜索樹的性質。
即每個節點的pripri值均小於左、右孩子的pripri值,每個節點的左子樹的valval均小於這個節點的valval值,右子樹的valval大於這個節點的valval值。
示例:
在這裏插入圖片描述
顯然,Treap的每棵子樹都是一棵Treap。
珂以證明(雖然我不會證qwq),Treap的樹高爲lognlogn
普通的Treap是通過旋轉來維護其性質的,而FHQ Treap通過合併(Merge)和分裂(Split)來維護qwq。

Merge操作

比如要合併兩棵分別以x,yx,y爲根的樹,假設xxvalval值<=yyvalval值。
然後按優先級決定誰是根qwq,當xxpripri值比yypripri值小時,xx爲新的根,否則yy爲新的根,這樣珂以使pripri仍然滿足小根堆的性質。
xxpripri值更小時,xxvalval值小於yyvalval值,根據二叉搜索樹的性質,應該把yy放到右子樹中。因此把xx的右孩子與yy合併。
yypripri值更小時,xxvalval值小於yyvalval值,所以應該把xx放到左子樹中,因此把yy的左孩子與xx合併。
最後需要更新根節點的sizsiz值qwq(即子樹內的節點個數)

Merge 的毒瘤代碼:

inline void Update(int x) { //更新子樹大小 
    tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
int Merge(int x,int y) {
    //合併以x和y爲根的樹 
    if(!x || !y)    return x+y;
    //用優先級決定新的根是x還是y 
    if(tree[x].pri<tree[y].pri) {
		rc(x)=Merge(rc(x),y);
        Update(x);
        return x;
    } else {
		lc(y)=Merge(x,lc(y));
        Update(y);
        return y;
    }
}

Split操作

這裏講述的是把一棵樹按權值分爲兩棵子樹的方法。
假設要把ii的子樹中權值&lt;=k&lt;=k的分到以xx爲根的Treap中,剩下的權值&gt;k&gt;k的分到以yy爲根的Treap中。
ii的權值&lt;=k&lt;=k時,根據二叉搜索樹的性質,ii的左子樹中的所有節點權值均&lt;=k&lt;=k
所以把iiii的左子樹內的所有節點都分給xx,然後遞歸把ii的右子樹中&lt;=k&lt;=k的分到xx的右子樹(這樣仍然滿足valval爲二叉搜索樹的性質),ii的右子樹中&gt;k&gt;k的分給yy即可。
ii的權值&gt;k&gt;k時,ii的右子樹中的所有節點權值均&gt;k&gt;k
同理,把iiii右子樹內的所有節點均分給yy,然後遞歸把ii的左子樹中的&gt;k&gt;k的分到yy的左子樹,ii的左子樹中&lt;=k&lt;=k的分給xx即可qwq。

Split 的毒瘤代碼:

void Split(int i,int k,int &x,int &y) {
    //把以i爲根的樹分成以x爲根和以y爲根的樹 
    //把所有權值<=k的分到x中,>k的分到y中 
    if(!i) {
        //若i爲空節點,則x和y也爲空 
        x=y=0;
    } else {
        if(tree[i].val<=k) {
        	//這裏先讓x=i,然後遞歸下去就把i的右子樹中<=k的分到了rc(x) 
            x=i;
            Split(rc(i),k,rc(x),y);
        } else {
            //同理 
            y=i;
            Split(lc(i),k,x,lc(y));
        }
        Update(i);	//記得更新子樹大小 
    }
}

找權值第k大

因爲Treap的valval滿足二叉搜索樹性質,所以類似地按照二叉搜索樹的方法找即可。
比較巧妙的一點是:tree[lc(i)].siz+1tree[lc(i)].siz+1表示左子樹加上根這個節點的總節點數qwq。
代碼:

int kth(int i,int k) {
    //找到i的子樹中val第k大的點的下標 
    while(true) {
        if(k<=tree[lc(i)].siz) {
            //若k比左子樹大小還小 
            //則第k大顯然在左子樹中 
            i=lc(i);
        } else if(k>tree[lc(i)].siz+1) {
            //同理 
            k-=tree[lc(i)].siz+1;
            i=rc(i);
        } else {
            return i;
        }
    }
}

另外放一個新建節點的New函數代碼:

inline int New(int v) {   //新建節點並返回其下標 
    tree[++tot].val=v;
    tree[tot].pri=rand();
    tree[tot].siz=1;
    return tot;
}

例題

洛谷P3369 【模板】普通平衡樹
LOJ #104 普通平衡樹

您需要寫一種數據結構(可參考題目標題),來維護一些數,其中需要提供以下操作:
1.插入xx
2.刪除xx數(若有多個相同的數,因只刪除一個)
3.查詢xx數的排名(排名定義爲比當前數小的數的個數+1。若有多個相同的數,因輸出最小的排名)
4.查詢排名爲xx的數
5.求xx的前驅(前驅定義爲小於xx,且最大的數)
6.求xx的後繼(後繼定義爲大於xx,且最小的數)

這裏講一下操作1和操作2,其他操作比較簡單,看代碼註釋就珂以了qwq(我不會告訴你只是我懶得寫qwq

操作1:

因爲Merge操作是需要xx的權值&lt;=y&lt;=y的權值,所以不能直接New一個節點然後直接大莉Merge qwq。
假設當前需要插入的節點權值爲num,我們考慮把這棵Treap按照權值分爲兩棵樹,權值&lt;=num&lt;=num的分到xx&gt;num&gt;num的分到yy
所以珂以把xx和New得到的節點合併,再把得到的結果與yy合併。

操作2:

假設要刪掉一個權值爲numnum的節點。
這裏珂以考慮把所有權值爲numnum的節點分到一棵樹中,然後刪根節點。
具體實現方法:把所有權值&lt;=num&lt;=num的節點分到xx,權值&gt;num&gt;num的分到zz
然後把z\color{red}z中權值&lt;=num1&lt;=num-1的分到xx,權值&gt;=num&gt;=num的分到yy
不難發現這樣yy中存的都是權值爲numnum的點,此時把根節點刪掉即可qwq

例題代碼

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<vector>
#define re register int
#define lc(x) tree[x].ls
#define rc(x) tree[x].rs
using namespace std;
typedef long long ll;
int read() {
    re x=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9') {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') {
        x=10*x+ch-'0';
        ch=getchar();
    }
    return x*f;
}
const int Size=200005;
struct node {
    int ls,rs;  //左右孩子 
    int val;    //權值,左<根<右 
    int pri;    //優先級,維護小根堆 
    int siz;    //子樹大小 
} tree[Size];
int n,tot;
inline void Update(int x) { //更新子樹大小 
    tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
inline int New(int v) {   //新建節點並返回其下標 
    tree[++tot].val=v;
    tree[tot].pri=rand();
    tree[tot].siz=1;
    return tot;
}
int Merge(int x,int y) {
    //合併以x和y爲根的樹 
    if(!x || !y)    return x+y;
    //用優先級決定新的根是x還是y 
    if(tree[x].pri<tree[y].pri) {
		rc(x)=Merge(rc(x),y);
        Update(x);
        return x;
    } else {
		lc(y)=Merge(x,lc(y));
        Update(y);
        return y;
    }
}
void Split(int i,int k,int &x,int &y) {
    //把以i爲根的樹分成以x爲根和以y爲根的樹 
    //把所有權值<=k的分到x中,>k的分到y中 
    if(!i) {
        //若i爲空節點,則x和y也爲空 
        x=y=0;
    } else {
        if(tree[i].val<=k) {
        	//這裏先讓x=i,然後遞歸下去就把i的右子樹中<=k的分到了rc(x) 
            x=i;
            Split(rc(i),k,rc(x),y);
        } else {
            //同理 
            y=i;
            Split(lc(i),k,x,lc(y));
        }
        Update(i);	//記得更新子樹大小 
    }
}
int kth(int i,int k) {
    //找到i的子樹中val第k大的點的下標 
    while(true) {
        if(k<=tree[lc(i)].siz) {
            //若k比左子樹大小還小 
            //則第k大顯然在左子樹中 
            i=lc(i);
        } else if(k>tree[lc(i)].siz+1) {
            //同理 
            k-=tree[lc(i)].siz+1;
            i=rc(i);
        } else {
            return i;
        }
    }
}
inline int Get_K(int rt,int rk) {
    //得到第k大的點的權值 
    return tree[kth(rt,rk)].val;
}
int main() {
    //暴力**不可避 
    srand(19260817);
    n=read();
    int root=0;
    int x=0,y=0,z=0;
    while(n--) {
        int op=read();
        int num=read();
        if(op==1) {
            //插入num 
            Split(root,num,x,y);
            root=Merge(Merge(x,New(num)),y);
        } else if(op==2) {
            //刪除num 
            Split(root,num,x,z);
            //此時x中存了所有權值<=num的點 
            Split(x,num-1,x,y);
            //此時x中存了所有權值<=num-1的點 
            //因此y中存了所有權值爲num的點 
            //然後把y的根節點刪掉 
            y=Merge(lc(y),rc(y));
            root=Merge(Merge(x,y),z);
        } else if(op==3) {
            //查詢num的排名 
            //x中存的是所有權值<=num-1的點 
            Split(root,num-1,x,y);
            //因此x的大小+1就是num的排名 
            printf("%d\n",tree[x].siz+1);
            root=Merge(x,y);
        } else if(op==4) {
            //查詢第num小的數 
            printf("%d\n",Get_K(root,num));
        } else if(op==5) {
            //查詢num的前驅 
            //x存的是所有權值<=num-1的點 
            //x中第(x子樹大小)小的點就是num前驅
            Split(root,num-1,x,y);
            printf("%d\n",Get_K(x,tree[x].siz));
            root=Merge(x,y);
        } else {
            //x的後繼(基本同理) 
            //y存的是所有權值>num的點 
            //因此y中第一小的就是num前驅 
            Split(root,num,x,y);
            printf("%d\n",Get_K(y,1));
            root=Merge(x,y);
        }
    }
    return 0;
}

其他題

bzoj3173 & 洛谷P4309 [TJOI2013]最長上升子序列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章