重探 FHQ Treap

感觉好多时间前学的 FHQ Treap,然后当时巨多大蒙古。现在接触了很多东西之后逐渐能明白这个很酷的数据结构,然后还深入感受了一下这个数据结构的妙处。

(注意:本篇文章涉及资本主义和共产主义,为了保全自身,请勿将关于政治的描述进行当今社会的带入,仅做类比)。

此外,本文还从比较地分析了权值树和平衡树的不同与取舍,笛卡尔树等。

维护平衡

平衡树最本质的是一个二叉搜索树,满足其中序遍历是一个有序的序列,左子树 \(<\)\(<\) 右儿子。但是因为很多原因,会导致它“不平衡”,即深度很大导致复杂度会爆炸。所以,各种各样的平衡树就想出了很多方法进行平衡树的形态。比如 splay 和普通 treap 是通过旋转让树平衡,替罪羊树是定期暴力重构让树平衡,而非旋 treap 是通过一个更加简洁的分裂合并来完成的。

仅从合并而言,有很多类似的东西:线段树合并,trie 合并,可并堆等等。其实 fhq-treap 本质也差不多,甚至更为简单:比如不像左偏树一样要保持左偏。在判断上也比线段树和 trie 会简单(因为只需要比较一个节点值的大小)。从分裂而言其实也是一样的,只不过是合并的一个逆的操作。

但是这样的话是不够的,因为你会发现如果不能保证均摊复杂度(线段树合并是有均摊复杂度保证的),那是非常容易炸的。在合并 \(x,y\) 两课树的时候,容易发现其实谁做根都无所谓。我们这里先假设 \(x\) 子树内全部小于 \(y\) 子树(这个东西是在分裂的时候得到保证的),那么其实有两种选择,要么 \(x\)\(y\) 的左儿子,要么 \(y\)\(x\) 的右儿子。此时,fhq treap 最酷的地方就来了:给每个节点赋予一个随机的附加权值 \(w\),然后实行权力至上的游戏:谁附加权值大谁做爸。这样是可以保证树的深度为 \(O(\log n)\) 级别,就保证了重构的复杂度和其他操作的复杂度。

int nw(int val) {return v[++tot]=val,w[tot]=rand(),sz[tot]=1,tot;}
void upd(int u) {sz[u]=sz[ch[u][0]]+sz[ch[u][1]]+1;} 
int merge(int x,int y) {
	if(!x||!y) return x+y;
	else if(w[x]>w[y]) return ch[x][1]=merge(ch[x][1],y), upd(x), x;
	else return ch[y][0]=merge(x,ch[y][0]), upd(y), y;
}
void split(int x,int val,int &l,int &r) {
	if(!x) {l=r=0; return;}
	else if(v[x]<=val) l=x, split(ch[x][1],val,ch[x][1],r);
	else r=x, split(ch[x][0],val,l,ch[x][0]);
	upd(x);
}

然后各种各样的操作就简单了。

