splay从入门到入土

文章目录

  • splay算法学习
    • 前言
    • 模板学习
      • 旋转
      • splay旋转模板
  • 例题学习:维护区间
      • 套路:
      • 例题1:裸的区间翻转
      • 例题2:插入点,区间删除

splay算法学习

前言

  • 本质: splay本质上是一棵平衡树。其通过旋转将树趋于平衡,同时不改变树的中序遍历,使得达到理想的时间复杂度( l o g 2 n log_2n log2n级别)
  • splay 的旋转功能可以实现一系列强大的操作,例如:区间翻转等
  • 注意点:splay只维护中序遍历,并无二叉搜索树的性质。
  • 套路1: splay 中序遍历递增,维护值
  • 套路2: splay 中序遍历维护区间,维护区间

模板学习

旋转

目的;

  • 将某个点 x 通过旋转的方式,往上提,直到成为一个目标点 goal 的下面,并保证中序遍历不变

单次旋转即单旋分为左旋右旋
splay从入门到入土_第1张图片

  • 当 x 为其父结点的左孩子,此时要右旋
  • 当 x 为其父节点的右孩子,此时要左旋
void rotate(int x) {//单旋一次 
	int fa1=tr[x].fa;
	int fa2=tr[fa1].fa;
	
	int tag=(tr[fa1].son[1]==x);//tag=0在左孩子,右旋;tag=1在右孩子,左旋 
	
	tr[fa2].son[fa1==tr[fa2].son[1]]=x;
	tr[x].fa=fa2;
	tr[fa1].son[tag]=tr[x].son[tag^1];
	tr[tr[x].son[tag^1]].fa=fa1;
	tr[x].son[tag^1]=fa1;
	tr[fa1].fa=x;
}

两次旋转即双旋分为一字型和之字型

  • 一字型(子与父和父与爷一致):对 x 的父节点单旋一次,再对 x 单旋一次
  • 之字型(子与父和父与爷不一致):对 x 结点单旋两次
    splay从入门到入土_第2张图片

splay旋转(goal一般为 0/root,表示将 x 提到根结点或根节点的下面)

  • 当 x 的父节点是 goal,对 x 进行单旋
  • 当 x 的父节点不是 goal,对 x 进行双旋
void splay(int x,int goal) { //将x旋转向上提,直到其父节点为goal
	while(tr[x].fa!=goal) {
		int fa1=tr[x].fa;
		int fa2=tr[fa1].fa;
		if(fa2!=goal)(tr[fa1].son[0]==x)^(tr[fa2].son[0]==fa1)?rotate(x):rotate(fa1);
			rotate(x);
		}
	if(goal==0)root=x;
}

为什么要有双旋和单旋之分

  • 显然只单旋完全可以,但是对于时间复杂度,双旋会大大降低时间。

splay旋转模板

#include
using namespace std;
const int N=1e5+100,inf=1e9;

struct Splay {
	int tot,root;
	int fa[N],son[N][2],size[N],val[N],cnt[N],id[N],tag[N];

