平衡樹之splay講解


  首先來說是splay是二叉搜索樹,它可以說是線段樹和SBT的綜合,更可以解決一些二者解決不了的問題,splay幾乎所有的操作都是由splay這一操作完成的,在介紹這一操作前我們先介紹幾個概念和定義

  二叉搜索樹,即BST(binary search tree),這樣的樹有一個關鍵字,滿足對於每個節點來說,以該節點左兒子為根節點的子樹中的所有節點的關鍵字小於該節點的關鍵字,以該節點右兒子為根節點的子樹中的所有節點的關鍵字大於該節點的關鍵字。

  splay主要可以用來解決區間的維護問題

  假設我們需要維護一個數列,支持

  1.在數列第i位后插入一個長為l的數列

  2.在數列第i為后刪除一個長為l的數列 

  3.將數列的l r區間翻轉(1 2 3  2 3 翻轉后為 3 2 3 2 1)

  4.將數列的l r區間同時加上一個值

  5.將數列的l r區間同時改為一個值

  6.求數列的l r區間的和(最大值)

  其實線段樹上的大部分操作這里都支持,比如區間最大子區間和

 

  首先對於當前的樹,它的中序遍歷就是當前的區間,每個點的關鍵字(二叉搜索樹的那個)是內個點表示區間元素的標號,比如一個點的關鍵字是3,那么這個點代表區間中第3個元素,每個點除了關鍵字外還記錄了一個tree[i]代表這個點對應區間內的元素是什么。

  上圖(節點內的數代表tree值)的樹表示數列 3 7 1 4 2 -1

  對於每個節點的記錄內容為

  son[x,0..1]左右兒子

  father[x]父親節點

  還有我們定義root為當前樹的根節點,sroot為超級節點(-1),sroot只連接着root(其實就是定義了root的father為-1)

  那么我們首先建樹的時候,具體過程為

function build(l,r:longint):longint;
var
    mid                            :longint;
begin
    mid:=(l+r) div 2;
    tree[mid]:=a[mid];//a為區間的值
    if l<=mid-1 then 
    begin
          son[mid,0]:=build(l,mid-1);
          father[son[mid,0]]:=mid;
    end;
    if mid+1<=r then 
    begin
          son[mid,1]:=build(mid+1,r);
          father[son[mid,1]]:=mid;
    end;
    update(mid);//可暫時忽略
exit(mid);
end;

  那么我們現在有了一顆樹,我們還需要改變這棵樹的形態,就是splay(x,y)代表將編號為x的點旋轉到y的兒子處,那么我們就需要介紹一個旋轉操作了,在介紹旋轉操作之前還應該引入一個find操作,假設我們需要找區間內第i個元素,樹中代表這個點的編號是多少(每個點都有一個編號,編號隨意定,滿足互不相同就行了,類似於線段樹,SBT中的點的編號,沒有實際意義)我們規定一個點的size值為以該點為根節點的子樹的節點數,那么find(l)表示數列中第l個元素在樹中的編號。

function find(x:longint):longint;
var 
    t                        :longint;
begin
    t:=root;    
    while true do
    begin
        push_down(t);//可暫時忽略
        if size[son[t,0]]+1=x then exit(t);
        if size[son[t,0]]+1>x then t:=son[t,0]
        else
            begin
                dec(x,size[son[t,0]]+1);
                t:=son[t,1];
            end;
    end;
end;

  那么我們介紹旋轉過程rotate(x,y)代表將編號為x的節點旋轉到他的父親節點,就是如果x是左兒子就右旋father[x],右兒子就左旋father[x],y代表x是他父親的左節點(0)還是右節點(1)。

procedure rotate(x,y:longint);
var 
    f                        :longint;
begin
    push_down(x);
    f:=father[x];
    father[son[x,y xor 1]]:=f;
    son[f,y]:=son[x,y xor 1];
    if f=root then root:=x
    else
        if f=son[father[f],0] then 
            son[father[f],0]:=x else 
            son[father[f],1]:=x;
    father[x]:=father[f];
    father[f]:=x;
    son[x,y xor 1]:=f;
    update(f);
    update(x);
end;

  那么對於splay過程我們就可以理解了

