学习笔记——平衡树


概述

平衡树是对二叉搜索树的进阶,常规的二叉搜索树在插入一定量的单调性数据后,叶子节点的深度将会不平衡,会出现树退化成链的情况。平衡树通过一系列调整,维持二叉搜索树 \(O(\log n)\) 的复杂度。

\(\text{Splay}\)

介绍

\(\text{Splay}\) 又名伸展树,其操作原理可以感性理解成:对于随机数据,无论怎样的操作都是可以维持平衡的;而对具有单调性的数据,每次操作结束都会将当前操作的点旋转至树根,于是下次查找就会减少冗余的搜索。

模板

首先 \(\text{Splay}\) 的要维护如下几个信息。

rt tot fa[x] ch[x][0/1] val[x] cnt[x] siz[x]
当前根节点编号 新节点编号 父亲节点的编号 两个儿子节点的编号 节点权值 该权值出现的次数 子树大小

常用函数

1. \(\operatorname{maintain}(x)\)

更新节点 \(x\) 的子树大小,不难发现这个值为 \(x\) 左右子树大小和加上 \(x\) 所代表的权值出现次数。

inline void maintain(int x){
    siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];
}

2. \(\operatorname{pdson}(x)\)

判断 \(x\) 是其父亲节点的左儿子还是右儿子。

inline bool pdson(int x){
    return x==ch[fa[x]][1];
}

旋转功能函数

1. \(\operatorname{rotate}(x)\)

在此之前,我们先要了解一下旋转的方式。


准确的说,左旋和右旋都是在以旋转的点 \(x\) 的父亲 \(y\) 为根的子树内进行的,显然根据二叉搜索树的性质,子树内无论怎样变化对子树外都没有影响。

