樹狀數組用來求區間元素和,求一次區間元素和的時間效率為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];