Splay模板 Splay題型大薈萃

以HDU4453爲例,整理了一些Splay的題型

/*
【算法介紹】
Splay叫做伸展樹,是一種二叉搜索樹,也可以說是一種平衡樹結構。
其可以維護節點的左右次序值,也就是說,我們在Splay上做中序遍歷的次序輸出節點,得到的便是所有節點的左右次序。

【數據結構】
int ch[N][2], fa[N];	//節點的鏈接關係
int num[N], sz[N];		//節點個數與子樹大小

1,anc定義爲該平衡樹實際上的根,然而其中並不存放任何信息,初始有const anc = 0
2,#define rt ch[anc][0] 爲該平衡樹邏輯上的根
3,#define keynode ch[ch[rt][1]][0] 爲邏輯根的右兒子的左兒子,可以方便我們做一系列操作

【基本操作】
1,int newnode(int val) 新建節點,給節點賦予初值val
2,int D(int x) 返回x是其父節點的哪個兒子
3,void setc(int x, int y, int d) 設置x的d號兒子爲y
4,void rotate(int x) 旋轉使得x與其父節點交換位置,然而其左右關係保持不變
5,void splay(int x) 把節點x旋轉到anc的左兒子位置,即rt位置。該操作的旋轉特性使得總複雜度維持在log級別
有了這5個操作,我們可以在不改變中序遍歷的條件下該邊子孫關係了。

【查找插入操作】
然而,splay的節點除了父子關係外,還具有權值v[]。
如果,v[]只是其先後次序的離散化映射,那麼,我們便可以在這個二叉搜索樹上查詢某個值val在樹的哪個節點上。
splay是一棵可以快速動態維護的二叉搜索樹.
所以我們還可以在樹上查找權值所對應的節點:find(val);可以在樹上ins(val)
平衡樹上的查找操作,除了find()
還有查找最左元素void first(),查找最右元素void last(),以及查找第k小操作void kth()

【高級區間操作】
在這些基礎操作之上,我們可以實現一些高級的Splay的區間操作
1,void del(x) 把x節點刪除
實現方法:先把x轉到rt,再把左子樹放到rt,再把右子樹放到左子樹的右子樹位置
2,void segment(l, r) 把[l,r]區間都轉到keynode的位置
{ splay(kth(l - 1), anc); splay(kth(r + 1), rt); return keynode; }
實現方法:先把第l-1個節點轉到anc的兒子(左兒子)位置,即rt位置。這時[l,n]的所有節點都在rt的右子樹上。
然後	再把第r+1個節點轉到rt的兒子(右兒子)位置,這時[l,r]的所有節點都在第r+1號節點的左兒子處,即keynode位置。
3,void split(l, r) 把區間[l,r]刪除
實現方法:先調用segment(l,r)得到要刪除的區間,然後直接刪除=w=
4,void inspos(int x, int pos) 把子樹x插入到pos位置的右側
實現方法:先調用segment(pos+1,pos)使得位於rt,pos+1位於rt的右兒子位置(ch[rt][1]位置)
這時ch[rt][1]的左兒子爲空,直接把x插入到該位置即可

【修改相關操作】
除了我們需要實現位置的變更以外,有時還需要用splay做區間修改操作。
區間修改操作爲了保證複雜度的要求,一定會需要用到延遲標記。
然而,這裏的延遲標記與線段樹的不同
線段樹的延遲標記是位於實際並不存在的虛擬段節點上
而Splay的延遲標記則是切切實實放在某個具體節點上的。

如果一個節點具有延遲性的標記,那麼意味着,在其整棵子樹上,都應該生效其影響。
於是,我們需要有pushdown()和pushup()的函數

什麼時候需要pushdown()?
在我們改變父子關係(即splay操作)的時候,需要對父與子都各自做一次pushdown()
在我們改變做子樹遍歷查找的時候,因爲這裏涉及到reverse操作,所以也需要pushdown()

什麼時候需要pushup()?
setc()的時候需要做pushup(),而且這個pushup()需要一直延續到根(即rt位置)


【Debug相關操作】
我們還可以通過一定函數實現方便的Debug


有兩個問題——
1,左右順序是我們定的,與val無關
2,左右順序是由val決定的
*/
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<string>
#include<ctype.h>
#include<math.h>
#include<set>
#include<map>
#include<vector>
#include<queue>
#include<bitset>
#include<algorithm>
#include<time.h>
#include<assert.h>
using namespace std;
#define MS(x,y) memset(x,y,sizeof(x))
#define MC(x,y) memcpy(x,y,sizeof(x))
#define MP(x,y) make_pair(x,y)
#define ls o<<1
#define rs o<<1|1
typedef long long LL;
typedef unsigned long long UL;
typedef unsigned int UI;
template <class T1, class T2>inline void gmax(T1 &a, T2 b) { if (b>a)a = b; }
template <class T1, class T2>inline void gmin(T1 &a, T2 b) { if (b<a)a = b; }
const int N = 2e5 + 10, M = 0, Z = 1e9 + 7, ms63 = 0x3f3f3f3f;
int casenum, casei;
int n, m, k1, k2;

