WJX博客

学习笔记—— *衡树

一、前言

总所周知,数论和树论是\(OI\)界的两大毒瘤,而今天,我们也将推开 (入土) *衡树的大门,感受其特有的魅力(duliu)。

二、*衡树的一些概念

1.二叉查找树(BST)

给定一颗二叉树,树上的每个节点带有一个数值,称为节点的“关键码”。

它有以下性质:

  • 该节点的关键码不小于它左子树中任何节点的关键码。

  • 该节点的关键码不大于它右子树中任意节点的关键码。

满足上述性质的二叉树就是一颗“二叉查找树(BST)”,且它的中序遍历是一个关键码非严格单调递增的节点序列。

BST的建立

一般来说,我们为了避免越界,减少边界情况的特殊判断,可以在\(BST\)中插入两个额外的关键码分别为\(INF\)\(-INF\)的节点,并用\(cnt\)记录和当前节点关键码相同的节点数。

上代码

int val[N],cnt[N];
//val[i]表示i节点的关键码
//cnt[i]表示i节点的副本数(与它相同关键码的节点数)
int ch[N][2];
//ch[i][0]表示i节点的左儿子的编号
//ch[i][1]表示i节点的右儿子的编号

il int NEW(int v){
	val[++tot]=v;//当前节点的关键码
	cnt[tot]=1;//因为是新节点,所以副本数为1
	return tot;//tot表示该节点的编号
}

il void build() {
	root=NEW(-INF);ch[root][1]=NEW(INF);
//先建两个节点,避免越界
//因为INF>-INF,所以INF是-INF的右儿子
}	

BST的检索

\(BST\)的性质,我们可以在\(O(logn)\)的复杂度内找到关键码为\(val\)的节点,过程如下(设\(id\)为当前节点的编号):

  • 1.若\(id\)的关键码等于\(val\),直接返回\(id\)

  • 2.若\(id\)的关键码大于\(val\)

    (1)若\(id\)的左儿子为空,则没有关键码为\(val\)的节点

    (2)若\(id\)的左儿子不为空,则在\(id\)的左子树中查找

  • 3.若\(id\)的关键码小于\(val\)

    (1)若\(id\)的右儿子为空,则没有关键码为\(val\)的节点

    (2)若\(id\)的右儿子不为空,则在\(id\)的右子树中查找

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

il int get_rank(int id,int v) {
	if(!id) return 0;//没找到
	if(v==val[id]) return id;//情况1
	else if(v<val[id]) return get_rank(ch[id][0],v);//情况2
	else return get_rank(ch[id][1],v);//情况3
}	

BST的插入

和检索一样,当我们搜索到\(id\)的子节点为空时,直接建立关键码为\(val\)的新节点作为\(id\)的子节点。当搜索到时,直接令\(id\)节点的副本数\(+1\)

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

il void insert(int &id,int v) {//注意,此时id为取地址符号,可以使id节点的父节点的ch[fa[id]][d]更新
	if(!id) {
		id=NEW(v);//建立一个新节点
		return ;
	}	
	if(v==val[id]) ++cnt[id];//直接加副本数
	else {
		int d=v<val[id]?0:1;//判断方向
		insert(ch[id][d],v);
	}	
}	

BST求前驱/后继

定义:

前驱:在\(BST\)中关键码小于\(val\)的前提下,关键码最大的节点。

后继:在\(BST\)中关键码大于\(val\)的前提下,关键码最小的节点。

过程:

之前和检索一样,对于找到该值后的情况,我们可以分类讨论

求前驱时,可以走到该节点的左节点,再一直往右走,就可以找到\(val\)的前驱。

求后继时,可以走到该节点的右节点,再一直往左走,就可以找到\(val\)的后继。

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

il int get_pre(int v) {
	int id=root,pre;
	while(id) {
		if(val[id]<v) pre=val[id],id=ch[id][1];
      //因为val[id]小于v,且val[id]一定不断变大,所以不用取max
		else id=ch[id][0];
      //当val[id]==v时,id会往左子树上走,然后一直向右,最后找到的pre就是v的前驱
      //当val[id]>v时,要一直往左走才能找到v的前驱
	}	
	return pre;
}	

