Treap和Splay学习小结

最近真的是花了很大的力气来学习平衡树的内容,因为之前在弄偏序的时候提到了平衡树的内容,真的是开了一个大坑啊。
先说说treap吧,网上的资料很多,描述的也很详细,我就不细说了,这里简单的谈谈我的看法,treap就是在二叉树的基础上加了一个随机域来保证堆的性质
这样可以把树的高度维护到期望的logn级别,其他的就是一棵二叉排序树,并没有什么特别的。
我实现了指针版和数组版两种,但是说实话,在写题的时候还是数组版的实用,无论是调试还是时间和代码量,数组都要显得优越很多。下面附上数组版的代码实现

struct Treaps
{
	const static int maxn = 1e6 + 3e5;
	int L[maxn], R[maxn], v[maxn], p[maxn], A[maxn], C[maxn], tot;
	void clear(){ A[0] = L[0] = R[0] = C[0] = 0; tot = 1; }
	int Node(int V, int P){ L[tot] = R[tot] = 0; v[tot] = V; p[tot] = P; A[tot] = C[tot] = 1; return tot++; }
	void Count(int x){ C[x] = A[x] + C[L[x]] + C[R[x]]; }
	void rotate_right(int &x)
	{
		int y = L[x]; L[x] = R[y]; R[y] = x; C[y] = C[x]; Count(x); x = y;
	}
	void rotate_left(int &x)
	{
		int y = R[x]; R[x] = L[y]; L[y] = x; C[y] = C[x]; Count(x); x = y;
	}
	void Insert(int& x, int V, int P)
	{
		if (!x) { x = Node(V, P); return; }
		if (v[x] == V) ++A[x];
		else if (V < v[x])
		{
			Insert(L[x], V, P);
			if (p[x]>p[L[x]]) rotate_right(x);
		}
		else
		{
			Insert(R[x], V, P);
			if (p[x] > p[R[x]]) rotate_left(x);
		}
		Count(x);
	}
	void add(int &x, int V){ Insert(x, V, rand()); }//外部直接调用,x是树根,v是值
	void Delete(int &x, int V)
	{
		if (!x) return;
		if (V < v[x]) Delete(L[x], V);
		else if (V > v[x]) Delete(R[x], V);
		else if (A[x] > 1) --A[x];
		else if (!L[x] || !R[x]) x = L[x] + R[x];
		else if (p[L[x]] < p[R[x]]){ rotate_right(x); Delete(R[x], V); }
		else { rotate_left(x); Delete(L[x], V); }
		Count(x);
	}
	void dec(int &x, int V) { Delete(x, V); }//外部直接调用,x是树根,v是值
	int find(int x, int V)//返回树x中小于等于V的数字个数
	{
		int ans = 0;
		for (int i = x; i; i = V < v[i] ? L[i] : R[i])
		{
			if (v[i] <= V) ans += C[L[i]] + A[i];
		}
		return ans;
	}
};

相比起treap,我觉得我对于splay的理解要深刻很多,毕竟做了那么多题,花了那么多时间的。
splay严格来讲不是二叉平衡树,但是它能保证均摊logn的效率,最神奇的是splay操作,简直就是线段树加强版。
相较于treap来说,splay不需要任何额外的内容,只要保证一个splay和旋转的操作即可,而所谓的splay操作
就是通过旋转把一个点向上转到目标点的操作。当然splay的具体操作等等都可以百度到,我就不多说了。
这里也谈谈我的理解吧,我觉得splay的关键操作只有两个,一个是旋转,一个是splay,下面附上代码


struct Splays
{
	const static int maxn = 3e5 + 10;			//节点个数
	const static int INF = 0x7FFFFFFF;			//int最大值
	int ch[maxn][2], F[maxn], sz;				//左右儿子,父亲节点和节点总个数
	int A[maxn];
	int Node(int f, int a) { A[sz] = a; ch[sz][0] = ch[sz][1] = 0; F[sz] = f; return sz++; }//申请一个新节点
	void clear(){ sz = 1; ch[0][0] = ch[0][1] = F[0] = 0; }//清空操作
	void rotate(int x, int k)
	{
		int y = F[x]; ch[y][!k] = ch[x][k]; F[ch[x][k]] = y;
		if (F[y]) ch[F[y]][y == ch[F[y]][1]] = x;
		F[x] = F[y];    F[y] = x;	ch[x][k] = y;
		//把y的值给x,重新计算y的值
	}
	void Splay(int x, int r)
	{
		while (F[x]!=r)
		{
			if (F[F[x]] == r) { rotate(x, x == ch[F[x]][0]); return; }
			int y = x == ch[F[x]][0], z = F[x] == ch[F[F[x]]][0];
			y^z ? (rotate(x, y), rotate(x, z)) : (rotate(F[x], z), rotate(x, y));
		}
	}
	void insert(int &x, int a)
	{
		if (!x) { x = Node(0, a); return; }
		int now = 0;
		for (int i = x; i; i = ch[i][A[i] < a])
		{
			//下传延迟标记
			if (!ch[i][A[i] < a]) { now = ch[i][A[i] < a] = Node(i, a); break; }
		}
		Splay(now, 0);	x = now;
	}
}solve;

从代码上看是不是并没有比treap复杂,其实的确如此,并且splay可以做到区间反转,移动等等高难度动作,虽然treap也可强行的做,但是我觉得那样treap本身的随机域就失去了意义了。我觉得我独到的理解在于两处打了注释的地方,这是splay延迟标记的关键,就想线段树区间更新一样,我们只要在下去的时候传标记,上来的时候统计即可,
一开始我也是模仿他人的代码写的,但是呢我这个人有强波症,这个版本是我精简多次以后确认的,虽然可能不是最短的,但我觉得应该还是很清晰的,对于不同的题目来说
splay操作是无需改动的,插入操作可能不同,旋转操作只有注释的地方根据不同题目会有不同。

根据一个星期来的做题经验,splay的题目难点在于两个,一个是延迟标记的时候判断细节,一个是删除操作和合并操作的细节处理,这两个问题可以说是最为困难的,
往往是极小的一个细节错误就会导致tle啊wa啊什么的,感觉做splay的题目对于培养细心真的是很有帮助的。

推荐以下5道难题,搞定以后基本是splay大成了感觉。。

HYSBZ(BZOJ) 1500 维修数列
POJ 3580 SuperMemo
HDU 2475 Box

FZU 1978 Repair the brackets


HDU 3726 Graph and Queries


现在是1点半了,这几天为了搞定splay我也是疯了。。


你可能感兴趣的:(Treap和Splay学习小结)