[SinGuLaRiTy] ZKW線段樹


【SinGuLaRiTy-1007】 Copyrights (c) SinGuLaRiTy 2017. All Rights Reserved.

關於ZKW線段樹

Zkw線段樹是清華大學張昆瑋發明非遞歸線段樹的寫法。實踐證明,這種線段樹常數更小,速度更快,寫起來也並不復雜。

建樹

ZKW線段樹本質上就是依賴於滿二叉樹中父節點與子節點的編號關系。

如上圖中的一個簡單的滿二叉樹,我們可以發現如下規律:

1>父子節點編號關系: 假設父節點的編號為 n ,那么,它的兩個子節點的編號就分別為 n*2(n<<1)、n*2+1(n<<1|1);

2>二叉樹層數與底層葉子節點數的關系:假設這個二叉樹的層數為 m ,那么,這個二叉樹的底層葉子節點數(由於是滿二叉樹,這也就是所有的葉子節點了)就是2^(m-1),同時,我們還可以知道,所有葉子節點中編號最小的,即在這個滿二叉樹左下角的葉子節點的編號也為 2^(m-1);

通過以上的兩大關系,我們在存儲一個數組的初始數據時,就可以直接將初始數據存儲在滿二叉樹的底層。假設數組中有 x 個元素,那么這 x 個元素在這個滿二叉樹中的編號就是2^(m-1)~2^(m-1)+x-1,訪問起來就很方便了。

於是就有了建樹代碼:(其中n代表的是初始數組中的元素個數,M代表的是最底層的葉子節點個數)

inline void Build()
{
    for(M=1;M<n;M<<=1) ;//由於要構建一個滿二叉樹,所以我們不能直接讓二叉樹的葉子節點數等於元素個數,M可能會大於n;本層循環使底層葉子節點數在滿足“滿二叉樹的前提下最小”
    for(int i=M;i<n+M;i++)//由於M也同樣是本層最左側葉子節點的編號,所以直接從這里開始賦值
        Tree[i]=Read();
}

(有些博客總是會在這里自問自答“建完了嗎?沒有。”,對於這種有點SB的行為,我表示無法理解)

不過確實,到目前為止,建樹還未完成,我們還需要從下往上更新其它節點的值。當然,知道了父子節點編號的關系,這個操作就非常好用了。

inline void upgrade()
{
    for(int i=M-1;i;i--)
    {
        Tree[i]=Tree[i<<1|1]+Tree[i<<1];//維護為區間和
    }
}

當然,你也可以將其維護為最大值,最小值之類的,代碼都大同小異:

<最大值>

Tree[i]=max(Tree[i<<1|1],Tree[i<<1]);

<最小值>

Tree[i]=min(Tree[i<<1|1],Tree[i<<1]);

到目前為止,我們才算是完成了ZKW線段樹的建樹工作。

<ZKW線段樹中的差分思想>

在建ZKW線段樹的過程中,可以用的Tree[i]表示父子節點的差值,也同樣可以達到存儲數據的目的。

void Build(int n)
{ 
    for(M=1;M<=n+1;M<<=1);
    for(int i=M;i<M+n;i++)
        Tree[i]=in();
    for(int i=M-1;i;--i) 
        Tree[i]=min(Tree[i<<1],Tree[i<<1|1]),Tree[i<<1]-=Tree[i],Tree[i<<1|1]-=Tree[i];
}

覺得稍微復雜了一些?但這樣的存儲方式可以為RMQ問題做准備。

<關於空間>

我們都知道,在建線段樹時,需要開的數組(或是結構體)的大小是 4n ;在這里 , 我們來計算一下ZKW線段樹的所需要的空間。(設初始數據中元素個數為 n )

最好的情況: 當 n=2^k 時,由於此時剛好可以把最底層排滿,則數組大小大概為 2n ;

最壞的情況: 當 n=2^k+1時,即底層剛好多出一個,仍需要把底層排滿時,則數組大小大概為 4n-1 ;

因此,即使是最壞的情況,ZKW線段樹也比普通線段樹的空間表現要好。

單點查詢

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

<單點查詢-差分版>

其實差分版單點查詢寫起來也不是很復雜,也比較利於理解。

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

區間查詢

<區間求和>

先看一下代碼:

int Sum(int s,int t,int Ans=0)
{ 
    s+=M-1,t+=M-1;
    Ans+=d[s]+d[t]; 
    if(s==t)
        return Ans-=d[s];
    for(;s^t^1;s>>=1,t>>=1)//s^t^1 : 如果s與t在同一個父親節點以下,就說明我們已經累加到這棵樹的根部了。當s與t在同一個父親節點下時,t-s=1,那么s^t=1,s^t^1=0,此時就退出循環。
    {
        if(~s&1)//這里在下面解釋
            Ans+=d[s^1]; //d[s^1]是d[s]的兄弟
        if(t&1)//這里在下面解釋
            Ans+=d[t^1];//d[t^1]是d[t]的兄弟
    }
    return Ans; 
}

<*關於代碼中的 ~s&1 與 t&1>

首先我們可以將這兩個位運算式轉化為好理解一點的式子:

if(~s&1) ->  if(s%2==0)

if(t&1) -> if(t%2!=0)
也就是說,這里是在判斷奇偶,結合滿二叉樹的編號規律我們很容易發現:若編號為奇,則為右兒子;若編號為偶,則為左兒子。那么,這里判斷左/右兒子有什么用呢?

我們看上面的這幅圖。如果我們知道要查詢的區間的兩個端點為編號4、7的節點,由於這是滿二叉樹,因此我們可以在圖中尋找位於4號節點右邊且位於7號節點左邊的節點,這些節點一定位於我們要查詢的區間之中。而我們又知道,在兩個兄弟節點A,B之中,若A為左兒子,那么B一定在A的右邊;若A為右兒子,那么B一定在A的左邊。也就是說,如果我們知道區間的兩個端點是左兒子還是右兒子,我們就可以知道它們的兄弟節點是否在區間的覆蓋范圍之內。又由於在ZKW線段樹中,我們已經將父節點維護成為包含其子節點信息的節點,因此不用擔心有漏算的情況。(要注意是開區間還是閉區間)

我們不妨畫個圖來驗證一下:

(注:圖中的紅點為累加過的點,橙色為訪問過的點)

圖中的累加節點覆蓋了所有的查詢范圍。

<區間查詢最大值>

和 區間求和 的代碼思路差不多,直接上代碼:

void Sum(int s,int t,int L=0,int R=0)
{ 
    for(s=s+M-1,t=t+M-1;s^t^1;s>>=1,t>>=1)
    { 
        L+=d[s],R+=d[t]; 
        if(~s&1) L=max(L,d[s^1]);
        if(t&1) R=max(R,d[t^1]); 
    } 
    int res=max(L,R);
    while(s) res+=d[s>>=1]; 
}

<區間查詢最小值>

void Add(int s,int t,int v,int A=0)
{
    for(s=s+M-1,t=t+M-1;s^t^1;s>>=1,t>>=1)
    {
        if(~s&1) d[s^1]+=v;
        if(t&1) d[t^1]+=v;
        A=min(d[s],d[s^1]);d[s]-=A,d[s^1]-=A,d[s>>1]+=A;
        A=min(d[t],d[t^1]);d[t]-=A,d[t^1]-=A,d[t>>1]+=A;
    }
    while(s) A=min(d[s],d[s^1]),d[s]-=A,d[s^1]-=A,d[s>>=1]+=A;
}

單點更新

void Change(int x,int v) 
{ 
    d[x=M+x-1]+=v; 
    while(x) 
        d[x>>=1]=d[x<<1]+d[x<<1|1];
 }

區間更新

舉個模板題例子。結合題目來看看代碼吧。

區間修改的RMQ問題

題目描述

給出N(1 ≤ N ≤ 50,000)個數的序列A,下標從1到N,每個元素值均不超過1000。有兩種操作:

(1)  Q i j:詢問區間[i, j]之間的最大值與最小值的差值

(2) C i j k:將區間[i, j]中的每一個元素增加k,k是一個整數,k的絕對值不超過1000。

一共有M (1 ≤ M ≤ 200,000) 次操作,對每個Q操作,輸出一行,回答提問。

輸入

第1行:2個整數N, M
第2行:N個元素
接下來M行,每行一個操作

輸出

對每個Q操作,在一行上輸出答案

樣例輸入 樣例輸出
5 4
1 2 3 4 5
Q 2 4
C 1 1 1
C 1 3 2
Q 1 5
2
1

 

 

 

 

 

 

代碼:

 

#include<cstdio>
#include<algorithm>
using namespace std;
 