il int get_next(int v) {
	int id=root,nxt;
	while(id) {
		if(val[id]>v) nxt=val[id],id=ch[id][0];
		else id=ch[id][1];
	}	
   //原理同上,就不再解释了
	return nxt;
}

BST的节点删除

先在\(BST\)中检索\(val\),得到其节点\(id\);

  • \(id\)节点的副本数大于\(1\)\(cnt[id]>1\)),直接删除其一个副本即可。

  • \(id\)节点的子树数小于\(2\),直接令其子节点代替\(id\)节点的位置。

  • \(id\)节点有两颗子树,则先求出\(val\)的后继节点\(nxt\),删除\(nxt\)并用\(nxt\)的右子树代替\(nxt\)的位置,再用\(nxt\)节点代替\(id\)节点位置并删除\(id\)节点即可。

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

il void Remove(int &id,int v) {//从以id为根的子树中删除值为v的节点
	if(!id) return ;//没有该节点,直接返回
	if(v==val[id]) {//检索到该节点
		if(cnt[id]>1) {--cnt[id];return ;}//副本数大于1,直接减副本数
		if(ch[id][0]==0) id=ch[id][1];//没有左儿子,直接用右子树代替id的位置
		else if(ch[id][1]==0) id=ch[id][0];//没有右儿子,直接用左子树代替id的位置
		else {
			int nxt=ch[id][1];
			while(ch[id][0]) nxt=ch[id][0];
        //因为已经检索到了该节点,所以只要从它的右子树一直往左走即可找到其后继
			Remove(ch[id][1],val[nxt]);//删除后继节点
			ch[nxt][0]=ch[id][0];
			ch[nxt][1]=ch[id][1];
			id=nxt;
        //用后继节点代替当前节点
		}	
	}	
	if(v<val[id]) Remove(ch[id][0],v);//往左子树上检索val
   else Remove(ch[id][1],v);//往右子树上检索val
}	

由上述操作可以看出,\(BST\)在随机数据中一次操作的期望时间复杂度为\(O(logn)\)。但如果其变成一条链,则一次操作的期望时间复杂度会退化为\(O(n)\)。我们追根溯源,可以发现这是由于其左右子树大小相差较大造成的不*衡,而维持其*衡的方法有很多,从而产生了各种*衡树。

三、几种*衡树 (终于讲到*衡树了)

一、有旋\(Treap\)

因为满足\(BST\)性质且中序遍历为相同序列的二叉查找树是不唯一的,但又都是等价的,所以我们可以在维持\(BST\)性质的基础上,改变二叉查找树的形态,使树上每个节点的左右子树大小达到*衡,从而使整颗树的深度维持在\(O(logn)\)级别。(不一定是完全二叉树,但尽量*衡)

改变形态并保持\(BST\)性质的方法就是“旋转”,包括左旋和右旋,如下图:

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

il void Rotate(int &id,int d) {//d=0时为左旋,1时为右旋
	int temp=ch[id][d^1];
	ch[id][d^1]=ch[temp][d];
	ch[temp][d]=id;
	id=temp;
}

理解参照上图,这里讲一下记忆技巧(\(from\) 天上一颗蛋 大佬)

在旋转操作中,每一个数据都是被记录后立马修改的,所以会形成“\(Z\)”型,即每一行的末尾变量就是下一行的开头变量

知道了维持*衡的操作,我们怎样判断该节点是否应该旋转呢?

由对\(BST\)的分析,我们知道,在随机数据的情况下\(BST\)差不多就是*衡的,所以我们可以给每一个节点额外增加一个随机权值,使整颗二叉查找树满足堆的性质。这样,这颗二叉查找树就同时满足了“\(Tree\)”和“Heap”的性质,合在一起就是我们的\(Treap\)算法了。