我们那左旋来举例,令旋转的节点 \(x\),父亲节点 \(y\),两个子节点 \(ch1,ch2\),对于权值进行排序得到:\(ch1< x< ch2< y\),我们在左旋之后,\(ch1\) 依旧作为 \(x\) 的左儿子,而 \(y\) 作为 \(x\) 的右儿子,此时的 \(y\) 左儿子已空,而 \(ch2\) 没有父亲,于是连接到 \(y\) 的左儿子,这样的依旧保持 \(ch1 的性质,右旋同理。

接着发现,其实左旋和右旋的难点在于将夹在 \(x\)\(y\) 之间的儿子从 \(x\) 转移至 \(y\)

我们来考虑如何进行一次旋转,设旋转的点 \(x\),其父亲 \(y\) 其父亲的父亲 \(z\),旋转应当分为三步。

  • \(x\) 作为 \(z\) 的子节点(更新 \(x\)\(fa\)\(z\)\(ch\)),此时 \(x\) 更新的身份与 \(y\) 之前的身份相同,于是需要刚刚的 \(\operatorname{pdson}()\) 函数
  • \(y\) 作为 \(x\) 的子节点(更新 \(y\)\(fa\)\(x\)\(ch\)),此时 \(y\) 更新的身份与 \(x\) 之前的身份是相反的(例如 \(x\) 先前为 \(y\) 的左儿子,\(y\) 现在为 \(x\) 的右儿子)
  • 将夹在 \(x\)\(y\) 中间的儿子节点与 \(y\) 相连(此儿子应与 \(x\) 的儿子身份不同,例如 \(x\)\(y\) 的左儿子,现在夹在中间的就是 \(x\) 的右儿子),此时该节点更新的的身份与 \(x\) 之前的身份相同。
inline void rotate(int x){
    int y=fa[x],z=fa[y],chk=pdson(x);
    //x->z
    ch[z][pdson(y)]=x,fa[x]=z;
    //y->x
    ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
    //ch->y
    ch[x][chk^1]=y,fa[y]=x;
    maintain(y),maintain(x);
}

2. \(\operatorname{splay}(x,goal)\)

不断旋转至一点的操作,同样也分 \(3\) 大种情况讨论。

  • \(x\) 的父亲的父亲 \(z\) 就是旋转的终点,直接左旋或右旋
  • \(x\) 的父亲 \(y\)\(x\) 的儿子身份相同,即同左或同右,先旋转 \(y\) 再旋转 \(x\),且旋转方向一致。
  • \(x\) 的父亲 \(y\)\(x\) 的儿子身份不同,即一左一右或一右一左,两次旋转 \(x\),且方向不一致。

(细化的话实际分 \(6\) 种小情况,自行画图很好理解)

这样不断旋转直至情况 \(1\),再旋转一次,就到达目标位置了。

inline void splay(int x,int goal=0){
    while(fa[x]!=goal){
        int y=fa[x],z=fa[y];
        if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
        rotate(x); 
    }
    if(!goal) rt=x;
}

更新和查询函数

1. \(\operatorname{insert}(k)\)

插入一个值为 \(k\) 的数。

既然是二叉搜索树,不断地向下搜索找到最接近的节点,若权值一样,直接将权值出现次数 \(+1\);反之则新开一个节点(新开时保证是叶子节点)。当然也要判断此时整棵树为空的情况。

inline void insert(int k){
    int x=rt,f=0;
    while(x&&k!=val[x]) f=x,x=ch[x][k>val[x]];
    if(x) cnt[x]++;
    else{
        x=++tot;
        if(f) ch[f][k>val[f]]=x;
        fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
    }
    splay(x);
}

2.\(\operatorname{find}(k)\)

找到权值为 \(k\) 的节点,将其旋转到根。

和查询操作时很相似的,这个功能在后续查前驱后继时也会用到。

inline void find(int k){
      int x=rt;
      if(!x) return;
      while(ch[x][k>val[x]]&&k!=val[x]) x=ch[x][k>val[x]];
      splay(x);
}

3.\(\operatorname{pre_nxt}(k,0/1)\)

查询值 \(k\) 的前驱后继。

操作非常好理解,拿查询前驱举例,将权值为 \(k\) 或接近 \(k\) 的节点旋转至根节点后,在左子树不断向右查询至叶子节点即为前驱,后继同理。注意这里不要把 \(\operatorname{find}(k)\)\(\operatorname{splay}(x)\) 弄混,前者是对权值,后者是对节点编号。

inline int pre_nxt(int k,bool pd){
    find(k);
    int x=rt;
    if(val[x]>k&&pd) return x;
    if(val[x]

4.\(\operatorname{erase}(k)\)

删除一个值为 \(k\) 的数。

操作是将 \(k\) 的前驱后继旋转到相连后,使 \(k\) 的节点没有子树,若有多个直接减少出现次数,反之删点即可。

inline void erase(int k){
    int pre=pre_nxt(k,0),nxt=pre_nxt(k,1);
    splay(pre),splay(nxt,pre);
    int x=ch[nxt][0];
    if(cnt[x]>1){
        cnt[x]--;
        splay(x);
    }
    else ch[nxt][0]=0;
}

5.\(\operatorname{kth}(k)\)

查询排名为 \(k\) 的数。

无脑向下找,思路同权值线段树。

inline int kth(int k){
    int x=rt;
    if(siz[x]siz[ch[x][0]]+cnt[x]){
            k-=siz[ch[x][0]]+cnt[x];
            x=ch[x][1];
        }
        else if(k<=siz[ch[x][0]]) x=ch[x][0];
        else return val[x];
    }
}

例题

1.P3369 【模板】普通平衡树

操作与上述相同,要先插入一个极大值、极小值作为边界。

点击查看代码
int rt,tot;
struct Splay{
	int fa[maxn],ch[maxn][2],val[maxn],cnt[maxn],siz[maxn];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x); 
		}
		if(!goal) rt=x;
	}
	inline void insert(int k){
		int x=rt,f=0;
		while(x&&k!=val[x]) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(f) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		splay(x);
	}
	inline void find(int k){
		int x=rt;
		if(!x) return;
		while(ch[x][k>val[x]]&&k!=val[x]) x=ch[x][k>val[x]];
		splay(x);
	}
	inline int pre_nxt(int k,bool pd){
		find(k);
		int x=rt;
		if(val[x]>k&&pd) return x;
		if(val[x]1){
			cnt[x]--;
			splay(x);
		}
		else ch[nxt][0]=0;
	}
	inline int kth(int k){
		int x=rt;
		if(siz[x]siz[ch[x][0]]+cnt[x]){
				k-=siz[ch[x][0]]+cnt[x];
				x=ch[x][1];
			}
			else if(k<=siz[ch[x][0]]) x=ch[x][0];
			else return val[x];
		}
	}
}tree;
int n;
int main(){
	n=read();
	tree.insert(maxxn),tree.insert(minxn);
	for(int i=1;i<=n;i++){
		int opt=read(),x=read();
		if(opt==1) tree.insert(x);
		if(opt==2) tree.erase(x);
		if(opt==3){
			tree.find(x);
			printf("%d\n",tree.siz[tree.ch[rt][0]]);
		}
		if(opt==4) printf("%d\n",tree.kth(x+1));
		if(opt==5) printf("%d\n",tree.val[tree.pre_nxt(x,0)]);
		if(opt==6) printf("%d\n",tree.val[tree.pre_nxt(x,1)]);
	}
	return 0;
}

