Splay(伸展树)学习笔记

Splay 时间复杂度证明

咕咕。

Splay 基本操作

具体原理

可以参考以下博客(个人觉得写得比较好):

普通平衡树(权值Splay)

题目地址

前置变量声明

const int N = 1e5+7;
int rt,tot;	//Splay的根,Splay总结点数 
int val[N];	//val[u] u的权值 
int fa[N],ch[N][2];	//fa[u] u的父亲,ch[u][0]左儿子,ch[u][1]右儿子 
int cnt[N];	//cnt[u] 有几个权值都是 val[u] 的数 
int sz[N];	//sz[u] u的子树大小

前置操作

inline int get(int x) { return ch[fa[x]][1] == x; }
inline void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; }

get(x)\(x\) 是父亲的左儿子还是右儿子,左儿子返回 \(0\),右儿子返回 \(1\)
push_up(x):更新 \(x\) 的子树大小。

旋转 rotate

最重要的操作,Splay 的核心。

inline void rotate(int x) {
	int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
	ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
	push_up(y); push_up(x);
}

注意代码赋值顺序

旋转虽然分为左旋和右旋,但自己手推后可以发现左旋和右旋是相互对称的,因此可以合并在一起写。下面给出右旋一张具体的图:

伸展 splay

void splay(int x,int goal) {	//将 x 旋转至 goal 的儿子 
	while(fa[x] != goal) {
		int y = fa[x], z = fa[y];
		if(z != goal) rotate((get(x)==get(y) ? y : x));
		rotate(x);
	}
	if(goal == 0) rt = x;
}

每次连续旋转两次,如果三点一线就先旋父亲,这个和 Splay 的复杂度分析有关,原理先咕咕。

插入 insert

void insert(int x) {	//插入一个权值为 x 的数 
	int now = rt, fath = 0;
	while(now!=0 && val[now]!=x) {
		fath = now; now = ch[now][x>val[now]];
	}
	if(now != 0) {
		++cnt[now];
	} else {
		now = ++tot; fa[now] = fath, ch[fath][x>val[fath]] = now; val[now] = x; cnt[now] = 1; sz[now] = 1;
	}
	splay(now,0);
}

插入后将新节点旋转到根,这个和 Splay 的复杂度分析有关,原理先咕咕。

查找 find

void find(int x) {	//找到 x 这个值对应的结点并将其旋转至根 
	int now = rt;
	if(!now) return ;
	while(ch[now][x>val[now]]!=0 && val[now]!=x) {
		now = ch[now][x>val[now]];
	}
	splay(now, 0);
}

查找操作可以为前驱后继操作服务。

注意find 操作结束后,根节点(也就是 \(x\) 这个值对应的结点)的左子树大小 \(+1\),即 \(sz[ch[rt,0]]+1\),就代表 \(x\) 这个值在集合中的排名。

前驱 pre

int pre(int x) {	//找到数 x 的前驱,并返回这个结点的编号
	find(x);
	if(val[rt] < x) return val[rt];
	//当集合中没有权值为 x 的这个结点,如果查找到的结点就小于 x,那么一定比自己的左儿子更接近 x 
	int now = ch[rt][0];
	while(ch[now][1] > 0) {
		now = ch[now][1];
	}
	return val[now];
}

后继 nxt

int nxt(int x) {	//找到数 x 的后继,并返回这个结点的编号
	find(x);
	if(val[rt] > x) return val[rt];
	//当集合中没有权值为 x 的这个结点,如果查找到的结点就大于 x,那么一定比自己的右儿子更接近 x 
	int now = ch[rt][1];
	while(ch[now][0] > 0) {
		now = ch[now][0];
	}
	return val[now];
}

删除 del

void del(int x) {	//删除权值为 x 的结点 
	int p = pre(x), q = nxt(x);
	splay(p,0), splay(q,p);
	if(cnt[ch[q][0]] > 1) {
		--cnt[ch[q][0]]; splay(ch[q][0], 0);
	} else {
		ch[q][0] = 0;
	}
}