具体一点,就是当我们插入一个节点时,自底而上依次检查,对于不满足堆性质的节点进行旋转。删除一个节点时,找到需要删除的节点并把它向下旋转成叶子节点,最后直接删除(可避免节点信息更新、堆性质维护等复杂问题)。

\(Talk\) \(is\) \(cheap\)\(show\) \(you\) \(the\) \(code\)

const int N=1e5+10,INF=1e18;
int n,tot,root;
int val[N],dat[N],siz[N],cnt[N];
//val[i]表示i节点的关键码
//dat[i]表示随机出的i节点的优先度
//siz[i]表示以i节点为根节点的子树大小
//cnt[i]表示i节点的副本数
int ch[N][2];
//ch[i][0]表示i节点的左儿子的编号
//ch[i][1]表示i节点的右儿子的编号

il int NEW(int v){//建立新节点
	val[++tot]=v;
	dat[tot]=rand();//赋值随机优先度
	siz[tot]=1;
	cnt[tot]=1;
	return tot;//返回新节点的编号
}

il void pushup(int id) {//更新以id节点为根的子树的大小
	siz[id]=siz[ch[id][0]]+siz[ch[id][1]]+cnt[id];
}	

il void build() {//建树
	root=NEW(-INF);ch[root][1]=NEW(INF);/
   //初始时建立INF和-INF两个节点,防止越界
	pushup(root);
}	

il void Rotate(int &id,int d) {//旋转(包括左旋和右旋)
	//d=0表示左旋,1表示右旋,理解参照上图
	int temp=ch[id][d^1];
	ch[id][d^1]=ch[temp][d];
	ch[temp][d]=id;
	id=temp;
	pushup(ch[id][d]);pushup(id);
}	

il void insert(int &id,int v) {//插入节点
	if(!id) {//原先没有,就新建一个节点插入
		id=NEW(v);
		return ;
	}	
	if(v==val[id]) ++cnt[id];//原先有,就在该节点处新加一个副本
	else {
		int d=v<val[id]?0:1;//判断插入左子树还是右子树
		insert(ch[id][d],v);
		if(dat[id]<dat[ch[id][d]]) Rotate(id,d^1);//自底而上维护优先度呈堆的性质
	}	
	pushup(id);//更新
}	

il void Remove(int &id,int v) {//删除节点
	if(!id) return ;//没有该节点,不删除
	if(v==val[id]) {//找到该节点
		if(cnt[id]>1) {--cnt[id];pushup(id);return ;}//删副本
		if(ch[id][0]||ch[id][1]) {
			if(!ch[id][1]||dat[ch[id][0]]>dat[ch[id][1]]) {Rotate(id,1);Remove(ch[id][1],v);}//将该节点旋转到叶子节点
			else {Rotate(id,0);Remove(ch[id][0],v);}//将该节点旋转到叶子节点
			pushup(id);//更新
		}	
		else id=0;//没有子树,直接删
		return ;
	}	
	v<val[id]?Remove(ch[id][0],v):Remove(ch[id][1],v);//检索该节点的位置
	pushup(id);//更新
}	

il int get_rank(int id,int v) {
	if(!id) return 0;
	if(v==val[id]) return siz[ch[id][0]]+1;
	else if(v<val[id]) return get_rank(ch[id][0],v);
	else return siz[ch[id][0]]+cnt[id]+get_rank(ch[id][1],v);
}	

il int get_val(int id,int rank) {
	if(!id) return INF;//没找到
	if(rank<=siz[ch[id][0]]) get_val(ch[id][0],rank);//在左子树中找
	else if(rank<=siz[ch[id][0]]+cnt[id]) return val[id];//就是该节点的值
	else return get_val(ch[id][1],rank-siz[ch[id][0]]-cnt[id]);
   //在右子树中查找,其在右子树中的排名要减去左子树大小及当前节点副本数
}

il int get_pre(int v) {
	int id=root,pre;
	while(id) {
		if(val[id]<v) pre=val[id],id=ch[id][1];
      //因为val[id]小于v,且val[id]一定不断变大,所以不用取max
		else id=ch[id][0];
      //当val[id]==v时,id会往左子树上走,然后一直向右,最后找到的pre就是v的前驱
      //当val[id]>v时,要一直往左走才能找到v的前驱
	}	
	return pre;
}	

