FHQ-Treap 学习笔记

FHQ-Treap 学习笔记

Treap = Tree + Heap.

Treap 是一种弱平衡二叉树,可以看作是笛卡尔树:其每个点有一个二元组 \((Key, Value)\)\(Key\) 满足二叉搜索树的性质,而 \(Value\) 满足堆的性质(一般是小根堆)。其中 \(Key\) 是我们实际用到的信息,而 \(Value\) 则是一个随即的权值,用来保证其期望高度为 \(\log n\)

旋转 Treap 通过旋转操作来维护其性质,而无旋 Treap,即 FHQ-Treap 则通过分裂合并的方式来实现。

分裂(Split)

功能:将一棵 Treap 按照一个值 \(k\) 分裂成两棵,第一棵的所有 \(Key\)\(< k\),第二课的所有 \(Key\)\(\ge k\)

考虑当前这棵 Treap 的根节点,如果其 \(Key < k\),那么他以及他的左子树一定都在第一棵 Treap 里,而右子树又变成了一个子问题,只要分裂好之后将第一棵 Treap 的根作为当前节点的右儿子即可。
当前节点 \(Key \ge k\) 同理,当前节点及其右子树都在第二棵 Treap 中,递归处理左儿子,将其分裂出来的第二棵 Treap 的根作为当前节点的左儿子即可。
当这棵树为空的时候则不需要分裂了,直接返回空节点即可。

具体实现时可以使用传实参的方法。代码如下:

void split(int now,int k,int &u,int &v) {
    if (now==0) {u=v=0;return ;}// 空节点
    if (key[now]<k) u=now,split(rs[now],k,rs[now],v);// 根节点在第一棵 Treap 中
    else v=now,split(ls[now],k,u,ls[now]);// 根节点在第二棵 Treap 中
    push_up(now);// 由于分裂会导致树的形态变化(合并也会),所以要 push_up(类似于线段树)保证节点信息(如子树大小等)正确。
    return ;
}

合并(Merge)

功能:合并两棵 Treap,前提是第一棵 Treap 的所有 \(Key\) 都小于等于第二棵 Treap 的任意 \(Key\)

考虑当前两棵 Treap 的根,若第一棵树的根的 \(Value\) 更小,则让其作为合并后 Treap 的根,其左子树不变,于是变成了右子树与第二棵 Treap 合并。
第二棵树的根的 \(Value\) 更小同理,递归合并第一棵树和第二棵树的左子树即可。
当某一棵树为空的时候直接返回另一棵树即可。

代码如下:

int merge(int u,int v) {// 返回值为合并后根的编号。
    if (u==0||v==0) return u+v;// 其中一棵树为空。
    if (val[u]<val[v]) {rs[u]=merge(rs[u],v);push_up(u);return u;}// 第一棵 Treap 的根的 Value 更小。
    else {ls[v]=merge(u,ls[v]);push_up(v);return v;}// 第二棵 Treap 的根的 Value 更小。
}

通过 Split 和 Merge 可以实现平衡树的基础操作。

基础操作

插入

设插入的值为 \(k\)

先按 \(k\) Split,然后新建一个 \(Key = k\) 的点,按顺序将第一棵树、新节点、第二棵树合并起来。

void insert(int k) {
    int u,v;
    split(root,k,u,v);
    root=merge(merge(u,new_node(k)),v);// root 用来存整个 Treap 的根的编号。
    // new_node 会新建一个 Key = k 的节点。
    return ;
}

删除

设删除的值为 \(k\)

先按 \(k\) Split,再按 \(k+1\) Split 第二棵树,这样得到三棵树,第二棵树上所有点都满足 \(Key = k\)。我们通过合并第二棵树的左右儿子的方式删除第二棵树的根,最后再把剩下的三棵树按顺序合并起来即可。

void erase(int 	k) {
    int u,v,w;
    split(root,k,u,v);
    split(v,k+1,v,w);
    v=merge(ls[v],rs[v]);
    root=merge(merge(u,v),w);
    return ;
}

查询排名

\(k\) Split,答案即为第一棵树的大小 \(+ 1\)

int order_of_key(int k) {
    int u,v;
    split(root,k,u,v);
    int ans=size[u]+1;
    root=merge(u,v);// 用完记得合并回去啊。
    return ans;
}

查询排名为 k 的数

按 BST 的写法写即可。

int find_by_order(int rk) {
    int now=root;
    while (now)
        if (rk==size[ls[now]]+1) return key[now];
        else if (rk<=size[ls[now]]) now=ls[now];
        else rk-=size[ls[now]]+1,now=rs[now];
    return -1;// 一般情况下不会执行到这一步,如果执行了要么是询问的 rk 出问题了要么是你 Treap 写萎了(^_^)。
}

查询前驱&后继

即查询排名比查询的值小 \(1\) 或大 \(1\) 的数。

int find_pre(int k) {return find_by_order(order_of_key(k)-1);}
int find_suf(int k) {return find_by_order(order_of_key(k+1));}

完整代码