将操作后的结点(如果 \(cnt[u]>1\),没有将结点删除)旋转到根,可能和 Splay 的复杂度分析有关,原理先咕咕。

查询 \(k\) 小值 kth

int kth(int k) {	//找到排名为 k 的数并返回 
	if(sz[rt] < k) return -1;	//没有排名为 k 的数 
	int now = rt;
	while(true) {
		if(k <= sz[ch[now][0]]) now = ch[now][0];
		else if(k <= sz[ch[now][0]]+cnt[now]) return val[now];
		else k -= sz[ch[now][0]]+cnt[now], now = ch[now][1];
	}
}

完整版 Code

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
inline int read() {
    int x = 0, f = 1; char ch = getchar();
    while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
    return x * f;
}
const int N = 1e5+7;
int m;
struct Splay {
	int rt,tot;	//Splay的根,Splay总结点数 
	int val[N];	//val[u] u的权值 
	int fa[N],ch[N][2];	//fa[u] u的父亲,ch[u][0]左儿子,ch[u][1]右儿子 
	int cnt[N];	//cnt[u] 有几个权值都是 val[u] 的数 
	int sz[N];	//sz[u] u的子树大小
	inline int get(int x) { return ch[fa[x]][1] == x; }
	inline void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; }
	inline void rotate(int x) {
		int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
		ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
		push_up(y); push_up(x);
	}
	void splay(int x,int goal) {	//将 x 旋转至 goal 的儿子 
		while(fa[x] != goal) {
			int y = fa[x], z = fa[y];
			if(z != goal) rotate((get(x)==get(y) ? y : x));
			rotate(x);
		}
		if(goal == 0) rt = x;
	}
	void insert(int x) {	//插入一个权值为 x 的数 
		int now = rt, fath = 0;
		while(now!=0 && val[now]!=x) {
			fath = now; now = ch[now][x>val[now]];
		}
		if(now != 0) {
			++cnt[now];
		} else {
			now = ++tot; fa[now] = fath, ch[fath][x>val[fath]] = now; val[now] = x; cnt[now] = 1; sz[now] = 1;
		}
		splay(now,0);
	}
	void find(int x) {	//找到 x 这个值对应的结点并将其旋转至根 
		int now = rt;
		if(!now) return ;
		while(ch[now][x>val[now]]!=0 && val[now]!=x) {
			now = ch[now][x>val[now]];
		}
		splay(now, 0);
	}
	int pre(int x) {	//找到数 x 的前驱,并返回这个结点的编号 
		find(x);
		if(val[rt] < x) return rt;
		//当集合中没有权值为 x 的这个结点,如果查找到的结点就小于 x,那么一定比自己的左儿子更接近 x 
		int now = ch[rt][0];
		while(ch[now][1] > 0) {
			now = ch[now][1];
		}
		return now;
	}
	int nxt(int x) {	//找到数 x 的后继,并返回这个结点的编号
		find(x);
		if(val[rt] > x) return rt;
		//当集合中没有权值为 x 的这个结点,如果查找到的结点就大于 x,那么一定比自己的右儿子更接近 x 
		int now = ch[rt][1];
		while(ch[now][0] > 0) {
			now = ch[now][0];
		}
		return now;
	}
	void del(int x) {	//删除权值为 x 的结点 
		int p = pre(x), q = nxt(x);
		splay(p,0), splay(q,p);
		if(cnt[ch[q][0]] > 1) {
			--cnt[ch[q][0]]; splay(ch[q][0], 0);
		} else {
			ch[q][0] = 0;
		}
	}
	int kth(int k) {	//找到排名为 k 的数并返回 
		if(sz[rt] < k) return -1;	//没有排名为 k 的数 
		int now = rt;
		while(true) {
			if(k <= sz[ch[now][0]]) now = ch[now][0];
			else if(k <= sz[ch[now][0]]+cnt[now]) return val[now];
			else k -= sz[ch[now][0]]+cnt[now], now = ch[now][1];
		}
	}
	int rk(int x) {
		find(x); return sz[ch[rt][0]] + 1;
	}
}T;
int main()
{
	m = read();
	T.insert(-INF), T.insert(INF);
	while(m--) {
		int opt = read(), x = read();
		if(opt == 1) {
			T.insert(x);
		} else if(opt == 2) {
			T.del(x);
		} else if(opt == 3) {
			printf("%d\n",T.rk(x)-1);
		} else if(opt == 4) {
			printf("%d\n",T.kth(x+1));
		} else if(opt == 5) {
			printf("%d\n",T.val[T.pre(x)]);
		} else if(opt == 6) {
			printf("%d\n",T.val[T.nxt(x)]);
		}
	}
    return 0;
}
/*
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

106465
84185
492737
*/