il int get_next(int v) {
	int id=root,nxt;
	while(id) {
		if(val[id]>v) nxt=val[id],id=ch[id][0];
		else id=ch[id][1];
	}	
   //解释同找前驱
	return nxt;
}

P3369 【模板】普通*衡树(板子题)(Treap题解)(我的Treap代码)

二、\(Splay\)(伸展树)

理解了\(Tresp\)的旋转操作,\(Splay\)的旋转操作也可以如法炮制,这里就不放图了,直接上代码

il void rotate(int id) {
	int father=fa[id],dir=get_dir(id);
	//father是当前节点的父节点
	//dir是当前节点属于其父节点的那个儿子
	int g_father=fa[father];
	//g_father是当前节点的祖父节点
	ch[father][dir]=ch[id][dir^1];
	fa[ch[father][dir]]=father;
	ch[id][dir^1]=father;
	fa[father]=id;
	fa[id]=g_father;
	if(g_father) {ch[g_father][ch[g_father][1]==father]=id;}//一定要注意!!!因为可能没有祖父节点,所以要先判断祖父节点的存在再连边	
	pushup(father);//更新
	pushup(id);//更新
}	

作为伸展树特有的方法,当然是其\(Splay\)操作了:将当前节点不断上旋直至根节点

分为两种情况:

1.该节点与其父节点和其父节点与其祖父节点不共线:直接旋转即可。

情况如下图:

2.该节点与其父节点和其父节点与其祖父节点共线:先旋转父亲节点,再旋转该节点。

情况如下图:

我们可以来对比一下一直旋转\(id\)节点的效果和先旋转父节点再旋转\(id\)节点的效果。

一直旋转\(id\)节点:

先旋转父节点再旋转\(id\)节点:

显然,先旋转父节点再旋转\(id\)节点会使树的深度减少一半,使树更加趋*于*衡。

最后再说点要注意的:每次进行有关点的操作时,都要进行一次\(Splay\),以保证树的随机性。