2.P6136 【模板】普通平衡树(数据加强版)

稍微有修改,除了强制在线,不保证查询数据一定存在,所有查排名时要先插入再删除。

点击查看代码
int rt,tot;
struct Splay{
	int fa[maxn],ch[maxn][2],val[maxn],cnt[maxn],siz[maxn];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x); 
		}
		if(!goal) rt=x;
	}
	inline void insert(int k){
		int x=rt,f=0;
		while(x&&k!=val[x]) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(f) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		splay(x);
	}
	inline void find(int k){
		int x=rt;
		if(!x) return;
		while(ch[x][k>val[x]]&&k!=val[x]) x=ch[x][k>val[x]];
		splay(x);
	}
	inline int pre_nxt(int k,bool tp){
		find(k);
		int x=rt;
		if(val[x]>k&&tp) return x;
		if(val[x]1){
			cnt[x]--;
			splay(x);
		}
		else ch[nxt][0]=0;
	}
	inline int kth(int k){
		int x=rt;
		if(siz[x]siz[ch[x][0]]+cnt[x]){
				k-=siz[ch[x][0]]+cnt[x];
				x=ch[x][1];
			}
			else if(k<=siz[ch[x][0]]) x=ch[x][0];
			else return val[x];
		}
	}
}tree;
int n,m;
int a[maxn],ans;
int main(){
	n=read(),m=read();
	tree.insert(maxxn),tree.insert(minxn);
	for(int i=1;i<=n;i++){
		a[i]=read();
		tree.insert(a[i]);
	}
	int last=0;
	for(int i=1;i<=m;i++){
		int opt=read(),x=read()^last;
		if(opt==1) tree.insert(x);
		if(opt==2) tree.erase(x);
		if(opt==3){
			tree.insert(x);
			tree.find(x);
			last=tree.siz[tree.ch[rt][0]];
			tree.erase(x);
			ans^=last;
		}
		if(opt==4){
			last=tree.kth(x+1);
			ans^=last;
		}
		if(opt==5){
			last=tree.val[tree.pre_nxt(x,0)];
			ans^=last;
		}
		if(opt==6){
			last=tree.val[tree.pre_nxt(x,1)];
			ans^=last;
		}
	}
	printf("%d\n",ans);
	return 0;
}

3、P2234 HNOI2002 营业额统计

简单的查询一下前驱后继,注意这里可以是与其权值相同的,然后再插入新的权值。

点击查看代码
int rt,tot;
struct Splay{
	int fa[40005],ch[40005][2],val[40005],cnt[40005],siz[40005];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x);
		}
		if(!goal) rt=x;
	}
	inline void insert(int k){
		int x=rt,f=0;
		while(x&&val[x]!=k) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(f) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		splay(x);
	}
	inline void find(int k){
		int x=rt;
		if(!rt) return;
		while(ch[x][k>val[x]]&&val[x]!=k) x=ch[x][k>val[x]];
		splay(x);
	}
	inline int pre_nxt(int k,bool pd){
		find(k);
		int x=rt;
		if(val[x]==k) return x;
		if(val[x]>k&&pd) return x;
		if(val[x]

4、P2286 HNOI2004 宠物收养场

看样子要维护两棵平衡树——关于宠物和关于主人的,然而考虑到待领养的只有主人或宠物一种,给当前的平衡树一个状态,判断平衡树是否为空并修改状态即可。

注意这里有删除操作,于是查询前驱后继时,要考虑是否计算与查询权值相等的。

点击查看代码
int rt,tot;
struct Splay{
	ll fa[80005],ch[80005][2],val[80005],cnt[80005],siz[80005];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x); 
		}
		if(!goal) rt=x;
	}
	inline void insert(ll k){
		int x=rt,f=0;
		while(x&&k!=val[x]) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(f) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		splay(x);
	}
	inline void find(ll k){
		int x=rt;
		if(!x) return;
		while(ch[x][k>val[x]]&&k!=val[x]) x=ch[x][k>val[x]];
		splay(x);
	}
	inline int pre_nxt(int k,bool pd,bool pd2=0){
		find(k);
		int x=rt;
		if(val[x]==k&&pd2) return x;
		if(val[x]>k&&pd) return x;
		if(val[x]1){
			cnt[x]--;
			splay(x);
		}
		else ch[nxt][0]=0;
	}
}tree;
int n;
ll ans;
int tp,cnt;
int main(){
	n=read();
	tree.insert(maxxn),tree.insert(minxn);
	for(int i=1;i<=n;i++){
		int opt=read();
		ll x=read();
		if(cnt==0){
			tree.insert(x);
			tp=opt;
			cnt++;
		}
		else if(tp==opt){
			tree.insert(x);
			cnt++;
		}
		else{
			int pos1=tree.pre_nxt(x,0,1),pos2=tree.pre_nxt(x,1,1);
			ll num1=tree.val[pos1],num2=tree.val[pos2];
			if(llabs(num1-x)<=llabs(num2-x)){
				ans=(ans+llabs(num1-x))%mod;
				tree.erase(num1);	
			}
			else{
				ans=(ans+llabs(num2-x))%mod;
				tree.erase(num2);
			}
			cnt--;
		}	
	}
	printf("%lld\n",ans);
	return 0;
}

5.P1486 NOI2004 郁闷的出纳员

与板子不同的地方:查询第 \(k\) 大,删除操作是小于 \(\min\) 的全部数据。

第一个不同直接简单修改一下向下搜索的判断即可,第二个操作考虑找到边界的后继,把剩余部分直接断边即可。

注意:插入时不能插入小于 \(\min\) 的数据。

点击查看代码
int rt,tot;
int n,lim,ans;
struct Splay{
	int fa[maxn],ch[maxn][2],val[maxn],cnt[maxn],siz[maxn];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x);
		}
		if(!goal) rt=x; 
	}
	inline void insert(int k){
		int x=rt,f=0;
		while(x&&val[x]!=k) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(fa) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		splay(x);
	}
	inline void find(int k){
		int x=rt;
		if(!rt) return;
		while(ch[x][k>val[x]]&&val[x]!=k) x=ch[x][k>val[x]];
		splay(x);
	}
	inline int pre_nxt(int k,bool pd,bool pd2=0){
		find(k);
		int x=rt;
		if(val[x]==k&&pd2) return x;
		if(val[x]>k&&pd) return x;
		if(val[x]siz[ch[x][1]]+cnt[x]){
				k-=siz[ch[x][1]]+cnt[x];
				x=ch[x][0];
			}
			else if(k<=siz[ch[x][1]]) x=ch[x][1];
			else return val[x];
		}
	}
	inline void add(int k){
		for(int i=1;i<=tot;i++) val[i]+=k;
	}
	inline void erase(int k){
		int x=pre_nxt(k+lim,1,1);
		splay(x);
		ans+=siz[ch[x][0]];
		ch[x][0]=0;
		maintain(x);
		add(-k);
	}
}tree;
int main(){
	n=read(),lim=read();
	tree.insert(maxxn);
	for(int i=1;i<=n;i++){
		char ch;
		cin>>ch;
		int x=read();
		if(ch=='I'&&x>=lim) tree.insert(x);
		if(ch=='A') tree.add(x);
		if(ch=='S') tree.erase(x);
		if(ch=='F') printf("%d\n",tree.kth(x+1));
	}
	printf("%d\n",ans);
	return 0;
}

6.P3391 【模板】文艺平衡树

区间翻转操作。

首先很明显的是,把区间作为子树的话,将每个节点的左右子树都交换,就能实现这个操作。于是可以打上翻转的懒标记,接着近似于删除一个点的操作一样,我们把区间 \([L,R]\) 独立出来的操作是:将区间前驱旋转至根,再将区间后继旋转至当前根(也就是区间前驱),按中序遍历输出即可。

点击查看代码
int n,m;
int rt,tot;
struct Splay{
	int fa[maxn],ch[maxn][2],val[maxn],siz[maxn],mark[maxn];
	inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void push_down(int x){
		if(mark[x]){
			mark[x]=0;
			mark[ch[x][0]]^=1,mark[ch[x][1]]^=1;
			swap(ch[x][0],ch[x][1]);
		}
	}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x);
		}
		if(!goal) rt=x;
	}
	inline void insert(int k){
		int x=rt,f=0;
		while(x) f=x,x=ch[x][k>val[x]];
		x=++tot;
		if(f) ch[f][k>val[f]]=x;
		fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,siz[x]=1,mark[x]=0;
		splay(x);
	}
	inline int kth(int k){
		int x=rt;
		while(1){
			push_down(x);
			if(k>siz[ch[x][0]]+1){
				k-=siz[ch[x][0]]+1;
				x=ch[x][1];
			}
			else if(k<=siz[ch[x][0]]) x=ch[x][0];
			else return x;
		}
	}
	inline void print(int x){
		push_down(x);
		if(ch[x][0]) print(ch[x][0]);
		if(val[x]>1&&val[x]

7.P3380 【模板】二逼平衡树(树套树)

需要支持区间 \([L,R]\) 中数据的排名、排名对应的数据、前驱后继以及单点修改。

需要线段树套平衡树,即在线段树的每个区间中,选择用平衡树来维护。

(1) 查询数据 \(k\) 在区间 \([L,R]\) 的排名

首先 \(k\) 的排名等价于,小于 \(k\) 的数据的个数 \(+1\),于是我们这个可以拆成若干个区间(线段树所维护的),求和即可。

inline int Seg_rank(int id,int l,int r,int pl,int pr,int k){
    if(pl<=l&&r<=pr){
        Splay_find(id,k);
        int x=rt[id];
        if(k<=val[x]) return siz[ch[x][0]]-1;
        else return siz[ch[x][0]]+cnt[x]-1;
    }
    int res=0;
    if(pl<=mid) res+=Seg_rank(lson,pl,pr,k);
    if(pr>mid) res+=Seg_rank(rson,pl,pr,k); 
    return res;
}

(2)查询区间 \([L,R]\) 中排名为 \(k\) 的数

这样的查询在跨区间的线段树中是不可行的,我们二分这个排名,然后进行查询排名操作直到找到答案。

inline int Seg_kth(int pl,int pr,int k){
    int l=0,r=1e8,res;
    while(l<=r){
        if(Seg_rank(1,1,n,pl,pr,mid)+1>k) r=mid-1;
        else res=mid,l=mid+1;
    }
    return res;
}

(3)修改位置 \(p\) 上的值为 \(k\)

非常简单,每个包含的区间都去修改即可。

inline void Seg_update(int id,int l,int r,int p,int k){
		Splay_erase(id,a[p]),Splay_insert(id,k);
		if(l==r){
			a[l]=k;
			return;
		}
		if(p<=mid) Seg_update(lson,p,k);
		else Seg_update(rson,p,k);
	}

(4)查询 \(k\) 在区间 \([L,R]\) 的前驱后继

前驱后继实质是一个数值,于是找每个区间的前驱最大值和后继最小值即可。

inline int Seg_pre(int id,int l,int r,int pl,int pr,int k){
    if(pl<=l&&r<=pr) return val[Splay_pre_nxt(id,k,0)];
    int res=minxn;
    if(pl<=mid) res=max(res,Seg_pre(lson,pl,pr,k));
    if(pr>mid) res=max(res,Seg_pre(rson,pl,pr,k));
    return res;
}
inline int Seg_nxt(int id,int l,int r,int pl,int pr,int k){
    if(pl<=l&&r<=pr) return val[Splay_pre_nxt(id,k,1)];
    int res=maxxn;
    if(pl<=mid) res=min(res,Seg_nxt(lson,pl,pr,k));
    if(pr>mid) res=min(res,Seg_nxt(rson,pl,pr,k));
    return res;
}
点击查看代码
int n,q;
int a[maxn];
int tot;
struct SegmentTree_Splay{
	int fa[maxm],ch[maxm][2],val[maxm],cnt[maxm],siz[maxm],rt[maxn];
	inline void Splay_maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
	inline bool Splay_pdson(int x){return x==ch[fa[x]][1];}
	inline void Splay_rotate(int x){
		int y=fa[x],z=fa[y],chk=Splay_pdson(x);
		ch[z][Splay_pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		Splay_maintain(y),Splay_maintain(x);
	}
	inline void Splay_splay(int id,int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) Splay_rotate(Splay_pdson(x)==Splay_pdson(y)?y:x);
			Splay_rotate(x); 
		}
		if(!goal) rt[id]=x;
	}
	inline void Splay_insert(int id,int k){
		int x=rt[id],f=0;
		if(!x){
			x=++tot;
			fa[x]=0,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
			rt[id]=x;
			return;
		}
		while(x&&val[x]!=k) f=x,x=ch[x][k>val[x]];
		if(x) cnt[x]++;
		else{
			x=++tot;
			if(f) ch[f][k>val[f]]=x;
			fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=siz[x]=1;
		}
		Splay_splay(id,x);
	}
	inline void Splay_find(int id,int k){
		int x=rt[id];
		if(!x) return;
		while(ch[x][k>val[x]]&&k!=val[x]) x=ch[x][k>val[x]];
		Splay_splay(id,x);
	}
	inline int Splay_pre_nxt(int id,int k,bool pd){
		Splay_find(id,k);
		int x=rt[id];
		if(val[x]>k&&pd) return x;
		if(val[x]1){
			cnt[x]--;
			Splay_splay(id,x);
		} 
		else ch[nxt][0]=0;
	}
	#define mid ((l+r)>>1)
	#define lson id<<1,l,mid
	#define rson id<<1|1,mid+1,r
	inline void Seg_build(int id,int l,int r){
		Splay_insert(id,maxxn),Splay_insert(id,minxn);
		if(l==r) return;
		Seg_build(lson),Seg_build(rson);
	}
	inline void Seg_insert(int id,int l,int r,int p,int k){
		Splay_insert(id,k);
		if(l==r) return;
		if(p<=mid) Seg_insert(lson,p,k);
		else Seg_insert(rson,p,k);
	}
	inline int Seg_rank(int id,int l,int r,int pl,int pr,int k){
		if(pl<=l&&r<=pr){
			Splay_find(id,k);
			int x=rt[id];
			if(k<=val[x]) return siz[ch[x][0]]-1;
			else return siz[ch[x][0]]+cnt[x]-1;
		}
		int res=0;
		if(pl<=mid) res+=Seg_rank(lson,pl,pr,k);
		if(pr>mid) res+=Seg_rank(rson,pl,pr,k); 
		return res;
	}
	inline void Seg_update(int id,int l,int r,int p,int k){
		Splay_erase(id,a[p]),Splay_insert(id,k);
		if(l==r){
			a[l]=k;
			return;
		}
		if(p<=mid) Seg_update(lson,p,k);
		else Seg_update(rson,p,k);
	}
	inline int Seg_pre(int id,int l,int r,int pl,int pr,int k){
		if(pl<=l&&r<=pr) return val[Splay_pre_nxt(id,k,0)];
		int res=minxn;
		if(pl<=mid) res=max(res,Seg_pre(lson,pl,pr,k));
		if(pr>mid) res=max(res,Seg_pre(rson,pl,pr,k));
		return res;
	}
	inline int Seg_nxt(int id,int l,int r,int pl,int pr,int k){
		if(pl<=l&&r<=pr) return val[Splay_pre_nxt(id,k,1)];
		int res=maxxn;
		if(pl<=mid) res=min(res,Seg_nxt(lson,pl,pr,k));
		if(pr>mid) res=min(res,Seg_nxt(rson,pl,pr,k));
		return res;
	}
	inline int Seg_kth(int pl,int pr,int k){
		int l=0,r=1e8,res;
		while(l<=r){
			if(Seg_rank(1,1,n,pl,pr,mid)+1>k) r=mid-1;
			else res=mid,l=mid+1;
		}
		return res;
	}
}tree;
int main(){
	n=read(),q=read();
	tree.Seg_build(1,1,n);
	for(int i=1;i<=n;i++){
		a[i]=read();
		tree.Seg_insert(1,1,n,i,a[i]);
	}
	while(q--){
		int opt=read();
		if(opt==1){
			int l=read(),r=read(),k=read();
			printf("%d\n",tree.Seg_rank(1,1,n,l,r,k)+1);
		}
		if(opt==2){
			int l=read(),r=read(),k=read();
			printf("%d\n",tree.Seg_kth(l,r,k));
		}
		if(opt==3){
			int p=read(),k=read();
			tree.Seg_update(1,1,n,p,k);
		}
		if(opt==4){
			int l=read(),r=read(),k=read();
			printf("%d\n",tree.Seg_pre(1,1,n,l,r,k));
		}
		if(opt==5){
			int l=read(),r=read(),k=read();
			printf("%d\n",tree.Seg_nxt(1,1,n,l,r,k));
		}
	}
	return 0;
}

8.P4036 JSOI2008 火星人

首先这个求 \(\operatorname{LCQ(x,y)}\) 显然可以二分求哈希,发现排序关键字直接瞎搞,强行维护一下序列即可。

最主要的是掌握对于单点和子树修改时,空出修改的部分使其独立,方式是查找前驱后继(本题是第 \(k\) 大)并旋转到梗,其他看代码吧。

点击查看代码
ull p[maxn];
int q;
int rt,tot;
struct Splay{
	int fa[maxn],ch[maxn][2],len[maxn],siz[maxn];
	ull val[maxn],h[maxn];
	inline void init(){
		ch[1][1]=2,fa[2]=1;
		rt=1,tot=2;
		maintain(2),maintain(1);
	}
	inline void maintain(int x){
		siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
		len[x]=len[ch[x][0]]+len[ch[x][1]]+((x>2)?1:0);
		h[x]=(h[ch[x][0]]*base1+val[x])*p[len[ch[x][1]]]+h[ch[x][1]]; 
	}
	inline bool pdson(int x){return x==ch[fa[x]][1];}
	inline void rotate(int x){
		int y=fa[x],z=fa[y],chk=pdson(x);
		ch[z][pdson(y)]=x,fa[x]=z;
		ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
		ch[x][chk^1]=y,fa[y]=x;
		maintain(y),maintain(x);
	}
	inline void splay(int x,int goal=0){
		while(fa[x]!=goal){
			int y=fa[x],z=fa[y];
			if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
			rotate(x);
		}
		if(!goal) rt=x;
		maintain(x);
	}
	inline int kth(int k){
		int x=rt;
		while(1){
			if(k>siz[ch[x][0]]+1){
				k-=siz[ch[x][0]]+1;
				x=ch[x][1]; 
			}
			else if(k<=siz[ch[x][0]]) x=ch[x][0];
			else return x;
		}
	}
	inline void insert(int x,int k){
		int pre=kth(x+1),nxt=kth(x+2);//插入位置的前一个字符为x,实质的位置为x+1,后面一个字符位置是x+2,旋转之后空出了x+1与x+2的空间
		splay(pre),splay(nxt,pre);
		fa[++tot]=nxt,ch[nxt][0]=tot,val[tot]=k;
		splay(tot);
	}
	inline ull get_h(int x,int len){
		int pre=kth(x),nxt=kth(x+len+1);//查询区间[x+1,x+len],前驱后继分别为x和x+len+1,空出检索的子树
		splay(pre),splay(nxt,pre);
		return h[ch[nxt][0]];
	}
	inline void update(int x,int k){
		int pre=kth(x),nxt=kth(x+2);//更新字符x,实际位置x+1,前驱后继分别为x和x+2,空出更新的字符位置
		splay(pre),splay(nxt,pre);
		val[ch[nxt][0]]=k;
		maintain(ch[nxt][0]);
		maintain(nxt),maintain(pre);
	}
	inline int query(int x,int y){
		int l=0,r=tot-max(x,y)-1,res;
		while(l<=r){
			int mid=(l+r)>>1;
			if(get_h(x,mid)==get_h(y,mid)){
				l=mid+1;
				res=mid;
			}
			else r=mid-1;
		}
		return res;
	}
}tree;
char s[maxn];
int main(){
	p[0]=1;
	for(int i=1;i<=maxn;i++){
		p[i]=p[i-1]*base1;
	}
	tree.init();//先插入两个边界值
	scanf("%s",s+1);
	for(int i=1;i<=strlen(s+1);i++){
		tree.insert(i-1,s[i]);
	}
	q=read();
	while(q--){
		char ch[2];
		scanf("%s",ch);
		if(ch[0]=='Q'){
			int x=read(),y=read();
			printf("%d\n",tree.query(x,y));
		}
		else if(ch[0]=='R'){
			int x=read(),c;
			scanf("%s",ch);
			c=ch[0];
			tree.update(x,c);
		}
		else{
			int x=read(),c;
			scanf("%s",ch);
			c=ch[0];
			tree.insert(x,c);
		}
	}
	return 0;
}