【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操作,輸出一行,回答提問。
輸入
輸出
對每個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