平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)

原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html

注意是简单教程,不是入门教程。

splay

1. 旋转:

假设点 y 原是点 x 的 father,旋转操作可以在不改变中序遍历的基础上,将 y 变成 x 的儿子。例如:

 

旋转后:

代码:

int wson(int x){
    return son[fa[x]][1]==x;
}
void pushup(int x){
    tot[x]=cnt[x]+tot[son[x][0]]+tot[son[x][1]];
}
void rotate(int x){
    if (!x)
        return;
    int y=fa[x],z=fa[y],L=wson(x),R=L^1;
    if (z)
        son[z][wson(y)]=x;
    fa[x]=z,fa[y]=x,fa[son[x][R]]=y;
    son[y][L]=son[x][R],son[x][R]=y;
    pushup(y),pushup(x);
}

 

2. splay

可以将一个点旋转到他的一个祖先下面,或者旋转到根的位置。

具体操作:

设当前旋转的点是 x ,y = fa[x] , z = fa[y] 。设 wson(x) 表示节点 x 是他的父亲的左儿子还是右儿子。

如果 wson(x) = wson(y) ,那么先旋 y,再旋 x;

如果 wson(x) $\neq$ wson(y) ,那么先选 x,再旋 x。

单次复杂度均摊 $O(\log n)$ 。

如果每次都旋转 x 的话是可以被卡掉的。

关于 splay 时间复杂度的证明:

https://www.cnblogs.com/zhouzhendong/p/JunTanFenXi.html

反正可以用就行了。注意用法是每找一个点都要 splay ,不管是插入节点还是查找特定值在平衡树中的位置。

代码:

void splay(int x,int k){
    if (!x)
        return;
    if (!k)
        root=x;
    for (int y=fa[x];fa[x]!=k;rotate(x),y=fa[x])
        if (fa[y]!=k)
            rotate(wson(x)==wson(y)?y:x);
}

3.插入

假设插入的值是 v。找到比 v 小的最大点,把它splay到根,找到它的右子树的最小值的位置,把新节点插到这个节点的左子节点位置即可。

4.删除

找到节点,splay到根,找到它的右子树的最左点,splay到根下面,这样就保证了根的右儿子没有左儿子,直接把根ban掉,把根左儿子变成右儿子的左儿子就好了。

5.其他

诸如查排名、查第k大、查前驱后继等比较简单,这里省略了。

关于区间修改:

可以支持翻转、区间覆盖、区间乘等等操作。

具体一些的方法是:打懒标记,每次splay之前先往上走一遍预先下传好标记,再splay。

6.模板 (BZOJ3224)

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int n,root=1,size=1,val[N],cnt[N],son[N][2],fa[N],tot[N];
int wson(int x){
    return son[fa[x]][1]==x;
}
void pushup(int x){
    tot[x]=cnt[x]+tot[son[x][0]]+tot[son[x][1]];
}
void rotate(int x){
    if (!x)
        return;
    int y=fa[x],z=fa[y],L=wson(x),R=L^1;
    if (z)
        son[z][wson(y)]=x;
    fa[x]=z,fa[y]=x,fa[son[x][R]]=y;
    son[y][L]=son[x][R],son[x][R]=y;
    pushup(y),pushup(x);
}
void splay(int x,int k){
    if (!x)
        return;
    if (!k)
        root=x;
    for (int y=fa[x];fa[x]!=k;rotate(x),y=fa[x])
        if (fa[y]!=k)
            rotate(wson(x)==wson(y)?y:x);
}
int find(int x,int v){
    return val[x]==v?x:find(son[x][v>val[x]],v);
}
int findkth(int x,int k){
    if (k<=tot[son[x][0]])
        return findkth(son[x][0],k);
    k-=tot[son[x][0]];
    if (k<=cnt[x])
        return x;
    k-=cnt[x];
    return findkth(son[x][1],k);
}
int findnxt(int x,int v){
    if (!x)
        return 0;
    if (val[x]<=v)
        return findnxt(son[x][1],v);
    else {
        int res=findnxt(son[x][0],v);
        return res?res:x;
    }
}
int findpre(int x,int v){
    if (!x)
        return 0;
    if (val[x]>=v)
        return findpre(son[x][0],v);
    else {
        int res=findpre(son[x][1],v);
        return res?res:x;
    }
}
void insert(int &x,int pre,int v){
    if (!x){
        x=++size;
        val[x]=v,cnt[x]=tot[x]=1,fa[x]=pre;
        splay(x,0);
        return;
    }
    tot[x]++;
    if (val[x]==v){
        cnt[x]++;
        return;
    }
    insert(son[x][v>val[x]],x,v);
}
void Insert(int v){insert(root,0,v);}
void Delete(int v){
    int x;
    splay(x=find(root,v),0);
    if (--cnt[x])
        return;
    splay(findnxt(root,v),root);
    root=son[x][1];
    son[root][0]=son[x][0];
    fa[son[x][0]]=root;
    fa[root]=son[x][0]=son[x][1]=0;
    pushup(root);
}
int Rank(int v){
    splay(find(root,v),0);
    return tot[son[root][0]]+1;
}
int main(){
    val[1]=2147483647;
    cnt[1]=tot[1]=1;
    scanf("%d",&n);
    while (n--){
        int opt,x;
        scanf("%d%d",&opt,&x);
        if (opt==1) Insert(x);
        if (opt==2) Delete(x);
        if (opt==3) printf("%d\n",Rank(x));
        if (opt==4) printf("%d\n",val[findkth(root,x)]);
        if (opt==5) printf("%d\n",val[findpre(root,x)]);
        if (opt==6) printf("%d\n",val[findnxt(root,x)]);
    }
    return 0;
}

 

