1. 程式人生 > >BIT二叉索引樹(樹狀陣列)

BIT二叉索引樹(樹狀陣列)

本文介紹BIT二叉索引樹這種資料結構的搭建和應用。該資料結構能在動態修改的陣列連續和查詢問題上有極其出色的表現。

POWERED BY PHANTOM_LSH
本文知識和程式碼(c++)風格來源於劉汝佳的《演算法競賽入門經典 訓練指南》


字首和

現在有如下一個簡單的問題:輸入n個數據,儲存在陣列a[n]中,要求多次查詢(sum(i , j))從i到j(即[i , j] )上所有元素的和。
由於查詢較多,迴圈顯然效率低下。可能大多數人都可以想到使用一個輔助陣列s計算每個元素的字首和(即s[x] = a[1]+a[2]+a[3]+......+a[x]),這樣,當需要計算sum(i , j)時只需要計算s[j] - s[i-1]即可。如果不明白這一點請認真思考並將它理解。

動態連續和查詢

    將上面的求連續和問題稍微改進一下,現在需要支援一種新的操作:add(x , d) 即把a[x]增加d。
    這樣一來,如果通過字首和的方式計算就不能簡化計算了,因為每次修改一個元素都要修改所有在它後面的字首和。有什麼解決辦法呢?我們需要用一種新的資料結構——BIT二叉索引樹(樹狀陣列)。

lowbit

    在介紹二叉索引樹之前必須要介紹這樣一個函式:lowbit(x),它返回的是正整數x的二進位制表示方法中最靠右的一個1所代表的數字(例如:lowbit(10) = 2,因為10的二進位制是1010,最靠右的一個1在第二位上,代表2)。
    其實,lowbit看似複雜其實程式碼實現異常簡單。即:
int lowbit ( int x){return x&-x;}
    它的原理其實是:在計算機內部,負數(-x)是x按位取反(1變0,0變1)然後加1之後的結果(這裡不討論為什麼),所以,-10的儲存其實是0110 。然後再進行與運算,就可以得到lowbit了。

BIT

     終於進入了正題。運用lowbit我們可以把從1到n一段數字沿lowbit從大到小一半一半的分割開。比如:1-10中,8的lowbit最大,所以1-7一段,9-10一段,以此類推。所以,好像線段樹那樣,一段連續的數字被不停地分割直到只剩下一個數字。
     我們把lowbit相同的節點放在同一層(沒有的補上去),這樣自然形成一棵結點數為大於n的 最小的 2的某整數次方-1 的一棵完全二叉樹,其根結點為lowbit最大的結點。通常我們把0也變成一個虛擬化的節點放在裡面,方便運算。

一個簡單的BIT示例
我們發現一個重要的結論:由於lowbit的定義,每個結點的兩個子結點與該結點的二進位制只有兩位不同,一位是該節點的lowbit,兩個子結點分別為0和1,另一位是子結點的lowbit,兩個子結點都為1但是該結點為0 。這樣的現象產生一個重要性質,即:對於一個結點x,它左上方(可以不是順著邊,而是首個向左上回溯的祖先結點)結點的編號為x-lowbit(x),而右上方(解釋同上)結點的編號為x+lowbit(x) 。請打草稿或者仔細思考一下這個結論。
因此,我們可以不要儲存這一棵樹,因為邊的關係可以直接計算。另:在BIT中只需要向上回溯。
一定要記住,BIT的結點儲存的是原來的序列的下標!切記!!

動態連續和

    那麼,BIT已經構造出來了,怎麼用來解決問題呢?
    現在,構造輔助陣列c[n](就像上面的s[n]一樣),c[x] = a[x-lowbit(x)+1]+a[x-lowbit(x)+2]+a[x-lowbit(x)+3] + ……+a[x](a陣列是原來的序列) 。這是什麼意思呢?請看圖:

BIT應用示例
每個結點都對應一個向左延伸的黃顏色的橫條。橫條所覆蓋的數對應的序列值的和記錄在c數組裡面,就好像字首和一樣。
那麼,c陣列有什麼用呢?
其實,如果要查詢一個結點x的字首和,只要不停地順著x = x-lowbit(x) 迭代到0,將沿途的C陣列的值全部相加就可以得到a[1]+a[2]+……+a[x]了。在上圖中,就是順著藍色的箭頭不停地向左上方移動,觀察一下,是不是通過黃色的橫條不重複不遺漏的覆蓋了所有的下標?這就是查詢操作了。
那麼增加的操作呢?觀察發現,只要沿著x = x+lowbit(x)不停地向右上方走(圖上的紅色箭頭),直到走出邊界,修改沿途的c值就可以了(使它們增加d)。因為能夠覆蓋到結點x的黃色橫條只有那些。修改操作也就這樣完成了!
最後再說一下預處理,預處理只要把a陣列和c陣列都清空,然後執行n次add操作,相當於從0開始add到原始資料就可以了!
看上去說的很多,其實程式碼實現很簡單,下面附上c++版本的sum函式(字首和)和add函式程式碼。

int sum(int x){ //求x字首和
        int ans = 0;
        while(x>0){ //迭代到虛擬結點
            ans+=c[x];
            x -= lowbit(x); //向左上方移動
        }
        return ans;
    }
    void add(int x, int d){ //修改操作
        while(x<=n){ //判斷是否超出邊界
            c[x]+=d;
            x+=lowbit(x); //向右上方走
        }
        return ;
    }
最終,要求[a , b]的連續和只需要求sum(b) - sum(a-1)就可以了!

時間複雜度分析

    由於二叉樹的基本特徵,很容易看出修改和查詢字首和兩個函式的時間複雜度都是O(logn),而預處理是n次add操作,所以預處理時間複雜度是O(nlogn) 在查詢量多時遠遠優於暴力演算法!

    好了,BIT二叉索引樹(樹狀陣列)就介紹到這裡,我只是寫下了我的理解,希望大家看了以後有所收穫!劉汝佳的書上有一道例題,可以直接搜尋LA 4329,供大家參考。
    謝謝閱讀!

POWERED BY PHANTOM_LSH