	Splay() {
		root=0;
		tot=0;
		insert(-inf,0);
		insert(inf,0);
	}
	void pushup(int u) {
		size[u]=size[son[u][0]]+size[son[u][1]]+cnt[u];
	}
	void pushdown(int u) {
		if(tag[u]) {
			tag[u]=0;
			swap(son[u][0],son[u][1]);
			tag[son[u][0]]^=1;
			tag[son[u][1]]^=1;
		}
	}
	void rotate(int u) {//单旋一次
		int fa1=fa[u];
		int fa2=fa[fa1];

		int tag=(son[fa1][1]==u);//tag=0在左孩子,右旋;tag=1在右孩子,左旋

		son[fa2][fa1==son[fa2][1]]=u;
		fa[u]=fa2;
		son[fa1][tag]=son[u][tag^1];
		fa[son[u][tag^1]]=fa1;
		son[u][tag^1]=fa1;
		fa[fa1]=u;

		pushup(fa1);
		pushup(u);
	}
	void splay(int u,int goal=0) { //将u旋转向上提,直到其父节点为goal
		while(fa[u]!=goal) {
			int fa1=fa[u];
			int fa2=fa[fa1];
			if(fa2!=goal)(son[fa1][0]==u)^(son[fa2][0]==fa1)?rotate(u):rotate(fa1);
			rotate(u);
		}
		if(goal==0)root=u;
	}
	void insert(int x,int ID=0) { //插入数据x,并将x提到树根
		int u=root,f=0;
		while(u!=0) {
			f=u;
			if(x>val[u])u=son[u][1];
			else if(x<val[u])u=son[u][0];
			else break;
		}
		if(u!=0)cnt[u]++;//存在这个数,计数加1
		else {
			u=++tot;//不存在这个数,动态开点
			if(f!=0)son[f][x>val[f]]=u;
			son[u][0]=son[u][1]=0;
			fa[u]=f;
			val[u]=x;
			cnt[u]=size[u]=1;
			id[u]=ID;
		}
		splay(u);
	}
	int find(int x) {//找 <=x 的最大值或 >=x 的最小值
		int u=root;
		while(son[u][x>val[u]]&&x!=val[u])u=son[u][x>val[u]];
		splay(u);
		return u;
	}
	int next(int x,int k) { //k=0找前驱,k=1找后继
		find(x);
		if((val[root]<x&&!k)||(val[root]>x&&k))return root;
		int u=son[root][k];
		while(son[u][k^1]!=0)u=son[u][k^1];//左子树的最右子树是前驱,右子树的最左子树是后继
		return u;
	}
	void del(int x) {//前驱转到根,后继转到根的下面,这样 x 就在根的右子树的左子树上了
		int u=next(x,0),v=next(x,1);
		splay(u,0);
		splay(v,u);
		if(cnt[son[v][0]]>1) {
			cnt[son[v][0]]--;
			splay(son[v][0]);
		} else {
			son[v][0] = 0;
			pushup(v);
			pushup(u);
		}
	}
	int rank(int x) {//x的排名 
		find(x);
		return size[son[root][0]]+(x>val[root]?cnt[root]:0);
	}
	int kth(int k) {//第 k 小 
		int u = root;
		k++;
		while(1) {
			//pushdown(u);
			if(size[son[u][0]]>=k)u=son[u][0];
			else if(size[son[u][0]]+cnt[u]>=k)return u;
			else k-=size[son[u][0]]+cnt[u],u=son[u][1];
		}
	}
	void write(int u) {//中序遍历输出 
		//pushdown(u);
		//if(son[u][0])write(son[u][0]);
		//if(id[u]>=1&&id[u]<=n)cout<
		//if(son[u][1])write(son[u][1]);
	}
	void reserve(int l,int r) {//与pushdown同用
		int u=kth(l-1),v=kth(r+1);
		splay(u,0);
		splay(v,u);
		tag[son[v][0]]^=1;
	}
	void delrange(int l,int r) {
		int u=kth(l-1),v=kth(r+1);
		splay(u,0);
		splay(v,u);
		son[v][0]=0;
		pushup(v);
		pushup(u);
	}
	void delminx(int x){//删除小于x的所有数 
		insert(x,0);
		int u=find(-inf),v=find(x); 
		splay(u,0);
		splay(v,u);
		son[v][0]=0;
		pushup(v);
		pushup(u);
		del(x);
	} 
	void delminnx(int x){//删除小于等于
		int u=find(-inf),v=next(x,1);
		splay(u,0);
		splay(v,u);
		son[v][0]=0;
		pushup(v);
		pushup(u);
	}
	void next_insert(int k,int x){
		int u=kth(k),v=kth(k+1);
		splay(u,0);
		splay(v,u);
		int w=++tot;
		size[v][0]=w;
		fa[w]=v;
		son[w][0]=son[w][1]=0;
		cnt[w]=size[w]=1;
		val[w]=x;
		pushup(v);
		pushup(u);                                                       
	}
}tr;

例题学习:维护区间

套路:

  • 核心: 维护 splay 的中序遍历作为需要维护的区间。

例题1:裸的区间翻转

例题链接

主要目的: 有 n 个数,第 i 个数为 i ,经过若干次区间翻转后,每个位置上的数是什么。

翻转区间 [ l , r ] [l,r] [l,r]

  • 中序遍历维护整个区间。
  • 将第 l − 1 l-1 l1 位置的数旋转到根,将第 r + 1 r+1 r+1 位置的数旋转到根结点的下面,那么 [ l , r ] [l,r] [l,r] 这个区间的所有数就都在第 r + 1 r+1 r+1 位置的数的左子树上了。
  • 我们再递归该子树,将每个结点的左右孩子交换即可实现区间翻转了。但是这样递归是 O(n) 的。
  • 我们可以向线段树一样,在要区间翻转的子树的根处打上懒标记,递归到的时候向下传递(pushdown)就可以了。

Code在板子里

例题2:插入点,区间删除

例题链接

主要目的: 若干次操作,每次操作要么添加一个数 x ,要么删除 < x <x 的所有数,要么查第 k 大。

如何区间删除:如何将中序遍历 [l,r] 的区间删除

  • 将第 l − 1 l-1 l1 位置的数旋转到根,将第 r + 1 r+1 r+1 位置的数旋转到根的下面,这样要删除的区间就是第 r + 1 r+1 r+1 位置的数的左子树上了,直接赋个 0 ,在 pushup 一下即可。

如何删除小于 x 的所有数

  • 插入数字 x ,将 − i n f -inf inf 旋转到根,将 x x x 旋转到根的右子树,这样小于 x x x 的所有数就在 x x x 的左子树上了。之后再删除 x x x 即可。

如何删除小于等于 x 的所有数

  • − i n f -inf inf 旋转到根,将 x x x 的后继旋转到根的右子树,这样小于等于 x 的所有数就在 x x x 的左子树上了。

Code都在板子里

你可能感兴趣的:(数据结构,知识图谱,算法,深度学习)