替罪羊树

1.思想

如果数据是随机的,那么直接暴力插入节点就AC了。

但是直接卡成一条链就TLE了。

那么我们来想想办法。

定期重构?

可以得到 $O(n\sqrt n)$ 的优秀复杂度。

 

这个东西不够智能,我们来给他升升级!

替罪羊树:当一棵子树的左右儿子不平衡到一定程度的时候,我们将整个子树暴力重构。

具体地,我们要设定一个参数 c ,当一个节点的其中左子树或者右子树占当前子树的比重超过c的时候,就将这个子树暴力重构。

注意,暴力重构的时候要从上往下,这样只重构最大的需要被重构的子树,常数较小。

c 的取值一般在 $0.5~1$ 之间,不能取 1 (取1的时候就是暴力),也不要取 0.5 (重构次数太多导致TLE),取 0.75~0.85 差不多就好了。

关于时间复杂度:

首先,树高显然是 $O(\log n)$ 的,所以查询的复杂度是对的。

我们来看看重构的复杂度是什么。

定义势函数 $\phi(T) = \sum_{x} {abs(size[ls]-size[rs])}$ (假设 ls 是 x 的左儿子,rs 是 x 的右儿子),那么,由于树高是 $O(\log n)$ 的,所以显然插入一个节点最多使 $\phi(T)$ 增加 $O(\log n)$ 。

每次暴力重构点 x 的时候,消耗了 $O(size[x])$ 的时间复杂度,使 $\phi(T)$ 至少减少了 $O(size[x]) \times (c-(1-c)) = O(size[x]\cdot (2c-1))$ ,所以重构的时间复杂度是均摊 $O(\log n)$ 的。

2.一道模板题

UOJ#55 紫荆花之恋

替罪羊树思想不一定只能放在平衡树上,还可以放在点分树上。

不过这题还要套个平衡树,可以用splay + 卡常数,或者 Treap 。

代码

