從一道水題來從頭介紹樹狀數組

樹狀數組用來求區間元素和,求一次區間元素和的時間效率為O(logn)。特別用于在數組內的參數變換后,再次求和所使用的時間復雜度減少問題。
下面是一道水題:POJ上的stars,題目大意為:按照縱坐標y從小到大的順序依次輸入幾個星星的橫縱坐標,橫縱坐標小于等于該星星的星星數量即為該星星的等級,求每個等級的星星數量。
由于輸入的順序是按照縱坐標從小到大的順序(y相同時x的坐標也是由小到大輸出),那么一個星星的滿足條件的星星一定在輸入的這個星星的前方,故在這道題中,y坐標是沒有用處的,我們可以每次輸入一個x值,就判斷之前輸入過的數中,小于x的星星的個數,例如:輸入坐標:
5 1
1 2
2 2
3 2
那么第三個數據之前有第二和第三兩個符合條件(橫坐標是1,2都小于5),所以第三顆星星的等級是2,但是如果按照這種方法逐個統計的話,在星星的數量達到一定數量時,遍歷一次并且每個都判斷一次的話,是一定超時的,所以,可以想到,開辟一個數組ar,下標1:ar[1]代表的是在輸入到當前時,橫坐標為1的個數,ar[2]即是橫坐標為2的星星的個數,依次類推,加入當前輸入的橫坐標是4,那么結果就是ar[0]+ar[1]+ar[2]+ar[3]+arr[4](縱坐標按順序輸入,那么則表明已經輸出的星星縱坐標一定小于等于當前縱坐標,只要橫坐標滿足條件則整體一定滿足),結果保留,并將ar[4]++。
但是,如果當前輸入的橫坐標n達到了數萬位,那么還要繼續從ar[0]遍歷求和到ar[n]嗎?當然是行不通的,所以,又需要將該模型進行優化。
我們的方法是:如果每間隔一段距離,一個arr數空間存放的就是前面的一段數組的和是不是會簡便很多?那樣就不需要每次都使用遍歷所有的節點來求和了,至少會遍歷較少的數,那么,這就涉及到了樹狀數組,不妨回憶一下二叉樹的原型,最上面一個根結點,下面每個節點都展開分支,我們假設每個葉子節點代表ar[i],他們的根結點來記錄和,那么整棵樹的根節點就是和了,想想是不是方便了很多?


如果當前橫坐標是3,那么只要輸出C的值,這樣減少了很多遍歷的時間,但是同時存在的問題是,如果當前橫坐標是0的話,還可以輸出ar[0],一的話輸出A,那么如果2的話呢?輸出的又該怎么處理呢?還有應該怎樣在輸出的時候順利得找到A,或C點呢?樹狀數組的處理將會解決這些問題:
下面是從網上轉載的一些關于樹狀數組的詳細推斷,以二進制的形式方便的找到每一個范圍內的非葉子節點:

假設樹狀數組C[],序列為A[1]~A[8]
網絡上面都有這個圖,但是我將這個圖做了2點改進。


(1)圖中有一棵滿二叉樹,滿二叉樹的每一個結點對應A[]中的一個元素。
(2)C[i]為A[i]對應的那一列的最高的節點。
那么C[]如何求得?
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

以上只是枚舉了所有的情況,那么推廣到一般情況,得到一個C[i]的抽象定義:

因為A[]中的每個元素對應滿二叉樹的每個葉子,所以我們干脆把A[]中的每個元素當成葉子,那么:C[i]=C[i]的所有葉子的和。
把具體的數標上不知道有沒有好點


現在不得不引出關于二進制的一個規律:

先仔細看下圖:


將十進制化成二進制,然后觀察這些二進制數最右邊1的位置:

1 --> 00000001

2 --> 00000010

3 --> 00000011

4 --> 00000100

5 --> 00000101

6 --> 00000110

7 --> 00000111

8 --> 00001000


1的位置其實從我畫的滿二叉樹中就可以看出來。但是這與C[]有什么關系呢?

接下來的這部分內容很重要:
在滿二叉樹中,

