1. 程式人生 > >『資料結構』樹狀陣列

『資料結構』樹狀陣列

樹狀陣列的問題模型:

現在有一個這樣的問題:

有一個數組\(a\),下標從\(0\)\(n-1\),現在你要進行\(w\)次修改,\(q\)次查詢。
修改是修改陣列中某一個元素的值;
查詢是查詢陣列中任意一個區間的和,\(w+q<500000\)
這個問題很普遍,首先分析下樸素做法的時間複雜度,
修改是\(O(1)\)的時間複雜度,
而查詢就要\(O(n^2)\)的複雜度,總體時間複雜度為\(O(q*q*n*n)\)
你或許會想到用字首和來優化這個查詢,
我們再來分析下,查詢的話是\(O(1)\)的複雜度,
但是修改的時候修改一個點,那麼在之後的所有字首和都要更新,
所以修改的時間複雜度是\(O(n^2)\)

,總體時間複雜度還是\(O(q*q*n*n)\)
可以發現,兩種做法中,要麼查詢是\(O(1)\),修改是\(O(n^2)\)
要麼修改是\(O(1)\),查詢是\(O(n^2)\)
有沒有一種做法可以降低時間複雜度呢?樹狀陣列。
我們先來了解下\(lowbit\)這個函式,
你也先不要問這個函式到底在樹狀陣列中有什麼用;
\(lowbit\)這個函式的功能就是求某一個數的二進位制表示中最低的一位1,

\(for\) \(example\)\(x=6\),它的二進位制為\(110\)
那麼\(lowbit(x)\)就返回\(2\),因為最後一位\(1\)表示\(2\)
我們怎麼求\(lowbit\)

呢?

求負數的補碼的簡便方法:

先把這個數的二進位制寫出來,然後從右向左找到第一個\(1\),這個\(1\)不要動和這個\(1\)右邊的二進位制不變,左邊的二進位制依次取反,這樣就求出的一個數的補碼,說這個方法主要是讓我們理解一個負數的補碼在二進位制上的特徵,然後我們把這個負數對應的正數與該負數與運算一下,由於這個\(1\)的左邊的二進位制與正數的原碼對應的部分是相反的,所以相與一定都為\(0\),由於這個\(1\)和這個\(1\)右邊的二進位制都是不變的,因此,相與後還是原來的樣子,

所以,這個得出的結果就是\(lowbit(x)\)的結果。

lowbit函式:

int lowbit(x) 
{   
    return x & -x;
}

二進位制的視角:一個數\(n\),假設\(n=6\),它的二進位制為\(110\),我們把它表示成累加的形式\(110=100+10\),這樣是可以的,那麼我們要求前\(6(110)\)項的和可以這樣求:

\(∑i=16=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])\)

注意括號中的元素個數,
是不是\(4(100)\)個加\(2(10)\)個,
\(110=100+10\)是不是很像,
\(10\)就是\(lowbit(110)\)的結果,\(100\)\(lowbit(100)\)的結果。
求和的時候我們總是把\(∑ni=1\)\(∑i=1n\)拆分成這樣的幾段區間和來計算,
區間的起點和長度就是根據\(n\)的二進位制來的,
二進位制怎麼拆的,你就怎麼拆分,而拆分二進位制就要用到上面說的\(lowbit\)函數了。
這裡也可以順理成章得給出\(c\)陣列的表示了。
\(c[i]\)表示從第i個元素向前數\(lowbit(i)\)個元素,這一段的和,這就是上面說的區間和,只不過這個區間是靠右端點的;你可能又想問,不是說區間是靠右端點的嗎,是字尾和啊,那中間的這些區間怎麼定義?其實遞迴定義就好了,比如說:

\(∑6i=1=(a[1]+a[2]+a[3]+a[4]+(a[5]+a[6])=\)

\(∑6i=1=(a[1]+a[2]+a[3]+a[4])+c[6]\)

\(∑i=16=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])=\)

\(∑i=16=(a[1]+a[2]+a[3]+a[4])+c[6];\)

你可以把\(c[6]\)去掉,就變成了

\(∑4i=1=(a[1]+a[2]+a[3]+a[4])\)

\(∑i=14=(a[1]+a[2]+a[3]+a[4])\)

這個區間就靠右端點了,

\(∑4i=1=c[4]=c[6-lowbit(6)]\)

\(∑i=14=c[4]=c[6-lowbit(6)]\)

設計一種資料結構,需要的操作無非就是更改和查詢,
這裡只討論查詢和修改操作具體是怎麼實現的;

查詢

這裡說的查詢是查詢任一區間的和,由於區間和具有可加減性,所以轉化為求字首和;
查詢字首和就是把大區間分成幾段長度不等的小區間,然後再求和。
區間的個數為\(O(logn*logn)\)
所以查詢的時間複雜度為\(O(logn*logn)\)

修改

修改某一位置上的元素的時間複雜度為\(O(1)\)
但是要更新\(c\)陣列,不然查詢的時間複雜度就會變高。
更新的方法就要提一下樹狀陣列的性質了和樹狀陣列那張經典的圖片了。

圖片中已經把\(c\)陣列的字尾和這個含義已經表達得很清楚了。
這個時候你再把查詢操作對應到這張圖上,
然後根據二進位制來操作,
就可以很直白地理解上面所說的查詢操作了!

樹狀陣列的程式碼實現

對某個元素進行加法操作:

void update(int x)
{
    while(x<=n)
    {
        c[i]+=x;
        x+=lowbit(x);
    }
}

查詢字首和:

int sum(int x)
{
    int res=0;
    while (x>0)
    {
        res+=c[x];
        x-=lowbit(x);
    }
    return res;
}

洛谷樹狀陣列題:

【模板】樹狀陣列 1:p3374

【模板】樹狀陣列 2:p3368

中位數:p1168

逆序對:p1908

虔誠的墓主人:p2154

無盡的生命:p2448

推銷員:p2672

上帝造題的七分鐘:p4514