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線段樹的優點不僅在於常數小,空間小(對於一般情況下的寫法),而且好寫好調,是一種優秀的數據結構。它的本質是非遞歸式線段樹。希望這篇博客的內容對大家有幫助,滿意請在右下方點個贊,謝謝。