Splay模板 Splay題型大薈萃
阿新 • • 發佈:2018-12-23
以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)]) */