procedure splay(x,y:longint);
var 
    u, v                    :longint;
begin
    while father[x]<>y do
        if father[father[x]]=y then 
            rotate(x,ord(x=son[father[x],1])) else
        begin
            if son[father[x],0]=x then u:=1 else u:=-1;
            if son[father[father[x]],0]=father[x] then v:=1 else v:=-1;
            if u*v=1 then
            begin
                rotate(father[x],ord(x=son[father[x],1]));
                rotate(x,ord(x=son[father[x],1]));
            end else
            begin
                rotate(x,ord(x=son[father[x],1]));
                rotate(x,ord(x=son[father[x],1]));
            end;
        end;
    update(x);
end;

  其中u=1代表x是父親的左節點,u=-1代表是右節點,v=1代表x父親是x爺爺的左節點,v=-1代表右節點

  那么v*u=1的情況就是x和父親,爺爺,祖孫三代是一條鏈(直觀的說)這種情況先旋父親,再旋x,否則旋兩次x,其實結果是一樣的,但是前人證明這樣操作會使splay樹更平衡些。

  那么剩下的操作就是基於這幾個操作的擴展了,比如添加區間,在l后加入長s的區間

for i:=n+1 to n+s do read(a[i]);
p:=build(n+1,n+s);//把這一區間建成一棵樹我們只需要插入p節點就行了
q:=find(l); splay(q,sroot);
q:=find(l+1); splay(q,root);
son[son[root,1],0]:=p;
father[p]:=son[root,1];
update(son[root,1]);
update(root);

  其中兩個find和splay操作是精華,我們先找到第l個元素,旋轉到根,再找到第l+1個元素,旋轉到根的右兒子,那么第l+1個節點是沒有左兒子的(因為當前以l為根,l+1元素左兒子代表比l大的,比l+1小的,顯然沒有),那么我們不是要在L后面插入區間么,就直接將p點當成l+1點的左兒子就行了。

  那么我們會發現,假如我要在區間的開頭插入區間怎么辦find(0)是沒有值的,那么我們就插入左右標兵,在最開始建樹的時候inc(n),root:=build(0,n);

  其實這樣多插入了兩個數,那么我們要find(l)時需要find(l+1),以后每次用find的時候+1就好了

  那么對於刪除操作假設刪除l r區間

p:=find(l); splay(p,sroot);
p:=find(r+2); splay(p,root);
son[son[root,1],0]:=-1;
update(son[root,1]);
update(root);

  我們將區間中第l-1個元素旋轉到根節點,r+1個元素旋轉到根節點的右兒子,那么以son[son[root,1],0]為根節點的子樹代表的就是區間l r,直接刪除就好,那么對於區間最大值操作,類似於線段樹就行了,因為旋轉后樹的結構已經改變了,那么我們需要維護節點存儲的信息,就是update操作

procedure update(x:longint);
begin
    sum[x]:=sum[son[x,0]]+tree[x]+sum[son[x,1]];
    size[x]:=size[son[x,0]]+1+size[son[x,1]];
    max[x]:=get_max(tree[x],get_max(max[son[x,0]],max[son[x,1]]));
end;

  對於區間賦值,修改這樣的,打標簽就好了,那么對於區間翻轉操作我們也可以打標簽,flag[x]為true代表以x為根節點的區間需要翻轉,那么我們旋轉一個區間的時候,假設根節點為x。

proceudre reverse(x:longint);
begin
     swap(son[x,1],son[x,0]);
     flag[son[x,1]]:=not flag[son[x,1]];
     flag[son[x,0]]:=not flag[son[x,0]];
end;    

  可以自己舉個例子,發現滿足這個性質

  push_down操作則為下放標簽

procedure push_down(x:longint);
var 
    l,r                        :longint;
begin
    l:=son[x,0];r:=son[x,1];
    if flag[x] then
        begin
            if l<>-1 then renew(l,0);
            if r<>-1 then renew(r,0);
            flag[x]:=false;
        end;
    if val[x]<>0 then
        begin
            if l<>-1 then renew(l,val[x]);
            if r<>-1 then renew(r,val[x]);
            val[x]:=0;
        end;
end;

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM