Loading

平衡树(fhq-Treap)

远古文章,谨慎食用!

稍微修了一下但感觉还是很naive。。。

fhq-Trea,又名非旋Treap

思路简单,代码简短,功能强大,LCT会多个log,但是其他操作与Splay功能差不了多少,还支持可持久化。

每个节点一个随机权值,维持平衡性。内部权值是个BST(二叉搜索树),随机权值是个Heap(堆)。

2大基本函数(如果学过ODT的话就是跟Split一样重要的函数),可随题目随机应变。

1.merge。建树及操作都要用。

pushup是修改节点信息,和线段树很像。这里只有修改size的作用。

void pushup(int u) {
        size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
    }
int merge(int x,int y) {
        if(!x||!y)return x+y;
        if(rnd[x]<rnd[y]) {
            ch[x][1]=merge(ch[x][1],y);
            pushup(x);
            return x;
        } else {
            ch[y][0]=merge(x,ch[y][0]);
            pushup(y);
            return y;
        }
    }

2.split.按照某种权值分裂(如果把排名当做权值就是排名分裂了),把权值不大于k的分到左子树,大于k的分到右子树。

void split(int u,int k,int &x,int &y) {
        if(!u) {
            x=y=0;
            return;
        }
        if(val[u]<=k)
            x=u,split(ch[u][1],k,ch[u][1],y);
        else
            y=u,split(ch[u][0],k,x,ch[u][0]);
        pushup(u);
    }

其余操作就是在这2个函数的基础上乱搞。基本套路:split到想要的东西,操作,然后把整棵树merge回去。

权值分裂:

P3369 【模板】普通平衡树

0.申请一个新节点。所有信息都记录一下。

int newnode(int x) {
        ++tot;
        rnd[tot]=rand();
        val[tot]=x;
        size[tot]=1;
        return tot;
    }

1.插入.申请一个新节点,和根节点merge一下

void insert(int x) {
        int a,b;
        split(rt,x,a,b);
        rt=merge(merge(a,newnode(x)),b);
    }

2.删除。把这个权值通过split找到,把它的左右子树merge一下。

void del(int x) {
        int a,b,c;
        split(rt,x,a,b);
        split(a,x-1,a,c);
        c=merge(ch[c][0],ch[c][1]);
        rt=merge(merge(a,c),b);
    }

3.查询值为x的数的排名。把比它小的数放到左子树,然后左子树大小加一就是答案。

int rk(int x) {
        int a,b;
        split(rt,x-1,a,b);
        int res=size[a]+1;
        rt=merge(a,b);
        return res;
    }

4.第k小值。经典二叉搜索树操作,不多说了。

int kth(int u,int k) {
        while(true) {
            if(k<=size[ch[u][0]])u=ch[u][0];
            else {
                if(k==size[ch[u][0]]+1)return val[u];
                else k-=size[ch[u][0]]+1,u=ch[u][1];
            }
        }
    }

5.前驱。把不大于它的放到左子树,把左子树里比它小的放到左子树的左子树,然后在左子树的左子树里查询第size[左子树的左子树]小的值。

int lowerbound(int x) {
        int a,b;
        split(rt,x-1,a,b);
        int res=kth(a,size[a]);
        rt=merge(a,b);
        return res;
    }

6.后继。同理。

int upperbound(int x) {
        int a,b;
        split(rt,x,a,b);
        int res=kth(b,1);
        rt=merge(a,b);
        return res;
    }

