splay複習小記

2021-07-14 03:45:16 字數 4636 閱讀 2704

splay的原名是伸展樹,一種超級實用的資料結構,能快速地乾很多資料結構不能幹的事情。

很久以前就聽說過並且略微地學了一些,但是當時了解地並不是很多。

有些人把splay達成spaly叫做死吧你,(⊙﹏⊙)b

實質上他是乙個二叉搜尋樹,就是每個節點的左兒子在原序列中是在自己左邊的,右兒子在原序列中是在自己右邊的,構圖的方式有很多。

每乙個節點都可以儲存一些值,表示它的子樹中的資訊(比如說什麼最大值啊,和啊之內的)。

fo(i,1,n)

f[n+1]=root=n+2,t[n+2][0]=n+1;update(n+2);

初學者的構圖可以構成一條鏈。這樣很明顯左兒子在原序列上的位置是在自己左邊的了。

但是有乙個很奇妙的問題,為什麼從2號點開始建?為什麼建完點還要再向上多開乙個n+2號點?

其實這種打法可以避免後面的特判,現在的1號點和n+2號點是建立在首尾兩段的,如果不建立這兩個點2號點的兒子和n+1號點的父親都會指向0並傳遞資訊,但是首尾建立乙個虛點在修改操作中可以更方便的操作。比如說旋轉的時候要涉及到首尾的時候,如果沒有虛點,無法把首尾單獨放到乙個子樹中去。

其實建成一條鏈在後面的操作會很慢。

int build(int l,int r,int y)

root=build(0,n+1,0);

insert是建點操作,到後面再講。

這樣一開始就建成一顆二叉樹會比較快。

旋轉

首先講乙個重要的部分,就是旋轉。

其實splay這顆二叉樹的中序遍歷就是原序列,例如圖中的原序列就是:axbyc。現在我們要把x旋轉到y的位置上,但是不能改變這棵樹的中序遍歷(及在原序列的先後順序)。

bool son(int x)

void rotate(int x)

其實**很短(f[x]表示x的父親,tt[x][0]和tt[x][1]分別是x的左右兒子)。

函式son(x)的作用是分辨x是其父親的左兒子還是右兒子(左兒子是0,右兒子是1)。

當把x旋轉到y是,y就成為的x的右兒子,但是x原來的右兒子b就沒有地方放了。怎麼辦?我們發現在原序列中的順序是x < b < y, 所以b應該在x的右邊但是在y的左邊,所以現在y的左兒子應該是b右兒子不變。如圖所示。

**運用了乙個小技巧z和1-z剛好把0和1轉化,也可以打成z和z^1(^是xor,異或)。

將x節點旋轉為y節點的兒子

void splay(int x,int y)

if(!y)root=x;

}

remove是懶標記下傳,後面再講。

為什麼是把x旋轉為y的兒子,因為這樣更加方便的操作,比如說要對x的子樹進行操作,如果變成把x旋轉到y的位置會麻煩很多而且不方便打。

旋轉的思路:如果x和x的父親和x的爺爺是一條折線,那麼就旋轉成不是乙個折線,然後像上面將的旋轉一樣向上推進。具體的最好自己畫個圖,有利於理解。

一般像splay(x,0)就是把x旋轉為根節點,splay(x,y)就是把x旋轉為y的兒子(具體是左兒子還是有兒子根據原序列中的順序來定)

節點值的更新

void update(int x)
當x節點的子節點變動是就需要更新,具體更新的內容據題目而定。

對於一段節點進行操作

x=kth(root,l-1);splay(x,0);

y=kth(root,r+1);splay(y,x);

如果要對[l,r]這段區間進行操作。思路:先把這段區間同時放到一顆子樹上去且這可子樹沒有其他多餘的節點。

首先如果把l-1旋轉成根節點,那麼[l,n]的節點都會在l-1(及root)的左子樹上。然後再把r+1旋轉為l-1的兒子,因為r+1在序列中再l-1的右邊,所以r+1旋轉之後一定是l-1的右兒子,那麼在原序列中的順序大於l-1的,小於r+1一定都是現在r+1節點的左子樹了。

那麼現在只要對r+1的左子樹進行操作就好了。

