數據結構3——淺談zkw線段樹


MENU

1、建樹(普通)

2、普通操作*4

3、差分思想*5

本文作者frankchenfu,blogs網址http://www.cnblogs.com/frankchenfu/,轉載請保留此文字。

 

 線段樹是所有數據結構中,最常用的之一。線段樹的功能多樣,既可以代替樹狀數組完成“區間和查詢,也可以完成一些所謂“動態RMQ”(可修改的區間最值問題)的操作。其中,它們大部分都是由遞歸實現的,因此就有一些問題——棧空間占用大和常數大

  因此,從中便衍生了一種非遞歸式的線段樹(作者是THU的張昆瑋,參見他自己的PPT講稿《統計的力量-線段樹),命名為zkw線段樹。

  以下內容均用zkw線段樹保存區間最大值作為演示。如果代碼細節上有問題,請大家以自己寫的為准,也歡迎向我反饋。

1、建樹

  我們可以先觀察左邊面這張圖。這張圖本來是一張堆式的樹形圖,這里把它轉化成了二進制。從中,我們可以發現最底層的節點舍去最低位,也就是說向右移一位之后,就變成了他們的父節點。同理,第二層中的結點也可以通過相同的方式變成根節點。

  因此,我們在構建這棵樹時,就可以利用計算機的二進制建樹,達到快速簡單的目的。

 

 

  zkw線段樹的操作幾乎沒有出現遞歸,而是用循環代替。例如建樹操作(d數組存儲數值):

void build(int n)
{
    for(bit=1;bit<=n+1;bit<<=1);
    for(int i=bit+1;i<=bit+n;i++)
        scanf("%d",&d[i]);
    for(int i=bit-1;i;i--)
        d[i]=max(d[i<<1],d[i<<1|1]);
        //i<<1|1 = (i<<1)+1 = 2*i+1
}   