整合一下就可以AC了。

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int n,rt,tot;
struct fhq {
    int size[N],ch[N][2],val[N],rnd[N];
    void pushup(int u) {
        size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
    }
    int newnode(int x) {
        ++tot;
        rnd[tot]=rand();
        val[tot]=x;
        size[tot]=1;
        return tot;
    }
    int merge(int x,int y) {
        if(!x||!y)return x+y;
        if(rnd[x]<rnd[y]) {
            ch[x][1]=merge(ch[x][1],y);
            pushup(x);
            return x;
        } else {
            ch[y][0]=merge(x,ch[y][0]);
            pushup(y);
            return y;
        }
    }
    void split(int u,int k,int &x,int &y) {
        if(!u) {
            x=y=0;
            return;
        }
        if(val[u]<=k)
            x=u,split(ch[u][1],k,ch[u][1],y);
        else
            y=u,split(ch[u][0],k,x,ch[u][0]);
        pushup(u);
    }
    void insert(int x) {
        int a,b;
        split(rt,x,a,b);
        rt=merge(merge(a,newnode(x)),b);
    }
    void del(int x) {
        int a,b,c;
        split(rt,x,a,b);
        split(a,x-1,a,c);
        c=merge(ch[c][0],ch[c][1]);
        rt=merge(merge(a,c),b);
    }
    int rk(int x) {
        int a,b;
        split(rt,x-1,a,b);
        int res=size[a]+1;
        rt=merge(a,b);
        return res;
    }
    int kth(int u,int k) {
        while(true) {
            if(k<=size[ch[u][0]])u=ch[u][0];
            else {
                if(k==size[ch[u][0]]+1)return val[u];
                else k-=size[ch[u][0]]+1,u=ch[u][1];
            }
        }
    }
    int lowerbound(int x) {
        int a,b;
        split(rt,x-1,a,b);
        int res=kth(a,size[a]);
        rt=merge(a,b);
        return res;
    }
    int upperbound(int x) {
        int a,b;
        split(rt,x,a,b);
        int res=kth(b,1);
        rt=merge(a,b);
        return res;
    }
} T;
int main() {
    scanf("%d",&n);
    for(int i=1; i<=n; i++) {
        int opt,x;
        scanf("%d%d",&opt,&x);
        if(opt==1)T.insert(x);
        if(opt==2)T.del(x);
        if(opt==3)printf("%d\n",T.rk(x));
        if(opt==4)printf("%d\n",T.kth(rt,x));
        if(opt==5)printf("%d\n",T.lowerbound(x));
        if(opt==6)printf("%d\n",T.upperbound(x));
    }
    return 0;
}

排名分裂的应用:

P3391 【模板】文艺平衡树

打个标记就好了,旋转的时候把标记下传给左右子树,然后把排名当权值。

#include<bits/stdc++.h>
using namespace std;
#define rint register int
const int N=100005;
int n,m,tot,rt;
struct FHQTREAP{
	int ch[N][2],pos[N],val[N],size[N];
	bool fl[N];
	void pushup(int u)
	{
		size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
	}
	int newnode(int x)
	{
		val[++tot]=x;
		size[tot]=1;
		pos[tot]=rand();
		return tot;
	}
	void pushdown(int p)
	{
		if(!fl[p])return;
		swap(ch[p][0],ch[p][1]);
		if(ch[p][0])fl[ch[p][0]]^=1;
		if(ch[p][1])fl[ch[p][1]]^=1;
		fl[p]=0;
	}
	int merge(int x,int y)
	{
		if(!x||!y)return x+y;
		if(pos[x]<pos[y])
		{
			pushdown(x);
			ch[x][1]=merge(ch[x][1],y);
			pushup(x);
			return x;
		}
		else 
		{
			pushdown(y);
			ch[y][0]=merge(x,ch[y][0]);
			pushup(y);
			return y;
		}
	}
	void split(int u,int k,int &x,int &y)
	{
		if(!u)
		{
			x=y=0;
			return;
		}
		pushdown(u);
		if(size[ch[u][0]]<k){x=u;split(ch[u][1],k-size[ch[u][0]]-1,ch[u][1],y);}
		else{y=u;split(ch[u][0],k,x,ch[u][0]);}
		pushup(u);
	}
	void print(int u)
	{
		if(!u)return;
		pushdown(u);
		print(ch[u][0]);
		printf("%d ",val[u]);
		print(ch[u][1]);
	}
}T;
int main()
{
	srand(13);
	scanf("%d%d",&n,&m);
	for(rint i=1;i<=n;i++)
		rt=T.merge(rt,T.newnode(i));
	for(int i=1;i<=m;i++)
	{
		int l,r,a,b,c;
		scanf("%d%d",&l,&r);
		T.split(rt,l-1,a,b);
		T.split(b,r-l+1,b,c);
		T.fl[b]^=1;
		rt=T.merge(a,T.merge(b,c));
	}
	T.print(rt);
	return 0;
}

然后一堆类似P2343 宝石管理系统P2286 [HNOI2004]宠物收养场的题就可以随便切了,把板子打熟。

P3960 列队 ,著名的树状数组+二分的题,但是如果思维没那么强,就可以用平衡树按照题意模拟。

一个比较显然的做法是对于最后一列以及每一行(最后一列除外)开一棵平衡树,然后就可以分裂出节点,把它从一棵树里删掉,再插入另一颗树。

