1. 程式人生 > >【詳解】樹狀陣列

【詳解】樹狀陣列

引入

如果給你n個數,然後進行q次詢問,每次詢問一個區間[x,y]的和,你會怎麼做? 
第一種方法:最簡單的方法,用陣列存起來,每次列舉x-y,ans加起來就可以,時間複雜度O(qn),十分慢。 
第二種方法:或許大多數人會使用字首和陣列:sum[i]=a[1]+a[2]+…+a[i],所以求[x,y]只需要輸出sum[y]-sum[x-1]即可,時間複雜度O(n),這是最快的方法之一了。


但是,如果加上一個條件:在q次詢問中,有可能會臨時使a[m]加上或減去一個數k(我們令這個為update(m,k)操作),也有可能會查詢一個區間的和,怎麼辦呢? 
如果還是用字首和陣列,就不方便了,因為update(m,k)需要更新sum[m]到sum[n]的值,於是時間複雜度又變為了O(qn)。 
那麼怎麼辦呢?於是有了樹狀陣列。

樹狀陣列

概念

樹狀陣列,時間複雜度log級別的資料結構,且實現複雜度極小,不論是上面提到的update操作還是求字首和。 
這裡寫圖片描述
如圖,A陣列是原始n個數的陣列,C陣列就是是樹狀陣列(“樹狀”陣列,是指一個普通陣列,按樹狀儲存,而不是一種STL中的資料結構)。

實現

觀察一下有什麼規律。

  • C[1] = A[1]
  • C[2] = C[1] + A[2] = A[1] + A[2]
  • C[3] = A[3]
  • C[4] = C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4]
  • C[5] = A[5]
  • C[6] = C[5] + A[6] = A[5] + A[6]
  • C[7] = A[7]
  • C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]

不難發現,好像和二進位制很有關係。

但是很難再想下去,事實上是這樣的: 
定義lowbit(x)為x二進位制下末尾0的個數。 
則含有C[i0]的C陣列中的位置有: 
i0 
i1 = i0 + lowbit(i0
i2 = i1 + lowbit(i1
i3 = 

i2 + lowbit(i2
… … 
ik = ik1 + lowbit(ik1
ikn) 
如果沒法理解,寫一個迴圈就懂了:

for(int i=x;i<=n;i+=lowbit(i))
  • 1

計算lowbit

lowbit(x)=x&-x 
為什麼?這裡複製了一篇證明(懶得打)

首先明白一個概念,計算機中-i=(i的取反+1),也就是i的補碼 
而lowbit,就是求(樹狀陣列中)一個數二進位制的1的最低位,例如01100110,lowbit=00000010;再例如01100000,lowbit=00100000。 
所以若一個數(先考慮四位)的二進位制為abcd,那麼其取反為(1-a)(1-b)(1-c)(1-d),那麼其補碼為(1-a)(1-b)(1-c)(2-d)。 
如果d為1,什麼事都沒有-_-|||但我們知道如果d為0,天理不容2Σ( ° △ °|||)︴ 
於是就要進位。如果c也為0,那麼1-b又要加1,然後又有可能是1-a……直到碰見一個為補碼為0的bit,我們假設這個bit的位置為x 
這個時候可以發現:是不是x之前的bit的補碼都與其自身不同?,x之後的補碼與其自身一樣都是0? 
例如01101000,反碼為10010111,補碼為10011000,可以看到在原來數正數第五位前,補碼的進位因第五位使其不會受到影響,於是0&1=0,; 
但在這個原來數“1”後,所有零的補碼都會因加1而進位,導致在這個“1”後所有數都變成0,再加上0&0=0,所以他們運算結果也都是零; 
只有在這個數處,0+1=1,連鎖反應停止,所以這個數就被確定啦O(∩_∩)O 
所以and以後只有x這個bit是一……

update操作

當要動態改變一個數時,用剛剛的迴圈枚舉出與它相關的位置,都增加(減少)即可:

void update(int k,int x)
{
    for(int i=k;i<=n;i+=lowbit(i))
        C[i]+=x;
}
  • 1
  • 2
  • 3
  • 4
  • 5

getsum操作

就是求字首和,同樣的,倒著進行剛剛的迴圈,累加路上的值即可:

int getsum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i))//i要大於0
        ans+=C[i];
    return ans;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

關於程式碼風格

樹狀陣列的update和getsum基本是通用的,建議不要自己改函式名,lowbit可以寫函式,也可以巨集定義:#define lowbit(x) (x&-x)

例題

Stars