\(Thats\) \(all\)\(let's\) \(coding\)

const int N=1e5+10;
int n,whole_size,root;
int fa[N],cnt[N],val[N],ch[N][2],siz[N];

il void tree_clear(int id) {//清除该点
	ch[id][0]=ch[id][1]=fa[id]=siz[id]=cnt[id]=val[id]=0;
}	

il bool get_dir(int id) {//判断该点属于其父节点的那个儿子
	return ch[fa[id]][1]==id;
}	

il void pushup(int id) {//更新以id为根节点的子树的大小
	if(id) {
		siz[id]=cnt[id];
		if(ch[id][0]) siz[id]+=siz[ch[id][0]];
		if(ch[id][1]) siz[id]+=siz[ch[id][1]];
	}	
	return ;
}	

il void rotate(int id) {//旋转操作
	int father=fa[id],dir=get_dir(id);
	//father是当前节点的父节点
	//dir是当前节点属于其父节点的那个儿子
	int g_father=fa[father];
	//g_father是当前节点的祖父节点    
	ch[father][dir]=ch[id][dir^1];
	fa[ch[father][dir]]=father;
	ch[id][dir^1]=father;
	fa[father]=id;
	fa[id]=g_father;
	if(g_father) {ch[g_father][ch[g_father][1]==father]=id;}
	//一定要注意!!!因为可能没有祖父节点,所以要先判断祖父节点的存在再连边(差错差了一上午,血淋淋的教训)
	pushup(father);
	pushup(id);
}	

il void splay(int id) {
	for(re int father;father=fa[id];rotate(id)) 
   //因为一次可以判断到id的祖父节点,且不管共不共线最后都要旋转id
   //所以将最后一次旋转放到循环结束之时
		if(fa[father]) 
			rotate(get_dir(id)==get_dir(father)?father:id);
      //判断是否共线
	root=id;//因为是直接旋转到根节点,所以更新根节点的编号为id
}	

il void insert(int v) {//插入节点
	if(!root) {//树中没有节点,建一个新点作为根节点
		++whole_size;
		root=whole_size;
		ch[whole_size][0]=ch[whole_size][1]=fa[whole_size]=0;
		siz[whole_size]=cnt[whole_size]=1;
		val[whole_size]=v;
		return ;
	}	
	int now=root,father=0;
	while(1) {
		if(v==val[now]) {//找到该节点
			++cnt[now];//增加其副本数
			pushup(now);pushup(father);
			splay(now);//把该节点旋转到根
			break;
		}	
		father=now;now=ch[now][val[now]<v];//判断往哪颗子树走
		if(!now) {//树中没有该节点,新建一个
			++whole_size;
			ch[whole_size][0]=ch[whole_size][1]=0;
			fa[whole_size]=father;
			siz[whole_size]=cnt[whole_size]=1;
			ch[father][val[father]<v]=whole_size;
			val[whole_size]=v;
			pushup(father);
			splay(whole_size);
			break;
		}	
	}	
}	

il int find_rank(int v) {//根据节点的值找排名
	int now=root,ans=0;
	while(1) {
		if(v<val[now]) now=ch[now][0];//往左子树走
		else {
			ans+=(ch[now][0]?siz[ch[now][0]]:0);//有左子树的话就加上左子树的大小
			if(v==val[now]) {splay(now);return ans+1;}//如果是当前节点,就返回ans+1(这个1是因为该节点本身排一名)
			ans+=cnt[now];//如果在右子树中,排名还要加上该节点的副本数
			now=ch[now][1];//往右子树上走
		}	
	}
}	

il int find_val(int rank) {//根据节点排名找对应的值
	int now=root;
	while(1) {
		if(ch[now][0]&&rank<=siz[ch[now][0]]) now=ch[now][0];
		else {
			int temp=(ch[now][0]?siz[ch[now][0]]:0)+cnt[now];
			if(rank<=temp) return val[now];//因为该排名已经大于左子树的大小,又小于等于左子树和该节点副本数之和,所以答案就是当前节点的值
			rank-=temp;//计算出其在右子树中的排名
			now=ch[now][1];
		}	
	}	
}	

//注:使用时应先插入val,再运行此函数,最后删去val
//因为插入时保证值为val的节点处在根节点,所以直接从根节点的左儿子一直往右走就是val的前驱
il int find_pre() {//找前驱
	int now=ch[root][0];
	while(ch[now][1]) now=ch[now][1];
	return now;
}

//注:使用时应先插入val,再运行此函数,最后删去val
//因为插入时保证值为val的节点处在根节点,所以直接从根节点的右儿子一直往左走就是val的后继
il int find_nxt() {//求后继
	int now=ch[root][1];
	while(ch[now][0]) now=ch[now][0];
	return now;
}	

il void tree_delete(int v) {//删除值为val的节点
	find_rank(v);//把该节点旋转到根节点
	if(cnt[root]>1) {--cnt[root];pushup(root);return ;}//副本数大于1,直接减副本数
	if(!ch[root][0]&&!ch[root][1]) {tree_clear(root);root=0;return ;}//是叶子节点,直接删
	if(!ch[root][0]) {//没有左子树,用右子树直接代替该节点
		int old_root=root;
		root=ch[root][1];
		fa[root]=0;
		tree_clear(old_root);	
		return ;
	}
	if(!ch[root][1]) {//没有右子树,用左子树直接代替该节点
		int old_root=root;
		root=ch[root][0];
		fa[root]=0;
		tree_clear(old_root);
		return ;
	}	
	int left_max=find_pre(),old_root=root;
	splay(left_max);//找该节点的前驱并把前驱旋转到根节点
   //因为旋转完之后该节点一定没有左儿子(该节点是它前驱的后继),直接用该节点的右儿子代替该节点
	ch[root][1]=ch[old_root][1];
	fa[ch[old_root][1]]=root;
	tree_clear(old_root);
	pushup(root);
}	

P3369 【模板】普通*衡树(板子题)(Splay题解)(我的Splay代码)

三、\(FHQ\) \(Treap\)

这是一种非旋转的\(Treap\),只需要通过分裂和合并两个简单的操作,就可以达到和其他*衡树一样的效果 (最重要的是终于不用调几百行的旋转函数了) ,既然它这么方便,那就赶紧学一下:

1.\(split\)

和它的名字一样,就是把一个\(Treap\)分成两个。

它有两种分法:按权值分裂和按排名分裂

权值版:

il void split(int id,int v,int &x,int &y) {
	//注意这里的取地址符号,返回它分裂的两颗树的根节点
   //id表示这棵树的根节点
   //v表示按权值分裂
   //x表示这颗树的左树,y表示这棵树的右树
	if(!id) x=y=0;//这棵树没有节点,分完了
	else {
		if(val[id]<=v) x=id,split(ch[id][1],v,ch[id][1],y);
      //该节点及其左子树都小于等于权值,全部分给左树
		else y=id,split(ch[id][0],v,x,ch[id][0]);
      //全部分给右树
		pushup(id);
      //更新节点大小
	}	
}

大小版:

il void split(int id,int rank,int &x,int &y) {
	//注意这里的取地址符号,返回它分裂的两颗树的根节点
   //id表示这棵树的根节点
   //rank表示按排名分裂
   //x表示这颗树的左树,y表示这棵树的右树
	if(!id) x=y=0;//这棵树没有节点,分完了
	else {
		if(rank<=siz[ch[id][0]]) y=id,split(ch[id][0],rank,x,ch[id][0]);
      //该节点及其右子树都大于等于排名,全部分给右树
		else x=id,split(ch[id][1],rank-siz[ch[id][0]]-1,ch[id][1],y);
       //全部分给左树
		pushup(id);
      //更新节点大小
	}	
}

2.\(merge\)

就是将两颗树合并,并返回合并后的根节点。

il int merge(int x,int y) {
	if(!x||!y) return x+y;//有一颗子树为空的话,另一颗子树就是它的根节点
	if(dat[x]<dat[y]) {//判断附加权值的大小,维护堆的性质
		ch[x][1]=merge(ch[x][1],y);//因为x的附加权值更小,合并它的右儿子和y
		pushup(x);//更新
		return x;
	}	
	else {
		ch[y][0]=merge(x,ch[y][0]);////因为y的附加权值更小,合并它的左儿子和x
		pushup(y);//更新
		return y;
	}	
}	

3.\(insert\)

把这棵树按权值分裂,再插入该权值即可。

split(root,v,x,y);
root=merge(merge(x,NEW(v)),y);

4.\(delete\)

把这棵树按权值\(v\)分为\(x\)\(z\)两棵树,再按权值\(v-1\)分为\(x\)\(y\)两棵树。

split(root,v,x,z);
split(x,v-1,x,y);
y=merge(ch[y][0],ch[y][1]);
root=merge(merge(x,y),z);

5.\(pre\)

找前驱时,把这棵树按权值\(v-1\)分裂,在左树中找最大值即可。

6.\(nxt\)

找后继时,把这棵树按权值\(v\)分裂,在右树中找最小值即可。

7.\(find\) \(rank\)

找排名时,把这棵树按权值\(v\)分裂,排名就是左树大小加\(1\)

\(show\) \(you\) \(the\) \(whole\) \(code\)

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstdlib>
#define il inline
#define ll long long
#define int long long
#define re register
#define gc getchar
using namespace std;
//------------------------初始程序-------------------------- 
il int read(){
	re int x=0;re bool f=0;re char ch=gc();
	while(!isdigit(ch)){f|=ch=='-';ch=gc();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
	return f?-x:x;
}

il int max(int a,int b){
	return a>b?a:b;
}

il int min(int a,int b){
	return a<b?a:b;
}


//------------------------初始程序-------------------------- 

const int N=1e5+10;
int n,whole_size,root;
int val[N],dat[N],ch[N][2],siz[N];

il void pushup(int id) {//更新
	if(id) {
		siz[id]=1;
		if(ch[id][0]) siz[id]+=siz[ch[id][0]];
		if(ch[id][1]) siz[id]+=siz[ch[id][1]];
	}	
}	

il int NEW(int v) {//新建节点
	int id=++whole_size;
	val[id]=v;
	dat[id]=rand();
	siz[id]=1;
	return id;
}	

il void split(int id,int v,int &x,int &y) {
	//注意这里的取地址符号,返回它分裂的两颗树的根节点
   //id表示这棵树的根节点
   //v表示按权值分裂
   //x表示这颗树的左树,y表示这棵树的右树
	if(!id) x=y=0;//这棵树没有节点,分完了
	else {
		if(val[id]<=v) x=id,split(ch[id][1],v,ch[id][1],y);
      //该节点及其左子树都小于等于权值,全部分给左树
		else y=id,split(ch[id][0],v,x,ch[id][0]);
      //全部分给右树
		pushup(id);
      //更新节点大小
	}	
}

il int merge(int x,int y) {
	if(!x||!y) return x+y;//有一颗子树为空的话,另一颗子树就是它的根节点
	if(dat[x]<dat[y]) {//判断附加权值的大小,维护堆的性质
		ch[x][1]=merge(ch[x][1],y);//因为x的附加权值更小,合并它的右儿子和y
		pushup(x);//更新
		return x;
	}	
	else {
		ch[y][0]=merge(x,ch[y][0]);////因为y的附加权值更小,合并它的左儿子和x
		pushup(y);//更新
		return y;
	}	
}	

il int find_id(int now,int rank) {//now是当前节点,根据排名找节点
	while(1) {
		if(rank<=siz[ch[now][0]]) now=ch[now][0];
		else if(rank==siz[ch[now][0]]+1) return now;
		else rank-=siz[ch[now][0]]+1,now=ch[now][1];
	}	
}	

signed main()
{
	int x=0,y=0,z=0;
	n=read();
	while(n--) {
		int opt=read(),a=read();
		if(opt==1) {//插入节点
			split(root,a,x,y);
			root=merge(merge(x,NEW(a)),y);
		}	
		if(opt==2) {//删除节点
			split(root,a,x,z);
			split(x,a-1,x,y);
			y=merge(ch[y][0],ch[y][1]);
			root=merge(merge(x,y),z);
		}
		if(opt==3) {//查询排名
			split(root,a-1,x,y);
			printf("%lld\n",siz[x]+1);
			root=merge(x,y);
		}	
		if(opt==4) printf("%lld\n",val[find_id(root,a)]);//按排名查询值
		if(opt==5) {//找前驱
			split(root,a-1,x,y);
			printf("%lld\n",val[find_id(x,siz[x])]);
			root=merge(x,y);
		}
		if(opt==6) {//找后继
			split(root,a,x,y);
			printf("%lld\n",val[find_id(y,1)]);
			root=merge(x,y);
		}	
	}	
	return 0;
}

例题:

P2234 [HNOI2002]营业额统计(求前驱后继)(Treap题解)(我的Treap代码)

P3391 【模板】文艺*衡树(区间翻转)(Splay题解)(我的Splay代码)

P2042 [NOI2005] 维护数列(许多区间操作的*衡树)(Splay题解)(我的Splay代码)

P2286 [HNOI2004]宠物收养场(循环使用同一颗Splay)(Splay题解)(我的Splay代码)

P1486 [NOI2004] 郁闷的出纳员(Splay题解)(我的Splay代码)

本题区间操作都是对于整体进行,所以可以用一个变量记录而不用加tag标记,且由于要删除的节点都小于“最低工资\(-\)工资总变化量”,可以把它们移动到同一颗子树上再进行区间操作。

P3850 [TJOI2007]书架(按排名分裂)(FHQTreap题解)(我的FHQTreap代码)

posted @ 2021-08-05 16:02  WJX3078  阅读(43)  评论(0)    收藏  举报