struct FHQ_Treap {
	int root,cnt;
	int key[1100005],val[1100005],size[1100005];
	int ls[1100005],rs[1100005];
	int new_node(int k) {cnt++;key[cnt]=k;val[cnt]=gen();size[cnt]=1;return cnt;}
	void push_up(int now) {size[now]=size[ls[now]]+size[rs[now]]+1;return ;}
	int merge(int u,int v) {
		if (u==0||v==0) return u+v;
		if (val[u]<val[v]) {rs[u]=merge(rs[u],v);push_up(u);return u;}
		else {ls[v]=merge(u,ls[v]);push_up(v);return v;}
	}
	void split(int now,int k,int &u,int &v) {
		if (now==0) {u=v=0;return ;}
		if (key[now]<k) u=now,split(rs[now],k,rs[now],v);
		else v=now,split(ls[now],k,u,ls[now]);
		push_up(now);
		return ;
	}
	void insert(int k) {
		int u,v;
		split(root,k,u,v);
		root=merge(merge(u,new_node(k)),v);
		return ;
	}
	void erase(int 	k) {
		int u,v,w;
		split(root,k,u,v);
		split(v,k+1,v,w);
		v=merge(ls[v],rs[v]);
		root=merge(merge(u,v),w);
		return ;
	}
	int order_of_key(int k) {
		int u,v;
		split(root,k,u,v);
		int ans=size[u]+1;
		root=merge(u,v);
		return ans;
	}
	int find_by_order(int rk) {
		int now=root;
		while (now)
			if (rk==size[ls[now]]+1) return key[now];
			else if (rk<=size[ls[now]]) now=ls[now];
			else rk-=size[ls[now]]+1,now=rs[now];
		return -1;
	}
	int find_pre(int k) {return find_by_order(order_of_key(k)-1);}
	int find_suf(int k) {return find_by_order(order_of_key(k+1));}
};

文艺平衡树

题目链接:https://www.luogu.com.cn/problem/P3391

写一种数据结构,来维护一个有序数列,要求支持区间翻转操作。

参考前面实现平衡树基础操作的方式,我们考虑每次将操作区间 Split 出来,然后操作完再 Merge 回去。

然而序列显然不会有序,故 Treap 不能按值维护了。
我们考虑根据子树大小来分裂,即将 Treap 分裂成两棵树,使得第一棵树的大小恰好为某个值。

void split(int now,int sz,int &u,int &v) {
    if (now==0) {u=v=0;return ;}
    if (size[ls[now]]<sz) u=now,split(rs[now],sz-size[ls[now]]-1,rs[now],v);
    else v=now,split(ls[now],sz,u,ls[now]);
    push_up(now);
    return ;
}

对于区间翻转,我们可以使用类似于线段树的懒标记,只要每次访问到当前点时 push_down 即可。

void reverse(int l,int r) {
    int u,v,w;
    split(root,l-1,u,v);
    split(v,r-l+1,v,w);
    lzy[v]^=1;
    swap(ls[v],rs[v]);
    merge(merge(u,v),w);
    return ;
}

完整代码:

//Think twice,code once.
#include<chrono>
#include<random>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

int n,m;
mt19937 gen(chrono::system_clock::now().time_since_epoch().count());
struct Fido_Puppy_Treap {
	int root,cnt;
	int key[100005],val[100005],size[100005],lzy[100005];
	int ls[100005],rs[100005];
	int new_node(int k) {cnt++;key[cnt]=k;val[cnt]=gen();size[cnt]=1;return cnt;}
	void push_up(int now) {size[now]=size[ls[now]]+size[rs[now]]+1;return ;}
	void push_down(int now) {
		swap(ls[ls[now]],rs[ls[now]]);
		swap(ls[rs[now]],rs[rs[now]]);
		lzy[ls[now]]^=1;lzy[rs[now]]^=1;
		lzy[now]=0;
		return ;
	}
	int merge(int u,int v) {
		if (u==0||v==0) return u+v;
		if (lzy[u]) push_down(u);
		if (lzy[v]) push_down(v);
        // 访问到就下传。
		if (val[u]<val[v]) {rs[u]=merge(rs[u],v);push_up(u);return u;}
		else {ls[v]=merge(u,ls[v]);push_up(v);return v;}
	}
	void split(int now,int sz,int &u,int &v) {
		if (now==0) {u=v=0;return ;}
		if (lzy[now]) push_down(now);// 访问到就下传。
		if (size[ls[now]]<sz) u=now,split(rs[now],sz-size[ls[now]]-1,rs[now],v);
		else v=now,split(ls[now],sz,u,ls[now]);
		push_up(now);
		return ;
	}
	void insert(int k) {root=merge(root,new_node(k));return ;}// 这里在最后插电,故不用分裂,直接 Merge 即可。
	void reverse(int l,int r) {
		int u,v,w;
		split(root,l-1,u,v);
		split(v,r-l+1,v,w);
		lzy[v]^=1;
		swap(ls[v],rs[v]);
		merge(merge(u,v),w);
		return ;
	}
	void print(int now) {// 按顺序输出结果,直接对先序遍历即可。
		if (now==0) return ;
		if (lzy[now]) push_down(now);// 访问到就下传。
		print(ls[now]);
		printf("%d ",now);
		print(rs[now]);
		return ;
	}
}tr;

int main() {
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++) tr.insert(i);
	while (m--) {
		int l,r;
		scanf("%d%d",&l,&r);
		tr.reverse(l,r);
	}
	tr.print(tr.root);
	puts("");
	return 0;
}
posted @ 2023-02-05 12:56  Mine_King  阅读(64)  评论(0编辑  收藏  举报