(這里解釋一下,bit表示非葉子節點,即倒二層及以上的節點數,每個節點保存的是它的值,如:和,最大值,最小值……

 

  而普通的線段樹建樹則類似於(代碼來自這里):

struct SegTreeNode
{
    int val;
}segTree[MAXNUM];//定義線段樹

void build(int root, int arr[], int istart, int iend)
{
    if(istart == iend)//葉子節點
        segTree[root].val = arr[istart];
    else
    {
        int mid = (istart + iend) / 2;
        build(root*2+1, arr, istart, mid);//遞歸構造左子樹
        build(root*2+2, arr, mid+1, iend);//遞歸構造右子樹
        //根據左右子樹根節點的值,更新當前根節點的值
        segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
    }
}

  很簡單的例子,說明了zkw線段樹不僅不需要遞歸,而且在代碼上也更簡潔。

2、普通操作

  既然是線段樹,那么就肯定能完成修改與查詢操作

2.1 單點修改——二進制思想的運用

  單點修改也不難,他的思想就是先把葉節點修改,然后依次維護父節點(把所有和它有關的的修改掉)。例如這樣:

void update(int x,int y)
{
    for(d[x+=bit]=y,x>>=1;x;x>>=1)
        d[x]=max(d[x<<1],d[x<<1|1]);
}

  這個代碼就更為簡短了(這里就不拿出來對比了)。

  當然,如果不是整個修改,而是加上或減去某數,只需要將for循環中的 d[x+=bit]=y 改為 d[x+=bit]+=y 即可(這里統一用整體修改作示范,下同)。

2.2 單點查詢——最簡單的查詢

  假設數組中有 x 個元素,二叉樹層數為 m,那么這 x 個元素在這個滿二叉樹中的編號就是$2^m$和$2^m+x-1$之間,即第x個元素就是$2^m+x-1$,訪問起來很方便。

2.3 區間查詢——單點查詢的升級版

  區間查詢也不難,規律同上,就是沿區間往上找。這里就直接上代碼。

int query(int s,int t)
{
    int ans=-1;
    for(s+=bit-1,t+=bit+1;s^t^1;s>>=1,t>>=1)
    {
        if(~s&1)
            ans=max(ans,d[s^1]);
        if(t&1)
            ans=max(ans,d[t^1]);
    }
    return ans;
}

2.4 區間修改——差分思想

   區間修改這時候看起來就很難辦了……呃,怎么辦呢??

  經過作者一個中午的實驗,發現,用上述代碼的思想似乎較難完成O($log_2$ n)級別的區間修改。這時候,翻開zkw神犇PPT講稿,發現……原來,可以用差分的思想。(事實上,在普通線段樹中,可以使用“懶標記”思想,不過限於作者水平,這里不再展開討論)

3、差分思想

差分?

  差分是化絕對為相對的重要手段。我們接下來,數組里的d值就不在存最大值$d_n$了,而是另外開個數組m,存$m_n = d_n - d_{\frac{n}{2}} $,讓每一個結點的值都是存他與他父親結點的差值。

有什么用嗎?

  當然有(不然說了干什么)!這時候,我們進行區間修改,就只需要修改$m_n$的值。

  這時候查詢可以完成嗎?可以。

  單點查詢就是在m數組中,從要查的結點一直查到根結點,再加上d數組的值,就可以找到答案(這個應該很好理解吧)。

小插曲

  然后,我們在寫代碼的時候會發現,如果我們把d數組初始化為0的話,那么所有的修改都記在數組m中,d數組的值會變嗎?不會。

  因此,我們干脆連值也不存了,把差分的“標記”直接當作值。於是,基本的差分思想就出來了。

  不過,值得一提的是,在常數上,差分的寫法可能更大一些(不一定會明顯優於遞歸版的普通線段樹)。

3.1 差分思想與建樹

 這時候,每個點就像前面說的,存差就好了。代碼如下,應該很好理解:

void build(int n)
{
    for(bit=1;bit<=n+1;bit<<=1);
    for(int i=bit+1;i<=bit+n;i++)
        scanf("%d",&d[i]);
    for(int i=bit-1;i;i--)
    {
        d[i]=min(d[i<<1],d[i<<1|1]);
        d[i<<1]-=d[i];d[i<<1|1]-=d[i];
    }
}

3.2 差分思想與單點修改

   你當然可以嘗試區間修改,然后用像 query(1,1,x) 這樣的方法修改。

不過完全沒有這個必要。

void update(int s,int t,int x)
{
    int tmp;
    for(d[s]+=x;s>1;s>>=1)
    {
        tmp=max(d[s],d[s^1]);d[s]-=tmp;d[s^1]-=tmp;d[s>>1]+=tmp;
        s>>=1;
    }        
}

3.3差分思想與單點查詢

   不得不承認,差分思想的運用,唯一一個不好的地方就是單點查詢從O(1)變為了O($log_2$ n),但是他可以幫助我們完成區間修改的操作,因此也只好忍受一下了。

 因為差分存儲方式的運用,相應的,這時候的代碼就成了這樣:

void query(int x)
{  
    int res=0;
    while(x)
        res+=d[x],x>>=1;
    return res;  
} 

 

3.4差分思想與區間修改

 就為了這個區間查詢,我們幾乎把內容翻了一倍——講差分存儲方式。而這種方式就是能夠讓我們完成區間修改。修改方式在上面介紹差分作用時提過了,這里就不在贅述了。代碼:

void update(int s,int t,int val)
{
    s+=bit;t+=bit;int tmp;
    if(s==t)
    {
        for(d[s]+=val;s>1;s>>=1)
        {
            tmp=min(d[s],d[s^1]);d[s]-=tmp;d[s^1]-=tmp;d[s>>1]+=tmp;
        }
        return;
    }
    for(d[s]+=val,d[t]+=val;s^t^1;s>>=1,t>>=1)
    {
        if(~s&1)d[s^1]+ =val;
        if(t&1) d[t^1]+=val;
        tmp=min(d[s],d[s^1]);d[s]-=tmp;d[s^1]-=tmp;
        d[s>>1]+=tmp;tmp=min(d[t],d[t^1]);
        d[t]-=tmp;d[t^1]-=tmp;d[t>>1]+=tmp;
    }
    for(;s>1;s>>=1)
    {
        tmp=min(d[s],d[s^1]);d[s]-=tmp;d[s^1]-=tmp;
     d[s
>>1]+=tmp; } return; }

 

3.5差分思想與區間查詢

 區間查詢?其實和之前沒用差分的差不多,只是把它求出來之后,再把值依層還原回去。

int query(int s,int t)
{
    int lans=0,rans=0,ans;
    if(s==t)
    {
        for(s+=bit;s;s>>=1)
            lans+=d[s];
        return lans;
    }
    for(s+=bit,t+=bit;s^t^1;s>>=1,t>>=1)
    {
        lans+=d[s];rans+=d[t];
        if(~s&1) lans=min(lans,d[s^1]);
        if(t&1) rans=min(rans,d[t^1]);
    }
    lans+=d[s];rans+=d[t];
    for(ans=min(lans,rans);s>1;)
        ans+=d[s>>=1];
    return ans;
}

 

至此,zkw線段樹的基本操作到這里就講完了。讓我們回顧一下,zkw線段樹的優點不僅在於常數小,空間小(對於一般情況下的寫法),而且好寫好調,是一種優秀的數據結構。它的本質是非遞歸式線段樹。希望這篇博客的內容對大家有幫助,滿意請在右下方點個贊,謝謝。


免責聲明!

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



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