//Splay模板
struct SPT
{
	int ch[N][2], fa[N];	//節點的結構狀況
	int num[N], sz[N];		//節點個數與子樹大小
	int v[N];				//節點權值
	int rev[N], ad[N];		//節點標記信息
	int ID;
	const int anc = 0;
#define rt ch[anc][0]
#define keynode ch[ch[rt][1]][0]

	//基本點操作:單點初始化
	void clear(int x)
	{
		ch[x][0] = ch[x][1] = fa[x] = 0;
		sz[x] = rev[x] = ad[x] = 0;
	}

	//基本點操作,從內存池中創建新節點
	int newnode(int val)
	{
		int x = ++ID; clear(x);
		v[x] = val;
		num[x] = sz[x] = 1;
		return x;
	}

	//基本點操作:返回當前節點是父節點的第幾個兒子
	int D(int x)
	{
		return ch[fa[x]][1] == x;
	}

	//基本點操作:設置x的d號兒子爲y
	void setc(int x, int y, int d)
	{
		ch[x][d] = y;
		if (y)fa[y] = x;
		if (x)pushup(x);
	}

	//區間操作轉爲點操作:對x的左右子樹做反轉
	void reverse(int x)
	{
		if (x == 0)return;//Necessary
		swap(ch[x][0], ch[x][1]);
		rev[x] ^= 1;
	}

	//區間操作轉爲點操作:對x的子樹做加權
	void add(int x, int val)
	{
		if (x == 0)return;
		v[x] += val;
		ad[x] += val;
	}

	//基本點操作:把x的信息從子節點處更新
	void pushup(int x)
	{
		sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + num[x];
	}

	//基本點操作:把x的延遲標記向下打
	void pushdown(int x)
	{
		if (x == 0)return;
		if (rev[x])
		{
			reverse(ch[x][0]);
			reverse(ch[x][1]);
			rev[x] = 0;
		}
		if (ad[x])
		{
			add(ch[x][0], ad[x]);
			add(ch[x][1], ad[x]);
			ad[x] = 0;
		}
	}

	//內存池初始化
	void init()
	{
		ID = 0; clear(0);
	}

	//旋轉操作:旋轉使得節點x與其父節點交換位置
	void rotate(int x)
	{
		int f = fa[x];
		int ff = fa[f];
		bool d = D(x);
		bool dd = D(f);
		setc(f, ch[x][!d], d);	//第一步:把與x原父節點同方向的子樹取作原父節點f的子樹
		setc(x, f, !d);			//第二步:使與x原父節點同方向的子樹連接原父節點f
		setc(ff, x, dd);		//第三步:使得x替代原父節點f,變爲新父節點點ff的子樹
	}

	//旋轉操作:把節點x旋轉到anc的左兒子位置,即rt位置。
	void splay(int x, int anc = 0)
	{
		if (x == 0)return;
		while (fa[x] != anc)
		{
			//pushdown(fa[fa[x]]);
			pushdown(fa[x]);
			pushdown(x);
			if (fa[fa[x]] != anc)rotate(D(x) == D(fa[x]) ? fa[x] : x);
			rotate(x);
		}
	}

	//查找操作:查找權值爲val的節點。查找不到返回0;查找到返回相應節點編號,並把該節點旋轉爲根節點
	int find(int val)
	{
		int x = rt;
		while (1)
		{
			if (x == 0)return 0;
			if (val == v[x]) { splay(x); return x; }
			bool d = val > v[x];
			x = ch[x][d];
		}
		splay(x);
	}