printf("%d\n",t[tt[y][0]].mx);
比如要輸出[l,r]段的最大值,上段程式後面只用加上這段程式就可以了。

插入乙個或者一段節點

現在要把a陣列中的數從posi位置後開始插入進序列中。

參照對於一段節點進行操作

fo(i,1,k)scanf("%d",&a[i]);

x=kth(root,posi);splay(x,0);

y=kth(root,posi+1);splay(y,x);

tt[y][0]=build(1,k,y);

現在只需要把這k個數插進y的左子樹中就可以了。build就是上面構圖中的build,實質就是把a陣列1到k中的節點插為y的子樹。

刪除乙個或者一段節點

現在要從posi這個位置開始刪去k個節點。

參照對於一段節點進行操作

scanf("%d%d",&posi,&k);posi++;

x=kth(root,posi-1);splay(x,0);

y=kth(root,posi+k);splay(y,x);

del(tt[y][0]);

tt[y][0]=0;

update(y);

update(x);

這裡也同理,因為要從posi開始刪節點,序列的位置要比posi-1大,比posi+k小。

del函式是什麼呢?

void del(int x)
因為刪去了一些點,這些點原來的位置不能浪費在那裡,用乙個棧存起來,建點的之後再用。

建點操作

int insert(int x)
為了防止空間的浪費,如果還有刪除過得節點的位置空在那裡的話,就呼叫出來,否則就新建乙個位置。

區間的修改操作

例如從posi開始後的k個點都加上k,參照對於一段節點進行操作

x=kth(root,posi-1);splay(x,0);

y=kth(root,posi+k);splay(y,x);

change(tt[y][0],zhi);

update(y);update(x);

同理。

void change(int x,int y)
懶標記下傳
void down(int x)

}

void remove(int x,int y)while(x!=y);

while(d[0])down(d[d[0]--]);

}

這種東西支援區間修改的資料結構都要用到的,但是splay中的有所不同,因為只有在旋轉的之後才用的到,例如splay(x,y),所以需要把x到y的路徑上的標記都下傳。

區間翻轉操作

例如把x的子樹的區間翻轉。

void overturn(int x)
其實很簡單,只需要把所有節點的左右兒子調換即可。注意懶標記的biao要用^或者1-biao,因為如果某段區間被同時翻轉兩次相當於沒有翻轉。

查詢序列中第k個位置

int kth(int x,int k)
這個很簡單啦。

其實如果想知道第x節點在序列中的序號的話,可以把x旋轉到根(及splay(x,0)),然後t[tt[x][0]].size+1就是x在原序列中的序號。

區間分離

把x為根節點的這棵樹以原序列序號y為分水嶺,分成l和r兩顆子樹。

void split(int x,int y,int &l,int &r)
區間合併把以x為根的樹和以y為根的樹合併為樹l。

為什麼要找到x樹中第 size[x]大(及在原序列中序號最大的節點)的節點,因為在原序列中序號最大的節點沒有右兒子,方便合併。

void merge(int x,int y,int &l)
維護各種的樹比如說link_cut_tree(及lct或動態樹)……

目前也只知道這麼多了,但是這些操作在大部分題目中都夠用了。

【cqoi2014】排序機械臂

【noi2005】維護數列

什麼最大值,排序也可以用splay來練練手。

splay模板複習

splay模板 記得加哨兵!include using namespace std pre def const double pi acos 1.0 const int inf 0x3f3f3f3f typedef long long ll typedef unsigned long long ul...

演算法整理 複習 Splay

一 zig 此時節點 u 是 root v 是左孩子 右旋 v 是右孩子 左旋 二 zig zig v u 同側,先 u 再 v 此時節點 u 不是 root v 與 u 同為左孩子 右旋兩次 v 與 u 同為右孩子 左旋兩次 三 zig zag v u 異側,先 v 再 u 此時節點 u 不是 r...

伸展樹splay學習小記

splay是基本操作,rotate是splay的基本操作。splay now,root 把now splay到root的下面。單旋和雙旋就不說了。我們可以簡化操作,如果是一字型 方向相同 則先旋father,否則先旋now。然後再旋now。rotate now是指將now繞father rotate...