以1結尾的那些結點(C[1],C[3],C[5],C[7]),其葉子數有1個,所以這些結點C[i]代表區間范圍為1的元素和;

以10結尾的那些結點(C[2],C[6]),其葉子數為2個,所以這些結點C[i]代表區間范圍為2的元素和;

以100結尾的那些結點(C[4]),其葉子數為4個,所以這些結點C[i]代表區間范圍為4的元素和;

以1000結尾的那些結點(C[8]),其葉子數為8個,所以這些結點C[i]代表區間范圍為8的元素和。

擴展到一般情況:
i的二進制中的從右往左數有連續的x個“0”,那么擁有2x個葉子,為序列A[]中的第i-2x+1到第i個元素的和。

終于,我們得到了一個C[i]的具體定義:
C[i]=A[i-2^x+1]+…+A[i],其中x為i的二進制中的從右往左數有連續“0”的個數。

利用樹狀數組求前i個元素的和S[i]
理解了C[i]后,前i個元素的和S[i]就很容易實現。
從C[i]的定義出發:

C[i]=A[i-2^x+1]+…+A[i],其中x為i的二進制中的從右往左數有連續“0”的個數。
我們可以知道:C[i]是肯定包括A[i]的,那么:
S[i]=C[i]+C[i-2^x]+…

也許上面這個公式太抽象了,因為有省略號,我們拿一個具體的實例來看:

S[7]=C[7]+C[6]+C[4]

因為C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

(1)i=7,求得x=0,那么我們求得了A[7];
(2)i=i-2^x=6,求得x=1,那么求得了A[6]+A[5];
(3)i=i-2^x=4,求得x=2,那么求得了A[4]+A[3]+A[2]+A[1]。

也就是說,每個C[i]所存放的是2^x個數據的和,或者說,每個C[i]內和的下標在 i~i-2x+1,下一個C[i-1]的第一個數為i-2x,C[i-1]=C[i-2x],將i=i-2x,不斷遞歸,結果則為總的和。

講到這里其實有點難度,因為S[i]的求法,如果要講清楚,那么得寫太多的東西了。所以不理解的同學,再反復多看幾遍。

從(1)(2)(3)這3步可以知道,求S[i]就是一個累加的過程,如果將2^x求出來了,那么這個過程用C++實現就沒什么難度。

現在直接告訴你結論:2^x=i&(-i)
證明:設A’為A的二進制反碼,i的二進制表示成A1B,其中A不管,B為全0序列。那么-i=A’0B’+1。由于B為全0序列,那么B’就是全1序列,所以-i=A’1B,所以:

i&(-i)= A1B& A’1B=1B,即2^x的值。

所以根據(1)(2)(3)的過程我們可以寫出如下的函數:

int Sum(int i) //返回前i個元素和
{
       int s=0;
       while(i>0)
      {
              s+=C[i];
              i-=i&(-i);
       }
       return s;
}

更新C[]
當有星星不斷輸入,那么A的數目也會隨之改變,所以,如果數組A[i]被更新了怎么辦?那么如何改動C[]?

如果改動C[]也需要O(n)的時間復雜度,那么樹狀數組就沒有任何優勢。所以樹狀數組在改動C[]上面的時間效率為O(logn),為什么呢?

因為改動A[i]只需要改動部分的C[]。這一點從圖中就可以看出來:


如上圖:
假如A[3]=3,接著A[3]+=1,那么哪些C[]需要改變呢?

答案從圖中就可以得出:C[3],C[4],C[8]。因為這些值和A[3]是有聯系的,他們用樹的關系描述就是:C[3],C[4],C[8]是A[3]的祖先。

那么怎么知道那些C[]需要變化呢?
我們來看“A”這個結點。這個“A”結點非常的重要,因為他體現了一個關系:A的葉子數為C[3]的2倍。因為“A”的左子樹和右子樹的葉子數是相同的。 因為2x代表的就是葉子數,所以C[3]的父親是A,A的父親是C[i+20],即C[3]改變,那么C[3+2^0]也改變。