文艺平衡树(序列 Splay)

题目地址

在维护序列操作的 Splay 上,排名为 \(i\) 的结点对应在序列上下标为 \(i\) 的数。

性质1: 中序遍历的树,只要不断交换左右子树就可以实现序列翻转。

手推一下即可。

因此我们可以用 Splay + 懒标记实现。

Code

#include<bits/stdc++.h>
using namespace std;
inline int read() {
    int x = 0, f = 1; char ch = getchar();
    while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
    return x * f;
}
const int N = 1e5+7;
int n,m;
struct Splay {
	int rt,tot;
	int fa[N],ch[N][2],lazy[N],sz[N];
	inline int get(int x) {
		return ch[fa[x]][1] == x;
	}
	inline void push_up(int x) {
		sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1;
	}
	inline void push_down(int x) {
		if(lazy[x] == 1) {
			lazy[ch[x][0]] ^= 1, lazy[ch[x][1]] ^= 1; swap(ch[x][0],ch[x][1]); lazy[x] = 0;
		}
	}
	inline void rotate(int x) {
		push_down(x);
		int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
		ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
		push_up(y); push_up(x);
	}
	void splay(int x,int goal) {	//将 x 旋转到 goal 的儿子 
		while(fa[x] != goal) {
			int y = fa[x], z = fa[y];
			if(z != goal) rotate((get(x)==get(y) ? y : x));
			rotate(x);
		}
		if(goal == 0) rt = x;
	}
	void insert(int x) {
		int now = rt, fath = 0;
		while(now != 0) {
			fath = now; now = ch[now][x>now];
		}
		now = ++tot; ch[fath][x>fath] = now; fa[now] = fath; sz[now] = 1; lazy[now] = 0;
		splay(now,0);
	}
	int find(int x) {	//找的序列中下标为 x 的在splay树上代表的结点 
		int now = rt;
		if(x > sz[rt]) return -1;
		while(true) {
			push_down(now);
			if(x <= sz[ch[now][0]]) now = ch[now][0];
			else if(x == sz[ch[now][0]]+1) return now;
			else x -= sz[ch[now][0]]+1, now = ch[now][1];
		}
	}
	void Turn(int l,int r) {
		int x = find(l), y = find(r+2);
		splay(x,0), splay(y,x);
		lazy[ch[y][0]] ^= 1;
	}
	void Print(int now) {
		push_down(now);
		if(ch[now][0]) Print(ch[now][0]);
		if(1<now && now<n+2) printf("%d ",now-1);
		if(ch[now][1]) Print(ch[now][1]);
	}
}T;
int main()
{
	n = read(), m = read();
	for(int i=1;i<=n+2;++i) T.insert(i);
	while(m--) {
		int l = read(), r = read();
		T.Turn(l,r);
	}
	T.Print(T.rt);
	return 0;
}
/*
5 3
1 3
1 3
1 4

4 3 2 1 5
*/

更多练习题

咕咕。

posted @ 2020-11-21 23:54  基地AI  阅读(324)  评论(0)    收藏  举报