废话少说,普通平衡树用 pbds 写写就好了,没必要学,不如直接来学 FHQ_Treap。

文艺平衡树把一个序列放在了二叉树上,满足二叉树的中序遍历是这个序列。这里我们使用 FHQ_Treap 来维护这棵二叉树。

以下的序列在文艺平衡树中都表现为一棵二叉树的中序遍历。
在文艺平衡树中,FHQ_Treap 需要具有以下必需功能

  • split:把一个长度为 $n$ 的序列从中间某点断开成两个序列,其中第一个序列的长度为 $k$,第二个序列的长度为 $n-k$。
  • merge:把两个长度为 $x,y$ 的序列实现拼接功能(字符串加法),变成一个长度为 $x+y$ 的序列。

因此每个节点应记录以下信息:

  • $ls,rs$ 表示该节点的左右儿子
  • $siz$ 表示子树大小
  • $pri$ 是每个节点随机分配的优先级,其用途在 merge 中会提到
  • $val$ 表示为该节点的值
struct node{
	node *ls,*rs;
	int val,pri,siz,tag;
	node(int k=0){
		ls=rs=nullptr;
		val=k;pri=int(rng());siz=1;tag=0;
	}
	node *upd(){
		siz=1+(ls?ls->siz:0)+(rs?rs->siz:0);
		return this;
	}
}

在 split 时,如果发现 $siz_{ls}<k$,说明整个 $ls$ 部分包括当前节点均为第一个序列,我们需要对 $rs$ 继续进行 split,并且第一个序列的根就是 $now$,其左儿子就是 $ls$,而右儿子就是对 $rs$ 继续进行 split 的结果。
否则,就说明整个 $rs$ 部分包括当前节点均为第二个序列,我们需要对 $ls$ 继续进行 split,并且第二个序列的根就是 $now$,其右儿子就是 $ls$,而左儿子就是对 $ls$ 继续进行 split 的结果。

void split(node *now,int k,node *&x,node *&y){
	if(!now){x=y=nullptr;return;}
	int tmp=now->ls?now->ls->siz:0;
	if(tmp<k)x=now,split(now->rs,k-tmp-1,now->rs,y);
	else y=now,split(now->ls,k,x,now->ls);
	now->upd();
}

而对于序列 $x,y$ merge,我们发现有两种合并方法:

  • 把 $x$ 的 $rs$ 和 $y$ 进行 merge,递归处理
  • 把 $x$ 和 $y$ 的 $ls$ 进行 merge,递归处理

为了使树高在 $\log$ 的数量级,我们得选出一种 merge 方法。
可以证明,给每个节点分配一个随机优先级,按照优先级的大小选择 merge 的方法就可以使树高在 $\log$ 的数量级。

node *merge(node *x,node *y){
	if(!x)return y;
	if(!y)return x;
	if(x->pri<y->pri)return x->rs=merge(x->rs,y),x->upd();
	else return y->ls=merge(x,y->ls),y->upd();
}

在序列下标 $k$ 处增加一个元素 $v$,只需先把序列分成长度 $k-1$ 和 $n-(k-1)$ 两部分,然后在中间放个 $v$ 拼起来即可。

void insert(int k,int v){
	node *x=nullptr,*y=nullptr;
	split(rt,k-1,x,y);
	rt=merge(merge(x,new node(v)),y);
}

删除序列下标为 $k$ 的元素,只需先把序列分成长度 $k-1,1,n-k$ 三部分,然后只合并第一、三部分即可。

void erase(int k){
	node *x=nullptr,*y=nullptr,*z=nullptr;
	split(rt,k,x,z);
	split(x,k-1,x,y);
	// y=merge(y->ls,y->rs);
	// rt=merge(merge(x,y),z);
	rt=merge(x,z);
}

访问下标为 $k$ 的元素,只需在树上按照每个节点的 $siz$ 走就行了。

node *kth(node *now,int k){
	for(;;){
		now->pushdw();
		int tmp=now->ls?now->ls->siz:0;
		if(k<=tmp)now=now->ls;
		else if(k==tmp+1)return now;
		else k-=tmp+1,now=now->rs;
	}
}
int operator[](int k){
	return kth(rt,k)->val;
}

上面是基本操作,即 $\mathcal O(\log n)$ 时间复杂度的插入、删除、随机访问。文艺平衡树还有一个经典操作是区间翻转。
我们发现,如果我们要把区间 $[l,r]$ 翻转,只需先 split 出这一段序列,然后将这棵二叉树上每个节点的左右儿子互换就实现了区间翻转。
可是这样子复杂度不对,但是可以打 lazy tag,由于所有操作都是基于 split 和 merge 的,所以只需在 split 和 merge 时下传标记。
不过 kth 没有用到 split 和 merge,所以 kth 时也要下传标记。

struct treap{
	struct node{
		node *ls,*rs;
		int val,pri,siz,tag;
		node(int k=0){
			ls=rs=nullptr;
			val=k;pri=int(rng());siz=1;tag=0;
		}
		node *upd(){
			siz=1+(ls?ls->siz:0)+(rs?rs->siz:0);
			return this;
		}
		node *pushdw(){
			if(!tag)return this;
			if(ls)ls->tag^=1,swap(ls->ls,ls->rs);
			if(rs)rs->tag^=1,swap(rs->ls,rs->rs);
			tag=0;
			return this;
		}
	}*rt=nullptr;
	node *merge(node *x,node *y){
		if(!x)return y;
		if(!y)return x;
		x->pushdw();y->pushdw();
		if(x->pri<y->pri)return x->rs=merge(x->rs,y),x->upd();
		else return y->ls=merge(x,y->ls),y->upd();
	}
	void split(node *now,int k,node *&x,node *&y){
		if(!now){x=y=nullptr;return;}
		now->pushdw();
		int tmp=now->ls?now->ls->siz:0;
		if(tmp<k)x=now,split(now->rs,k-tmp-1,now->rs,y);
		else y=now,split(now->ls,k,x,now->ls);
		now->upd();
	}
	node *kth(node *now,int k){
		for(;;){
			now->pushdw();
			int tmp=now->ls?now->ls->siz:0;
			if(k<=tmp)now=now->ls;
			else if(k==tmp+1)return now;
			else k-=tmp+1,now=now->rs;
		}
	}
	int operator[](int k){
		return kth(rt,k)->val;
	}
	void insert(int k,int v){
		node *x=nullptr,*y=nullptr;
		split(rt,k-1,x,y);
		rt=merge(merge(x,new node(v)),y);
	}
	void erase(int k){
		node *x=nullptr,*y=nullptr,*z=nullptr;
		split(rt,k,x,z);
		split(x,k-1,x,y);
		// y=merge(y->ls,y->rs);
		// rt=merge(merge(x,y),z);
		rt=merge(x,z);
	}
	void reverse(int l,int r){
		node *x=nullptr,*y=nullptr,*z=nullptr;
		split(rt,r,x,z);
		split(x,l-1,x,y);
		swap(y->ls,y->rs);
		y->tag^=1;
		rt=merge(merge(x,y),z);
	}
}