我們再來看看“B”這個結點。B結點的葉子數為2倍的C[6]的葉子數。所以B和C[6+21]在同一列,所以C[6]改變,C[6+21]也改變。

推廣到一般情況就是:
如果A[i]發生改變,那么C[i]發生改變,C[i]的父親C[i+2^x]也發生改變。
這一行的迭代過程,我們可以寫出當A[i]發生改變時,C[]的更新函數為:

void Update(int i,int value)  //A[i]的改變值為value
{
       while(i<=n)
       {
              C[i]+=value;
              i+=i&(-i);
       }
}

經過以上推斷,我們便可以據此得到該題的具體代碼實現了:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <math.h>
#include <string.h>
#define lowbit(x)  (x&(-x))
#define MMAX 15005
#define NMAX 32005
using namespace std;

int C[NMAX];

int sum(int i)
{
    int ans=0;
    while (i>0)
    {
        ans+=C[i];
        i-=lowbit(i);
    }
}

void add(int num,int i)
{
    while (i<=NMAX)
    {
        C[i]+=num;
        i+=lowbit(i);
    }
}


void main()
{
    int arr[MMAX]={0};
    int i,j,k,n,m,x,y;
    scanf("%d",&n);
    m=n;
    while (n--)
    {
        scanf("%d%d",&x,&y);
        arr[sum(++x)]++;
        add(1,x);
    }
    for (i=0;i<=m;i++)
        printf("%d\n",arr[i]);
}

以上是一維樹狀數組的詳解,關于二維的樹狀數組,則與一維的相似,主要在于元素數組為一個二維的A[i][j],相對應的是,樹狀數組同樣對應的一個二維數組C[i][j],不同的是C[i][j]代表的是0到i行和0到j列的和。
下面是在網絡上找到的相關注解:

一維樹狀數組很容易擴展到二維,在二維情況下:
數組A[][]的樹狀數組定義為:
C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= y.
例:舉個例子來看看C[][]的組成。
設原始二維數組為:A[][]={{a11,a12,a13,a14,a15,a16,a17,a18,a19},
{a21,a22,a23,a24,a25,a26,a27,a28,a29},
{a31,a32,a33,a34,a35,a36,a37,a38,a39},
{a41,a42,a43,a44,a45,a46,a47,a48,a49}};
那么它對應的二維樹狀數組C[][]呢?
記: B[1]={a11,a11+a12,a13,a11+a12+a13+a14,a15,a15+a16,...}
這是第一行的一維樹狀數組
B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...}
這是第二行的一維樹狀數組
B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...}
這是第三行的一維樹狀數組
B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...}
這是第四行的一維樹狀數組
那么:
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a1
6,...
這是A[][]第一行的一維樹狀數組
C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a2
2+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,...
這是A[][]數組第一行與第二行相加后的樹狀數組
C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a3 6,...
這是A[][]第三行的一維樹狀數組
C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,...
這是A[][]數組第一行+第二行+第三行+第四行后的樹狀數組
搞清楚了二維樹狀數組C[][]的規律了嗎?
仔細研究一下,會發現:

(1)在二維情況下,如果修改了A[i][j]=delta,
則對應的二維樹狀數組更新函數為:
private void Modify(int i, int j, int delta){
A[i][j]+=delta;
for(int x = i; x< A.length; x += lowbit(x))
for(int y = j; y <A[i].length; y += lowbit(y)){
C[x][y] += delta; } }

(2)在二維情況下,求子矩陣元素之和∑ a[i]j的函數為
int Sum(int i, int j){
int result = 0;
for(int x = i; x > 0; x -= lowbit(x)) {
for(int y = j; y > 0; y -= lowbit(y)) {
result += C[x][y]; } }
return result; }
比如:
Sun(1,1)=C[1][1]; Sun(1,2)=C[1][2]; Sun(1,3)=C[1][3]+C[1][2];...
Sun(2,1)=C[2][1]; Sun(2,2)=C[2][2]; Sun(2,3)=C[2][3]+C[2][2];...
Sun(3,1)=C[3][1]+C[2][1]; Sun(3,2)=C[3][2]+C[2][2];

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容