这题发现开不下 \(n*m\) 个节点,所以每个节点存的是区间。接着我们发现需要一个节点就没法搞了。讲讲如何把一个节点从区间内分离出来。

把一个节点裂成两个区间,然后把不需要的区间放到右子树,就好了。

这种需要改split的题目难度会高一些,我想了好久才会……

void changesize(int u,int k) {
	if(k>=r[u]-l[u]+1)return;
	int v=l[u]+k-1;
	int nn=newnode(v+1,r[u]);
	r[u]=v;
	ch[u][1]=merge(nn,ch[u][1]);
	upd(u);
}
void split(int u,int k,int &x,int &y) {
	if(!u) {x=y=0;return;}
	if(size[ch[u][0]]>=k) {
		y=u;
		split(ch[u][0],k,x,ch[u][0]);
	} else {
		changesize(u,k-size[ch[u][0]]);
		x=u;
		split(ch[u][1],k-size[ch[u][0]]-(r[u]-l[u]+1),ch[u][1],y);
	}
	upd(u);
}

完整的,注意主函数一堆乱七八糟的不要写错

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=6100005;
int n,m,q,root[N],ch[N][2],rnd[N],size[N],r[N],l[N],tot;
int newnode(int ll,int rr) {
	++tot;
	l[tot]=ll;
	r[tot]=rr;
	size[tot]=rr-ll+1;
	rnd[tot]=rand()*rand();
	return tot;
}
void upd(int u) {
	size[u]=size[ch[u][0]]+r[u]-l[u]+1+size[ch[u][1]];
}
int merge(int x,int y) {
	if(!x||!y)return x+y;
	if(rnd[x]<rnd[y]) {
		ch[x][1]=merge(ch[x][1],y);
		upd(x);
		return x;
	} else {
		ch[y][0]=merge(x,ch[y][0]);
		upd(y);
		return y;
	}
}
void changesize(int u,int k) {
	if(k>=r[u]-l[u]+1)return;
	int v=l[u]+k-1;
	int nn=newnode(v+1,r[u]);
	r[u]=v;
	ch[u][1]=merge(nn,ch[u][1]);
	upd(u);
}
void split(int u,int k,int &x,int &y) {
	if(!u) {x=y=0;return;}
	if(size[ch[u][0]]>=k) {
		y=u;
		split(ch[u][0],k,x,ch[u][0]);
	} else {
		changesize(u,k-size[ch[u][0]]);
		x=u;
		split(ch[u][1],k-size[ch[u][0]]-(r[u]-l[u]+1),ch[u][1],y);
	}
	upd(u);
}
signed main() {
	srand(time(0));
	scanf("%lld%lld%lld",&n,&m,&q);
	for(int i=1; i<=n; ++i)
		root[i]=newnode((i-1)*m+1,i*m-1);
	for(int i=1; i<=n; ++i)
		root[n+1]=merge(root[n+1],newnode(i*m,i*m));
	while(q--) {
		int x,y;
		scanf("%lld%lld",&x,&y);
		if(y!=m) {
			int a,b,c;
			split(root[x],y,a,b);
			split(a,y-1,a,c);
			printf("%lld\n",l[c]);
			int a1,b1,c1;
			split(root[n+1],x,a1,b1);
			split(a1,x-1,a1,c1);
			root[x]=merge(a,merge(b,c1));
			root[n+1]=merge(a1,merge(b1,c));
		} else {
			int a,b,c;
			split(root[n+1],x,a,b);
			split(a,x-1,a,c);
			printf("%lld\n",l[c]);
			root[n+1]=merge(a,merge(b,c));
		}
	}
	return 0;
}

几个月之后我再来补充一下我的感想:

fhq的常数比较大,在这方面与splay齐名

但是有个优秀的性质:树高严格 \(\log\) ,这个性质比较重要,有些操作可以基于这个保证复杂度

它可以很方便的分裂区间,这也是为什么理解起来简单,写起来简单,常数大的原因

大部分时候建议写treap或替罪羊,除非有什么操作比较复杂,那么首选fhq。

为了卡常可以把treap和fhq的操作混用,因为本质是同一个东西。

平衡树往往只是作为工具使用,重要的还是思维,板子打熟就好。

posted @ 2020-03-04 21:14  zzctommy  阅读(575)  评论(0编辑  收藏  举报