1. 程式人生 > >『資料結構』線段樹

『資料結構』線段樹

線段樹原理

線段樹,類似區間樹,它在各個節點儲存一條線段(陣列中的一段子陣列),主要用於高效解決連續區間的動態查詢問題,由於二叉結構的特性,它基本能保持每個操作的複雜度為\(O(logn)\)

線段樹的每個節點表示一個區間,子節點則分別表示父節點的左右半區間,例如父親的區間是\([a,b]\),那麼\((c=(a+b)/2)\)左兒子的區間是\([a,c]\),右兒子的區間是\([c+1,b]\)

下面我們從一個經典的例子來了解線段樹,問題描述如下:從陣列a[0...n-1]中查詢某個陣列某個區間內的最小值,其中陣列大小固定,但是陣列中的元素的值可以隨時更新。

對這個問題一個簡單的解法是:遍歷陣列區間找到最小值,時間複雜度是\(O(n)\)

,額外的空間複雜度\(O(1)\)。當資料量特別大,而查詢操作很頻繁的時候,耗時可能會不滿足需求。

另一種解法:使用一個二維陣列來儲存提前計算好的區間\([i,j]\)內的最小值,那麼預處理時間為\(O(n^2)\),查詢耗時\(O(1)\), 但是需要額外的\(O(n^2)\)空間,當資料量很大時,這個空間消耗是龐大的,而且當改變了陣列中的某一個值時,更新二維陣列中的最小值也很麻煩。

我們可以用線段樹來解決這個問題:預處理耗時\(O(n)\),查詢、更新操作\(O(logn)\),需要額外的空間\(O(n)\)。根據這個問題我們構造如下的二叉樹

葉子節點是原始組數\(a\)中的元素
非葉子節點代表它的所有子孫葉子節點所在區間的最小值
例如對於陣列\([2, 5, 1, 4, 9, 3]\)

可以構造如下的二叉樹(背景為白色表示葉子節點,非葉子節點的值是其對應陣列區間內的最小值,例如根節點表示陣列區間\(a[0...5]\)內的最小值是1):

由於線段樹的父節點區間是平均分割到左右子樹,因此線段樹是完全二叉樹,對於包含\(n\)個葉子節點的完全二叉樹,它一定有\(n-1\)個非葉節點,總共\(2n-1\)個節點,因此儲存線段是需要的空間複雜度是\(O(n)\)。那麼線段樹的操作:建立線段樹、查詢、節點更新 是如何運作的呢(以下所有程式碼都是針對求區間最小值問題)?

對於線段樹我們可以選擇和普通二叉樹一樣的鏈式結構。由於線段樹是完全二叉樹,我們也可以用陣列來儲存,下面的討論及程式碼都是陣列來儲存線段樹,節點結構如下(注意到用陣列儲存時,有效空間為\(2n-1\)

,實際空間確不止這麼多,比如上面的線段樹中葉子節點\(1\)\(3\)雖然沒有左右子樹,但是的確佔用了陣列空間,實際空間是滿二叉樹的節點數目。

線段樹的程式碼實現

建線段樹的過程

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);
}