以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)])
*/