	//插入操作:插入權值爲val的節點。並把節點旋轉爲根節點
	void ins(int val)
	{
		int x = find(val);
		if (x)
		{
			num[x] += 1;
			sz[x] += 1;
			return;
		}
		int fa = anc;
		x = rt;
		bool d = 0;
		while (x)
		{
			fa = x;
			d = val > v[x];
			x = ch[x][d];
		}
		x = newnode(val);
		setc(fa, x, d);
		splay(x);
	}

	//查找操作:返回子樹x下最小節點(rev延遲標記與此同時生效)
	int first(int x)
	{
		pushdown(x);
		while (ch[x][0])x = ch[x][0], pushdown(x);
		return x;
	}

	//查找操作:返回子樹x下最大節點(rev延遲標記與此同時生效)
	int last(int x)
	{
		pushdown(x);
		while (ch[x][1])x = ch[x][1], pushdown(x);
		return x;
	}

	//查找操作:返回樹中第k小的節點(延遲標記與此同時生效)
	int kth(int k)
	{
		++k;	//這裏涉及到區間操作,我們在左右界各添加新節點,因此進入時要++k
		int x = rt;
		//assert(sz[x] >= k);
		while (1)
		{
			pushdown(x);
			if (sz[ch[x][0]] >= k)x = ch[x][0];
			else if (sz[ch[x][0]] + num[x] >= k)return x;
			else
			{
				k -= sz[ch[x][0]] + num[x];
				x = ch[x][1];
			}
		}
	}

	//查找操作:返回樹中[l,r]區間段的根節點
	int segment(int l, int r)
	{
		splay(kth(l - 1), anc);
		splay(kth(r + 1), rt);
		return keynode;
	}

	//刪除操作:刪除節點x。先把其旋轉爲根,然後合併左右子樹
	void del(int x)
	{
		splay(x);//轉到根後便不再需要pushdown()
		if (ch[x][0] == 0)setc(anc, ch[x][1], 0);	//如果沒有左子樹,則直接把右子樹放到樹根
		else
		{
			setc(anc, ch[x][0], 0);					//第一步:把左子樹放到樹根
			splay(last(ch[x][0]), anc);				//第二步:把左子樹最大節點轉到樹根
			setc(rt, ch[x][1], 1);					//第三步:把右子樹接到樹根上
		}
	}

	//分離操作:把[l,r]區間段從樹中分離
	int split(int l, int r)
	{
		int x = segment(l, r); fa[x] = 0;
		setc(ch[rt][1], 0, 0);
		pushup(rt);
		return x;
	}

	//合併操作,把子樹x插入到pos位置的右側
	void inspos(int x, int pos)
	{
		segment(pos + 1, pos);	//使得pos位於根,pos+1位於根的右子樹
		setc(ch[rt][1], x, 0);
		pushup(rt);
	}

	//遍歷操作:中序遍歷以x爲根的子樹
	void print(int x)
	{
		if (ch[x][0])print(ch[x][0]);
		printf("(節點%d)(左兒子%d)(右兒子%d)(子樹大小%d)\n", x, ch[x][0], ch[x][1], sz[x]);
		if (ch[x][1])print(ch[x][1]);
	}

	//畫樹程序
	const int diF[20] = { 0,10,9,8,7,6,5,4,3,2,1 };
	char mp[20][200];
	int ln[20];
	int maxdep;
	void draw(int x, int dep, int pos, int anc = 0)
	{
		//pushdown(x);
		gmax(maxdep, dep);
		int d = diF[dep]; if (ch[x][0] == 0 || ch[x][1] == 0)d = 3;
		if (ch[x][0])draw(ch[x][0], dep + 1, pos - d, anc);
		//print information
		while (ln[dep] < pos - 1)mp[dep][ln[dep]++] = ' ';
		int tmp = v[x];
		if (tmp < 0)mp[dep][ln[dep]++] = '-', tmp = -tmp;
		if (tmp >= 10)mp[dep][ln[dep]++] = tmp / 10 + 48; mp[dep][ln[dep]++] = tmp % 10 + 48;
		//print information
		if (ch[x][1])draw(ch[x][1], dep + 1, pos + d, anc);
	}
	void DRAW()
	{
		MS(ln, 0);
		MS(mp, 0);
		maxdep = 1;
		draw(rt, 1, 40);
		for (int i = 1; i <= maxdep; ++i)puts(mp[i]);
		puts("");
	}