模板

  • 找最值
    暴力找即可。

  • 插入节点(权值为 \(v\)
    考虑先把树按 \(v\) 拆成两棵树 \(x\le v<y\),新建一个点 \(z\),然后从小到大(反过来亦可)合并 \(x,z,y\)

  • 删除节点(权值为 \(v\)
    考虑先把树按 \(v\) 拆成两棵树 \(x\le v<y\),这个目的是提取出 \(v\) 作为最大权值的子树 \(x\)。然后把 \(x\) 按照 \(v-1\) 的权值拆成两颗树 \(x_0<x_1=v\),然后把 \(v\) 的根节点给删掉,即合并 \(x_1\) 的两个儿子,然后按顺序合并 \(x_0,x_1,y\)

  • 前驱(权值为 \(v\)
    考虑把树按 \(v-1\) 拆成两棵树 \(x<v<y\),目的是提取出 \(<v\) 的子树 \(x\),然后找最大值。

  • 后继(权值为 \(v\)
    考虑把树按 \(v\) 拆成两棵树 \(x\le v<y\),目的是提取出 \(>v\) 的子树 \(y\),然后找最小值。

  • lower bound rank(权值为 \(v\)
    考虑把树按 \(v-1\) 拆成两颗树 \(x<v<y\),目的是提取出 \(<v\) 的子树,然后即 \(x\) 的大小 \(+1\)

  • upper bound rank(权值为 \(v\)
    考虑把树按 \(v\) 拆成两棵树 \(x\le v<y\),目的是提取出 \(\le v\) 的子树,然后即 \(x\) 的大小 \(+1\)

  • k 大
    考虑直接在二分查找树上二分即可,和权值树类似。

//P6136(平衡树模板)所需要的操作
namespace Treap {
	int v[N],w[N],ch[N][2],sz[N],tot,root;
	int nw(int val) {return v[++tot]=val,w[tot]=rand(),sz[tot]=1,tot;}
	void upd(int u) {sz[u]=sz[ch[u][0]]+sz[ch[u][1]]+1;} 
	int merge(int x,int y) { //x<y
		if(!x||!y) return x+y;
		else if(w[x]>w[y]) return ch[x][1]=merge(ch[x][1],y), upd(x), x;
		else return ch[y][0]=merge(x,ch[y][0]), upd(y), y;
	}
	void split(int x,int val,int &l,int &r) {
		if(!x) {l=r=0; return;}
		else if(v[x]<=val) l=x, split(ch[x][1],val,ch[x][1],r);
		else r=x, split(ch[x][0],val,l,ch[x][0]);
		upd(x);
	}
	void ins(int v) {
		int x,y; split(root,v,x,y); root=merge(merge(x,nw(v)),y);
	}
	void del(int v) {
		int x,y,x0,x1; split(root,v,x,y), split(x,v-1,x0,x1);
		root=merge(merge(x0,merge(ch[x1][0],ch[x1][1])),y);
	}
	int fmax(int x) {return x==0?-inf:max(fmax(ch[x][1]),v[x]);}
	int fmin(int x) {return x==0?inf:min(fmin(ch[x][0]),v[x]);}
	int pre(int v) {
		int x,y,res; split(root,v-1,x,y); res=fmax(x); merge(x,y);
		return res;
	}
	int nxt(int v) {
		int x,y,res; split(root,v,x,y); res=fmin(y); merge(x,y);
		return res;
	}
	int rk(int v) {
		int x,y,res; split(root,v-1,x,y); res=sz[x]+1; merge(x,y);
		return res;
	}
	int kth(int x,int k) {
		if(k<=sz[ch[x][0]]) return kth(ch[x][0],k);
		else if(k==sz[ch[x][0]]+1) return v[x];
		else return kth(ch[x][1],k-sz[ch[x][0]]-1);
	}
}

其实我们发现,以上的操作权值树(线段树,trie)都是可以支持的(包括分裂,合并),也可以通过类似的方法进行维护。但是我们发现权值树其实本质上是固定了结构,就可以通过新加一些节点维护部分节点的信息,然后可以直接通过搜新加的维护结构的节点的信息来做以上操作。其优势在于结构是静态的,可能会更加方便;而劣势在于需要花很多空间(\(\log n\) 倍)来记录这些信息,而各类平衡树都可以通过各种操作来让每个节点发挥自己最大的价值。

形象地说,权值树相当于一个固定的官僚结构,每次操作都通过上传和下放来完成,结构静态固定,但是人多(压缩 trie 叫裁剪冗员);平衡树相当于一个不固定的团体结构(人民公社?),每个节点既负责了存储值还负责了统计和管理,每次操作都会让比较方便(复杂度小)的节点进行统计,结构动态,所以人少。

笛卡尔树相关

FHQ Treap 其实还有一个东西,就是维护笛卡尔树。笛卡尔树和 Treap 本质是一样的:下标作为 BST 的比较依据,然后值作为堆的比较依据(即所谓附加值)。或者不用下标也行(对于第一维排个序就行),Treap 其实是一种权值随机的笛卡尔树。

不过注意一点:笛卡尔树是固定的,而且会出现不平衡的状态。所以只有在随机或者有均摊的保证下,才有能有正确的复杂度。

由于建立笛卡尔树是 \(O(n)\) 的(可以用单调栈)。 所以 Treap 如果在初始序列给定的情况下,可以直接用类似的方法建树。在特定情况下可以对复杂度进行一些可能用处不大的优化。

例题:ZJOI2012 小蓝的好友。

首先补集转换一下,算出有多少个区域没有资源点。这类问题有一个类似的方法:扫描线。考虑从上往下扫,然后维护一些东西。本题可以维护一个序列 \(S_{p}\) 表示每一列的比 \(i\) 小的最靠近 \(i\) 的资源点的纵坐标。设 \(M_p(a,b)\) 表示 \([a,b]\) 区间 \(S_p\) 的最大值,则有每条扫描线的贡献为 \(\sum_{i,j\in[1,c],i\le j} p-M_p(i,j)=\frac{c(c+1)}{2}p-\sum M[i,j]\)

考虑对每个 \(S_{p,i}\) 计算贡献。即对于每个横坐标,计算它能作为最大值的最大区间长度。这可以用笛卡尔树去做。

考虑维护这个 \(S_p\) 的笛卡尔树(横坐标为树的关键词 \(v\),纵坐标(\(S\))为堆的关键词 \(w\))。笛卡尔树的左节点代表左区间不大于这个节点的最大节点。利用这个性质,我们得知对于每个 \(S_p\),其贡献为 \(\frac{c(c+1)}{2}p-\sum_u w_u(s_{l_u}+1)(s_{r_u}+1)\)。然后维护笛卡尔树用 treap 即可(因为要单点的修改)。因为数据随机所以复杂度正确。

所以我们每个节点都维护一下这个即可。

#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=1e5+9,inf=(1<<30);
typedef pair<int,int>pii;

inline long long read() {
	long long res=0, w=1; char c=getchar();
	while(!isdigit(c)) {if(c=='-') w=-1; c=getchar();}
	while(isdigit(c)) {res=res*10+c-48; c=getchar();}
	return res*w;
}

namespace treap {
	int ls[N],rs[N],sz[N],v[N],w[N],val[N],tot,root;
	int nww(int vv,int ww) {return sz[++tot]=1,v[tot]=vv,w[tot]=val[tot]=ww,tot;}
	void upd(int x) {sz[x]=sz[ls[x]]+sz[rs[x]]+1, val[x]=val[ls[x]]+val[rs[x]]+w[x]*(sz[ls[x]]+1)*(sz[rs[x]]+1);}
	int build(int l,int r) {
		if(l>r) return 0;
		else if(l==r) return sz[l]=1,v[l]=l,l;
		else {
			int m=(l+r)/2;
			ls[m]=build(l,m-1), rs[m]=build(m+1,r), sz[m]=sz[ls[m]]+sz[rs[m]]+1, v[m]=m;
			return m;
		}
	}
	int merge(int x,int y) {
		if(!x||!y) return x+y;
		else if(w[x]>w[y]) return rs[x]=merge(rs[x],y), upd(x), x;
		else return ls[y]=merge(x,ls[y]), upd(y), y;
	}
	void split(int x,int val,int &l,int &r) {
		if(!x) {l=r=0; return;}
		else if(v[x]<=val) l=x, split(rs[x],val,rs[x],r);
		else r=x, split(ls[x],val,l,ls[x]);
		upd(x);
	}
	void mdf(int val,int nw) {
		int x,y,x1,x2; split(root,val,x,y), split(x,val-1,x1,x2);
		w[x2]=nw, root=merge(merge(x1,x2),y);
	}
	int qry() {return val[root];}
}
using namespace treap;

int r,c,n,ans;
vector<int>t[N];

signed main() {
	r=read(), c=read(), n=read();
	rep(i,1,n) {
		int x=read(), y=read();
		t[x].push_back(y);
	}
	root=build(1,c);
	rep(i,1,r) {
		for(auto y:t[i]) mdf(y,i);
		ans+=c*(c+1)/2*i-qry();
	}
	ans=c*(c+1)/2*r*(r+1)/2-ans;
	printf("%lld\n",ans);
	return 0;
}

一些应用

ZJOI2006 书架

考虑给每一个书定一个动态的权值。一开始这个权值是自己是从上到下第几个。维护一个 l 和一个 r,代表目前最小和最大权值。对于每一次 top 操作,就把书拎上来,权值变成 l-1;对于 bottom 操作,相反,权值变成 r+1。我们是很容易维护一个书编号和权值的对应的,所以可以用 treap 直接做。

#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
typedef pair<int,int> pii;
typedef vector<int> vi;

long long read() {
	long long res=0, w=1; char c=getchar();
	while(!isdigit(c)) {if(c=='-') w=-1; c=getchar();}
	while(isdigit(c)) {res=res*10+c-48, c=getchar();}
	return res*w;
}

long long read(char *s) {
	int len=0; char c=getchar();
	while(!isalpha(c)) c=getchar();
	while(isalpha(c)) {s[++len]=c; c=getchar();}
	return len;
}

const int N=1e5+9;
int n,m,id[N];
map<int,int>rid;
char op[N];

int l,r,sz[N],ls[N],rs[N],v[N],w[N],tot,root;
int newn(int x) {return v[++tot]=x,w[tot]=rand(),tot;}
void upd(int x) {sz[x]=sz[ls[x]]+sz[rs[x]]+1;}
int merge(int x,int y) { //tree(x)<tree(y)
	if((!x)||(!y)) return x+y;
	if(w[x]>w[y]) return rs[x]=merge(rs[x],y), upd(x), x;
	else return ls[y]=merge(x,ls[y]), upd(y), y;
}
void split(int x,int val,int &l,int &r) {
	if(!x) {l=r=0; return;}
	else if(v[x]<=val) l=x, split(rs[x],val,rs[x],r);
	else r=x, split(ls[x],val,l,ls[x]);
	upd(x);
}
void ins(int val) {
	int x,y; split(root,val-1,x,y);
	root=merge(merge(x,newn(val)),y);
}
void mdf(int val,int nv) {
	int x,y,z,w; split(root,val-1,x,y), split(y,val,z,w);
	root=merge(x,merge(merge(ls[z],rs[z]),w));
	v[z]=nv;
	split(root,nv-1,x,y);
	root=merge(merge(x,z),y);
}
int cnts(int val) {
	int x,y,res; split(root,val-1,x,y);
	res=sz[x]; merge(x,y); return res;
}
int fmax(int x) {return rs[x]?fmax(rs[x]):v[x];}
int fmin(int x) {return ls[x]?fmin(ls[x]):v[x];}
int prec(int val) {
	int x,y,res; split(root,val-1,x,y);
	res=fmax(x); merge(x,y); return res;
}
int succ(int val) {
	int x,y,res; split(root,val,x,y);
	res=fmin(y); merge(x,y); return res;
}
int kth(int k,int x=root) {
	while(k) {
		if(k<=sz[ls[x]]) x=ls[x];
		else if(k==sz[ls[x]]+1) return v[x];
		else k-=sz[ls[x]]+1, x=rs[x];
	} return assert(0), 0;
}

void top(int s) {
	mdf(id[s],--l); id[s]=l, rid[l]=s;
}
void bottom(int s) {
	mdf(id[s],++r); id[s]=r, rid[r]=s;
}
void insert(int s1,int t) {
	if(t==0) return;
	else if(t==-1) {
		int p2=prec(id[s1]); int s2=rid[p2],p1=id[s1];
		swap(id[s1],id[s2]), swap(rid[p1],rid[p2]);
	} else {
		int p2=succ(id[s1]); int s2=rid[p2],p1=id[s1];
		swap(id[s1],id[s2]), swap(rid[p1],rid[p2]);
	}
}
int ask(int s) {return cnts(id[s]);}
int query(int s) {return rid[kth(s)];}

int main() {
	n=read(), m=read(); l=1,r=n;
	rep(i,1,n) ins(i);
	rep(i,1,n) {
		int x=read();
		id[x]=i, rid[i]=x;
	}
	rep(i,1,m) {
		int tmp=read(op), s=read();
		if(op[1]=='T') top(s);
		else if(op[1]=='B') bottom(s);
		else if(op[1]=='I') insert(s,read());
		else if(op[1]=='A') printf("%d\n",ask(s));
		else if(op[1]=='Q') printf("%d\n",query(s));
		else throw 19260817;
	}
	return 0;
}
posted @ 2021-09-01 21:17  LarsWerner  阅读(59)  评论(0编辑  收藏  举报