『資料結構』線段樹
線段樹原理
線段樹,類似區間樹,它在各個節點儲存一條線段(陣列中的一段子陣列),主要用於高效解決連續區間的動態查詢問題,由於二叉結構的特性,它基本能保持每個操作的複雜度為\(O(logn)\)。
線段樹的每個節點表示一個區間,子節點則分別表示父節點的左右半區間,例如父親的區間是\([a,b]\),那麼\((c=(a+b)/2)\)左兒子的區間是\([a,c]\),右兒子的區間是\([c+1,b]\)。
下面我們從一個經典的例子來了解線段樹,問題描述如下:從陣列a[0...n-1]中查詢某個陣列某個區間內的最小值,其中陣列大小固定,但是陣列中的元素的值可以隨時更新。
對這個問題一個簡單的解法是:遍歷陣列區間找到最小值,時間複雜度是\(O(n)\)
另一種解法:使用一個二維陣列來儲存提前計算好的區間\([i,j]\)內的最小值,那麼預處理時間為\(O(n^2)\),查詢耗時\(O(1)\), 但是需要額外的\(O(n^2)\)空間,當資料量很大時,這個空間消耗是龐大的,而且當改變了陣列中的某一個值時,更新二維陣列中的最小值也很麻煩。
我們可以用線段樹來解決這個問題:預處理耗時\(O(n)\),查詢、更新操作\(O(logn)\),需要額外的空間\(O(n)\)。根據這個問題我們構造如下的二叉樹
葉子節點是原始組數\(a\)中的元素
非葉子節點代表它的所有子孫葉子節點所在區間的最小值
例如對於陣列\([2, 5, 1, 4, 9, 3]\)
由於線段樹的父節點區間是平均分割到左右子樹,因此線段樹是完全二叉樹,對於包含\(n\)個葉子節點的完全二叉樹,它一定有\(n-1\)個非葉節點,總共\(2n-1\)個節點,因此儲存線段是需要的空間複雜度是\(O(n)\)。那麼線段樹的操作:建立線段樹、查詢、節點更新 是如何運作的呢(以下所有程式碼都是針對求區間最小值問題)?
對於線段樹我們可以選擇和普通二叉樹一樣的鏈式結構。由於線段樹是完全二叉樹,我們也可以用陣列來儲存,下面的討論及程式碼都是陣列來儲存線段樹,節點結構如下(注意到用陣列儲存時,有效空間為\(2n-1\)
線段樹的程式碼實現
建線段樹的過程
void build(int l, int r, int root)
{
if (l==r)
{
sum[root]=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,root<<1);
build(mid+1,r,root<<1|1);
pushup(root);
}
pushup過程
void push(int root)
{
sum[root]=sum[root<<1]+sum[root<<1|1];
}
查詢線段樹
int query(int ansl, int ansr, int l, int r,int root)
{
if (ansl<=l && r<=ansr) return sum[root];
int mid=(l+r)>>1;
int ans=0;
if (ansl<=mid) ans+=query(ansl,ansr,l,mid,root<<1);
if (ansr>mid) ans+=query(ansl,ansr,mid+1,r,root<<1|1);
return ans;
}
單節點更新
void update(int pos, int c, int l, int r, int root)
{
if (l==r)
{
sum[root]+=c;
return;
}
int mid=(l+r)/2;
if (pos<=mid) update(pos,c,l,mid,root<<1); else update(pos,c,mid+1,r,root<<1|1);
pushup(root);
}