	//具體程序的函數實現——
	void add(int l, int r, int val)
	{
		int x = segment(l, r);
		add(x, val);
		splay(x);
	}

	void reverse(int l, int r)
	{
		int x = segment(l, r);
		reverse(x);
		splay(x);
	}

	void solve()
	{
		char op[10]; int val;
		init();
		for (int i = 0; i <= n + 1; ++i)
		{
			if (i >= 1 && i <= n)scanf("%d", &val); else val = 0;
			int x = newnode(val);
			setc(x, rt, 0);
			setc(anc, x, 0);
		}
		//DRAW();
		while (m--)
		{
			scanf("%s", op);
			if (op[0] == 'a')
			{
				scanf("%d", &val);
				add(1, k2, val);
			}
			else if (op[0] == 'r')
			{
				reverse(1, k1);
			}
			else if (op[0] == 'i')
			{
				scanf("%d", &val);
				inspos(newnode(val), 1);
			}
			else if (op[0] == 'd')
			{
				del(kth(1));
			}
			else if (op[0] == 'm')
			{
				int g = sz[rt] - 2;
				scanf("%d", &val);
				if (val == 1)
				{
					int x = split(g, g);
					inspos(x, 0);
				}
				else if (val == 2)
				{
					int x = split(1, 1);
					inspos(x, g - 1);
				}
			}
			else if (op[0] == 'q')
			{
				printf("%d\n", v[kth(1)]);
			}
		}
	}
	/*合併兩棵平衡樹
	void merge(int anc1, int anc2)
	{
	int rt1 = ch[anc1][0];
	int rt2 = ch[anc2][0];
	if (sz[rt1] > sz[rt2]) swap(rt1, rt2), swap(anc1, anc2);
	f[anc1] = anc2;	//不要忘了集合合併
	int tim = sz[rt1];
	while (tim--)
	{
	int x = ch[anc1][0];
	del(x, anc1);
	setc(x, 0, 0);
	setc(x, 0, 1);
	ins(v[x], num[x], anc2);
	}
	}*/
}spt;

void datamaker()
{
	freopen("c://test//input.in", "w", stdout);
	for (int tim = 1; tim <= 1000; ++tim)
	{
		n = rand() % 10 + 2;
		m = rand() % 10;
		k1 = rand() % n + 1;
		k2 = rand() % n + 1;
		printf("%d %d %d %d\n", n, m, k1, k2);
		for (int i = 1; i <= n; ++i)printf("%d ", i); puts("");
		for (int i = 1; i <= m; ++i)
		{
			int op = rand() % 6 + 1;
			if (op == 1)
			{
				printf("add");
				int val = rand() % 10;
				printf(" %d\n", val);
			}
			else if (op == 2)puts("reverse");
			else if (op == 3)
			{
				printf("insert");
				int val = rand() % 10;
				printf(" %d\n", val);
			}
			else if (op == 4)puts("query");// puts("delete");
			else if (op == 5)
			{
				printf("move");
				int val = rand() % 2 + 1;
				printf(" %d\n", val);
			}
			else if (op == 6)puts("query");
		}
	}
	puts("0 0 0 0");
}

int main()
{
	while (~scanf("%d%d%d%d", &n, &m, &k1, &k2), n || m || k1 || k2)
	{
		printf("Case #%d:\n", ++casei);
		spt.solve();
	}
	return 0;
}
/*
【題目總結】
HDU4453
[題意]
有一個圓環,圓環上有一個指針,指針位置設置爲1號位置,其餘位置按照順時針方向標記爲2~x號位置
圓環上每個點有一個權值,指針位置可能發生順時針逆時針變化,節點狀態也有所修改。讓你動態維護這個過程。
[分析]
如果沒有插入刪除操作,我們可以用線段樹實現。
然而,在存在插入刪除的條件下,加上有指針的位移操作,我們用Splay解決問題

需要實現Splay的——
1,區間加(add(1, k2, val))
2,區間翻轉(reverse(1, k1))
3,單點插入(inspos(newnode(val), 1))
4,單點刪除(del(kth(1)))
5,單點移動(split(p,p),inspos())
6,單點詢問(v[kth(1)])

*/



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章