#define lson pos << 1
#define rson pos << 1 | 1
#define fa(x) (x >> 1)
const int MAXN = 50000;
int d1[MAXN << 2], d2[MAXN << 2], M = 1, n, m;
// d1 -> max // d2 -> min
 
inline void Read(int &Ret){
    char ch; int flg = 1;
    while(ch = getchar(), ch < '0' || ch > '9')
        if(ch == '-') flg = -1;
    Ret = ch - '0';
    while(ch = getchar(), ch >= '0' && ch <= '9')
        Ret = Ret * 10 + ch - '0';
    Ret *= flg;
}
 
void build(int n){
    while(M < n) M <<= 1;
    int pos = M --;
    while(pos <= M + n){
        Read(d1[pos]);
        d2[pos] = d1[pos];
        pos ++;
    }
    pos = M;
    while(pos){
        d1[pos] = max(d1[lson], d1[rson]);
        d2[pos] = min(d2[lson], d2[rson]);
        d1[lson] -= d1[pos]; d1[rson] -= d1[pos];
        d2[lson] -= d2[pos]; d2[rson] -= d2[pos];
        pos --;
    }
}
 
inline void Insert(int L, int R, int v){//區間更新
    L += M; R += M;
    int A;
    if(L == R){
        d1[L] += v; d2[L] += v;
        while(L > 1){
            A = max(d1[L], d1[L ^ 1]); d1[L] -= A; d1[L ^ 1] -= A; d1[fa(L)] += A;
            A = min(d2[L], d2[L ^ 1]); d2[L] -= A; d2[L ^ 1] -= A; d2[fa(L)] += A;
            L >>= 1;
        }
        return;
    }
    d1[L] += v; d1[R] += v; d2[L] += v; d2[R] += v;
    while(L ^ R ^ 1){
        if(~L & 1) d1[L ^ 1] += v, d2[L ^ 1] += v;
        if(R & 1) d1[R ^ 1] += v, d2[R ^ 1] += v;
        A = max(d1[L], d1[L ^ 1]); d1[L] -= A; d1[L ^ 1] -= A; d1[fa(L)] += A;
        A = max(d1[R], d1[R ^ 1]); d1[R] -= A; d1[R ^ 1] -= A; d1[fa(R)] += A;
        A = min(d2[L], d2[L ^ 1]); d2[L] -= A; d2[L ^ 1] -= A; d2[fa(L)] += A;
        A = min(d2[R], d2[R ^ 1]); d2[R] -= A; d2[R ^ 1] -= A; d2[fa(R)] += A;
        L >>= 1; R >>= 1;
    }
    while(L > 1){
        A = max(d1[L], d1[L ^ 1]); d1[L] -= A; d1[L ^ 1] -= A; d1[fa(L)] += A;
        A = min(d2[L], d2[L ^ 1]); d2[L] -= A; d2[L ^ 1] -= A; d2[fa(L)] += A;
        L >>= 1;
    }
}
 
inline int getans(int L, int R){
    L += M; R += M;
    int ans1 = 0, ans2 = 0;
    if(L == R){
        while(L){
            ans1 += d1[L]; ans2 += d2[L];
            L >>= 1;
        }
        return ans1 - ans2;
    }
    int l1 = 0, r1 = 0, l2 = 0, r2 = 0;
    while(L ^ R ^ 1){
        l1 += d1[L]; r1 += d1[R];
        l2 += d2[L]; r2 += d2[R];
        if(~L & 1) l1 = max(l1, d1[L ^ 1]), l2 = min(l2, d2[L ^ 1]);
        if(R & 1) r1 = max(r1, d1[R ^ 1]), r2 = min(r2, d2[R ^ 1]);
        L >>= 1; R >>= 1;
    }
    l1 += d1[L]; r1 += d1[R]; l2 += d2[L]; r2 += d2[R];
    ans1 = max(l1, r1); ans2 = min(l2, r2);
    while(L > 1){
        L >>= 1;
        ans1 += d1[L]; ans2 += d2[L];
    }
    //printf("max=%d min=%d\n",ans1, ans2);
    return ans1 - ans2;
}
 
int main(){
    int a, b, c;
    char id[3];
    Read(n); Read(m);
    build(n);
    while(m --){
        scanf("%s",id);
        Read(a); Read(b);
        switch(id[0]){
            case 'C': Read(c), Insert(a, b, c); break;
            default: printf("%d\n",getans(a, b));
        }
    }
    return 0;
}

 

 

Time: 2017-03-11

 


免責聲明!

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



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