#pragma GCC optimize("Ofast","inline")
#include <bits/stdc++.h>
#define clr(x) memset(x,0,sizeof (x))
#define For(i,a,b) for (int i=a;i<=b;i++)
#define Fod(i,b,a) for (int i=b;i>=a;i--)
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define _SEED_ ('C'+'L'+'Y'+'A'+'K'+'I'+'O'+'I')
#define outval(x) printf(#x" = %d\n",x)
#define outvec(x) printf("vec "#x" = ");for (auto _v : x)printf("%d ",_v);puts("")
#define outtag(x) puts("----------"#x"----------")
using namespace std;
typedef long long LL;
LL read(){
    LL x=0,f=0;
    char ch=getchar();
    while (!isdigit(ch))
        f|=ch=='-',ch=getchar();
    while (isdigit(ch))
        x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return f?-x:x;
}
const int N=100005,INF=1.05e9,mod=1e9;
const double checkval=0.80;
namespace spl{
    const int S=N*100;
    int son[S][2],fa[S],val[S],size[S];
    int cnt=0;
    namespace cly{
        int st[S],top=0;
        inline int new_node(){
            return top?st[top--]:++cnt;
        }
        inline void Recover(int x){
            st[++top]=x;
            son[x][0]=son[x][1]=fa[x]=val[x]=size[x]=0;
        }
    }
    using cly::new_node;
    using cly::Recover;
    #define ls son[x][0]
    #define rs son[x][1]
    int new_node(int v){
        int x=new_node();
        val[x]=v;
        return x;
    }
    inline void pushup(int x){
        size[x]=size[ls]+size[rs]+1;
    }
    inline int wson(int x){
        return son[fa[x]][1]==x;
    }
    void rotate(int x){
        int y=fa[x],z=fa[y],L=son[y][1]==x,R=L^1;
        if (z)
            son[z][son[z][1]==y]=x;
        fa[x]=z,fa[y]=x,fa[son[x][R]]=y;
        son[y][L]=son[x][R],son[x][R]=y;
        pushup(y),pushup(x);
    }
    void splay(int x){
        for (int y=fa[x];fa[x];rotate(x),y=fa[x])
            if (fa[y])
                rotate(wson(x)==wson(y)?y:x);
    }
    inline void Ins(int &a,int v){
        register int x=a,c=0;
        int f=0;
        while (x){
            c++;
            size[x]++;
            f=x;
            x=v<val[x]?ls:rs;
        }
        val[son[f][v>=val[f]]=x=new_node()]=v,fa[x]=f,size[x]=1;
        if (!a||c>30)
            splay(a=x);
    }
    int Query(int &rt,int v){
        int ans=0,c=0;
        register int x=rt,p=0;
        while (x){
            p=x,c++;
            if (val[x]<=v)
                ans+=size[ls]+1,x=rs;
            else
                x=ls;
        }
        if (p&&c>30)
            splay(rt=p);
        return ans;
    }
    void Remove(int x){
        if (x)
            Remove(ls),Remove(rs),Recover(x);
    }
    int id[N];
    void Build(int &x,int L,int R,int f){
        if (L>R)
            return;
        int mid=(L+R)>>1;
        fa[x=id[mid]]=f;
        Build(ls,L,mid-1,x);
        Build(rs,mid+1,R,x);
        pushup(x);
    }
    void Build(int &x,vector <int> &v){
        if (v.empty())
            return;
        int n=0;
        sort(v.begin(),v.end());
        for (auto i : v)
            id[++n]=new_node(i);
        Build(x,1,n,0);
    }
    #undef ls
    #undef rs
}
int Test,n;
LL ans=0;
vector <int> e[N];
int fa[N][20],depth[N],len[N];
int LCA(int x,int y){
    if (depth[x]<depth[y])
        swap(x,y);
    Fod(i,16,0)
        if (depth[x]-(1<<i)>=depth[y])
            x=fa[x][i];
    if (x==y)
        return x;
    Fod(i,16,0)
        if (fa[x][i]!=fa[y][i])
            x=fa[x][i],y=fa[y][i];
    return fa[x][0];
}
int Dis(int x,int y){
    return len[x]+len[y]-2*len[LCA(x,y)];
}
int r[N];
namespace dt{
    int fa[N],size[N],mxs[N],depth[N];
    int rt[N],rtf[N];
    int node_cnt=0;
    void Init(){
        clr(fa),clr(size),clr(mxs),clr(depth),clr(rt),clr(rtf);
        depth[0]=-1,node_cnt++;
        size[1]=1,mxs[1]=depth[1]=rt[1]=0;
        spl::Ins(rt[1],0-r[1]);
    }
    int id[N],idc=0;
    void dfs(int x,int pre,int d){
        id[++idc]=x;
        for (auto y : e[x])
            if (y!=pre&&depth[y]>=d)
                dfs(y,x,d);
    }
    int sz[N],msz[N],RT,Size;
    void dfs2(int x,int pre){
        sz[x]=1,msz[x]=0;
        for (auto y : e[x])
            if (y!=pre&&!size[y]){
                dfs2(y,x);
                sz[x]+=sz[y];
                msz[x]=max(msz[x],sz[y]);
            }
        msz[x]=max(msz[x],Size-sz[x]);
        if (!RT||msz[x]<msz[RT])
            RT=x;
    }
    vector <int> id1,id2;
    void dfs3(int x,int pre,int anc){
        sz[x]=1;
        id1.pb(Dis(x,anc)-r[x]);
        if (fa[anc])
            id2.pb(Dis(x,fa[anc])-r[x]);
        for (auto y : e[x])
            if (y!=pre&&!size[y])
                dfs3(y,x,anc),sz[x]+=sz[y];
    }
    void build(int x,int f,int n){
        RT=0,Size=n;
        dfs2(x,0);
        x=RT;
        fa[x]=f,depth[x]=depth[f]+1,size[x]=n,mxs[x]=msz[x];
        id1.clear(),id2.clear();
        dfs3(x,0,x);
        spl::Build(rt[x],id1);
        spl::Build(rtf[x],id2);
        for (auto y : e[x])
            if (!size[y])
                build(y,x,sz[y]);
    }
    void Rebuild(int x,int f){
        idc=0;
        dfs(x,0,depth[x]);
        For(_t,1,idc){
            int i=id[_t];
            spl::Remove(rt[i]),spl::Remove(rtf[i]);
            depth[i]=size[i]=fa[i]=mxs[i]=rt[i]=rtf[i]=0;
        }
        build(x,f,idc);
    }
    void Ins(int x,int f){
        static vector <int> v;
        fa[x]=f,depth[x]=depth[f]+1;
        rt[x]=rtf[x]=0;
        v.clear();
        for (int i=x;i;i=fa[i]){
            v.pb(i),size[i]++;
            mxs[fa[i]]=max(mxs[fa[i]],size[i]);
            spl::Ins(rt[i],Dis(i,x)-r[x]);
            if (fa[i])
                spl::Ins(rtf[i],Dis(fa[i],x)-r[x]);
        }
        node_cnt++;
        reverse(v.begin(),v.end());
        for (auto i : v)
            if (mxs[i]>checkval*size[i]){
                Rebuild(i,fa[i]);
                break;
            }
        if (size[x]>1)
            ans+=spl::Query(rt[x],r[x])-1;
        for (int i=x,f,d;depth[i];i=f){
            f=fa[i],d=Dis(f,x);
            ans+=spl::Query(rt[f],r[x]-d)-spl::Query(rtf[i],r[x]-d);
        }
    }
}
int main(){
    Test=read(),n=read();
    For(i,1,n){
        int f=read()^(ans%mod),c=read();
        r[i]=read();
        if (!f){
            dt::Init();
            printf("%lld\n",ans);
            continue;
        }
        assert(1<=f&&f<i);
        e[i].pb(f),e[f].pb(i);
        fa[i][0]=f;
        For(j,1,16)
            fa[i][j]=fa[fa[i][j-1]][j-1];
        depth[i]=depth[f]+1;
        len[i]=len[f]+c;
        dt::Ins(i,f);
        printf("%lld\n",ans);
    }
    return 0;
}
紫荆花之恋

 

非旋Treap

1. Treap 简介

Treap = Tree + Heap 。

我们考虑给每一个节点随机赋一个值 ckv ,假设这个节点本来的值是 val,那么,我们建一个平衡树,满足对于 val,它是个二叉搜索树,对于 ckv 它满足堆性质,就是满足 $val[ls]\leq val[x]\leq val[rs], ckv[x]\leq ckv[ls], ckv[x] \leq ckv[rs]$。

可以证明这样的树是唯一的。

由于它的随机的,所以树高是 $O(\log n)$ 的。

我们来看看插入节点的时候怎么维护它的性质:旋转!首先插入到一个满足二叉搜索树性质的位置,然后,如果 ckv[x] < ckv[fa[x]] ,那么将 x 上旋,直到满足这个条件为止。

这个需要旋转的 Treap 有一定的局限性,接下来介绍功能更加强大的非旋 Treap 。

2. Merge

合并 Treap x 和 Treap y,需要保证 x 中的任意元素小于 y 中的任意元素

假设 x 为空,则返回 y 。

假设 y 为空,则返回 x 。

假设 ckv[x] < ckv[y] ,则将点 x 作为根, rs[x] = Merge(rs[x], y)

否则,将点 y 作为根, ls[y] = Merge(x, ls[y])  (注意这里 x 要放在前面)

时间复杂度 $O(\log n)$ 。

代码:

    int Merge(int x,int y){
        if (!x||!y)
            return x+y;
        if (ckv[x]<ckv[y]){
            son[x][1]=Merge(son[x][1],y),pushup(x);
            return x;
        }
        else {
            son[y][0]=Merge(x,son[y][0]),pushup(y);
            return y;
        }
    }

3. Split

这个函数的作用就是将一个平衡树前 k 小节点构成的平衡树和剩余节点构成的平衡树。时间复杂度 $O(\log n)$ 。

直接看代码很容易理解的。

代码:

    pair <int,int> Split(int x,int k){
        if (!x)
            return mp(0,0);
        if (k<=size[ls]){
            pair <int,int> p=Split(ls,k);
            ls=p.se,pushup(x);
            return mp(p.fi,x);
        }
        else {
            pair <int,int> p=Split(rs,k-size[ls]-1);
            rs=p.fi,pushup(x);
            return mp(x,p.se);
        }
    }

4. 其他操作

Rank : 直接写。

Find-Kth = Split + Merge 

Insert = Rank + Split + Merge + Merge

Delete = Split + Split + Merge

区间修改:由于每次操作都是自顶向下的,所以可以进行标记下传,所以支持区间修改。

可持久化:由于每次操作都是自顶向下的,而且只改变节点的儿子,所以是可以可持久化的。这一点其他平衡树都做不了。

5. 模板(BZOJ3224)

#include <bits/stdc++.h>
#define clr(x) memset(x,0,sizeof (x))
#define For(i,a,b) for (int i=a;i<=b;i++)
#define Fod(i,b,a) for (int i=b;i>=a;i--)
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define _SEED_ ('C'+'L'+'Y'+'A'+'K'+'I'+'O'+'I')
#define outval(x) printf(#x" = %d\n",x)
#define outvec(x) printf("vec "#x" = ");for (auto _v : x)printf("%d ",_v);puts("")
#define outtag(x) puts("----------"#x"----------")
using namespace std;
typedef long long LL;
LL read(){
    LL x=0,f=0;
    char ch=getchar();
    while (!isdigit(ch))
        f|=ch=='-',ch=getchar();
    while (isdigit(ch))
        x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return f?-x:x;
}
const int N=100005;
int randint(){
#ifdef windows
    return (rand()<<15)^rand();
#else
    return rand();
#endif
}
namespace Treap{
    const int S=N;
    int son[N][2],size[N],val[N],ckv[N];
    int cnt=0;
    #define ls son[x][0]
    #define rs son[x][1]
    int new_node(int v){
        cnt++,val[cnt]=v,ckv[cnt]=randint(),size[cnt]=1;
        return cnt;
    }
    void pushup(int x){
        size[x]=size[ls]+size[rs]+1;
    }
    int Merge(int x,int y){
        if (!x||!y)
            return x+y;
        if (ckv[x]<ckv[y]){
            son[x][1]=Merge(son[x][1],y),pushup(x);
            return x;
        }
        else {
            son[y][0]=Merge(x,son[y][0]),pushup(y);
            return y;
        }
    }
    pair <int,int> Split(int x,int k){
        if (!x)
            return mp(0,0);
        if (k<=size[ls]){
            pair <int,int> p=Split(ls,k);
            ls=p.se,pushup(x);
            return mp(p.fi,x);
        }
        else {
            pair <int,int> p=Split(rs,k-size[ls]-1);
            rs=p.fi,pushup(x);
            return mp(x,p.se);
        }
    }
    int Find(int x,int v){
        return x?(val[x]==v?x:Find(son[x][val[x]<v],v)):0;
    }
    int _Rank(int x,int v){
        return x?(val[x]>v?_Rank(ls,v):size[ls]+1+_Rank(rs,v)):0;
    }
    int Rank(int x,int v){
        return _Rank(x,v-1);
    }
    void Insert(int &x,int v){
        pair <int,int> p=Split(x,_Rank(x,v));
        x=Merge(p.fi,Merge(new_node(v),p.se));
    }
    void Delete(int &x,int v){
        int rk=Rank(x,v);
        pair <int,int> pL=Split(x,rk),pR=Split(pL.se,1);
        x=Merge(pL.fi,pR.se);
    }
    int QRank(int x,int v){
        return Rank(x,v)+1;
    }
    int Find_Kth(int x,int k){
        return k==size[ls]+1?x:(k<=size[ls]?Find_Kth(ls,k):Find_Kth(rs,k-size[ls]-1));
    }
    int Get_pre(int &x,int v){
        return Find_Kth(x,Rank(x,v));
    }
    int Get_nxt(int &x,int v){
        return Find_Kth(x,_Rank(x,v)+1);
    }
    #undef ls
    #undef rs
}
using namespace Treap;
int n,root=0;
int main(){
    srand(_SEED_);
    n=read();
    while (n--){
        int opt=read(),x=read();
        if (opt==1)
            Insert(root,x);
        else if (opt==2)
            Delete(root,x);
        else if (opt==3)
            printf("%d\n",QRank(root,x));
        else if (opt==4)
            printf("%d\n",val[Find_Kth(root,x)]);
        else if (opt==5)
            printf("%d\n",val[Get_pre(root,x)]);
        else if (opt==6)
            printf("%d\n",val[Get_nxt(root,x)]);
    }
    return 0;
}

一些题目(点击进入)

 

posted @ 2019-03-03 22:38 -zhouzhendong- 阅读(...) 评论(...) 编辑 收藏
希望