深入理解樹狀數組


樹狀數組(Binary Indexed Tree(BIT), Fenwick Tree)是一個查詢和修改復雜度都為log(n)的數據結構。主要用於查詢任意兩位之間的所有元素之和,但是每次只能修改一個元素的值;經過簡單修改可以在log(n)的復雜度下進行范圍修改,但是這時只能查詢其中一個元素的值(如果加入多個輔助數組則可以實現區間修改與區間查詢)。

百度上給出了令人難以理解的概念,其實這個東西我也是琢磨了一天,參考了大量博客的筆記才搞清楚了大致思路和原理,說說心得吧!

假設數組a[1..n],那么查詢a[1]+...+a[n]的時間是log級別的,而且是一個在線的數據結構,支持隨時修改某個元素的值,復雜度也為log級別。
來觀察這個圖:
令這棵樹的結點編號為C1,C2...Cn。令每個結點的值為這棵樹的值的總和,那么容易發現:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
...
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
這里有一個有趣的性質:
設節點編號為x,那么這個節點管轄的區間為2^k(其中k為x二進制末尾0的個數)個元素。因為這個區間最后一個元素必然為Ax,
所以很明顯:Cn = A(n – 2^k + 1) + ... + An
算這個2^k有一個快捷的辦法,定義一個函數如下即可:
int lowerbit(int x)
{
return x&(x^(x–1));
}
 
利用機器補碼特性,也可以寫成:
int lowerbit(int x)
{
    return x&-x;
}
 
當想要查詢一個SUM(n )(求a[n]的和),可以依據如下算法即可:
step1: 令sum = 0,轉第二步;
step2: 假如n <= 0,算法結束,返回sum值,否則sum = sum + Cn,轉第三步;
step3: 令n = n – lowbit(n),轉第二步。
可以看出,這個算法就是將這一個個區間的和全部加起來,為什么是效率是log(n)的呢?以下給出證明:
n = n – lowbit(n)這一步實際上等價於將n的二進制的最后一個1減去。而n的二進制里最多有log(n)個1,所以查詢效率是log(n)的。
那么修改呢,修改一個節點,必須修改其所有祖先,最壞情況下為修改第一個元素,最多有log(n)的祖先。
所以修改算法如下(給某個結點i加上x):
step1: 當i > n時,算法結束,否則轉第二步;
step2: Ci = Ci + x, i = i + lowbit(i)轉第一步。
i = i +lowbit(i)這個過程實際上也只是一個把末尾1補為0的過程。
對於 數組求和來說樹狀數組簡直太快了!
注:
求lowbit(x)的建議公式:
lowbit(x):=x and -x;
或lowbit(x):=x and (x xor (x - 1));
lowbit(x)即為2^k的值。
上面的解釋可能會讓人產生疑惑,下面給出稍微容易理解的解釋吧!

先看兩幅圖(網上找的,如果雷同,不要大驚小怪~),下面的說明都是基於這兩幅圖的,左邊的叫A圖吧,右邊的叫B圖:

      是不是很像一顆樹?對,這就是為什么叫樹狀數組了~先看A圖,a數組就是我們要維護和查詢的數組,但是其實我們整個過程中根本用不到a數組,你可以把它當作一個擺設!c數組才是我們全程關心和操縱的重心。先由圖來看看c數組的規則,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必糾結怎么做到的,我們只要知道c數組的大致規則即可,很容易知道c8表示a1~a8的和,但是c6卻是表示a5~a6的和,為什么會產生這樣的區別的呢?或者說發明她的人為什么這樣區別對待呢?答案是,這樣會使操作更簡單!看到這相信有些人就有些感覺了,為什么復雜度被lg了呢?可以看到,c8可以看作a1~a8的左半邊和+右半邊和,而其中左半邊和是確定的c4,右半邊其實也是同樣的規則把a5~a8一分為二……繼續下去都是一分為二直到不能分,可以看看B圖。怎么樣?是不是有點二分的味道了?對,說白了樹狀數組就是巧妙的利用了二分,她並不神秘,關鍵是她的巧妙!

       她又是怎樣做到不斷的一分為二呢?說這個之前我先說個叫lowbit的東西,lowbit(k)就是把k的二進制的高位1全部清空,只留下最低位的1,比如10的二進制是1010,則lowbit(k)=lowbit(1010)=0010(2進制),介於這個lowbit在下面會經常用到,這里給一個非常方便的實現方式,比較普遍的方法lowbit(k)=k&-k,這是位運算,我們知道一個數加一個負號是把這個數的二進制取反+1,如-10的二進制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,繼續下面。介於下面討論十進制已經沒有意義(這個世界本來就是二進制的,人非要主觀的構建一個十進制),下面所有的數沒有特別說明都當作二進制。

       上面那么多文字說lowbit,還沒說它的用處呢,它就是為了聯系a數組和c數組的!ck表示從ak開始往左連續求lowbit(k)個數的和,比如c[0110]=a[0110]+a[0101],就是從110開始計算了0010個數的和,因為lowbit(0110)=0010,可以看到其實只有低位的1起作用,因為很顯然可以寫出c[0010]=a[0010]+a[0001],這就為什么我們任何數都只關心它的lowbit,因為高位不起作用(基於我們的二分規則它必須如此!),除非除了高位其余位都是0,這時本身就是lowbit。

既然關系建立好了,看看如何實現a某一個位置數據跟改的,她不會直接改的(開始就說了,a根本不存在),她每次改其實都要維護c數組應有的性質,因為后面求和要用到。而維護也很簡單,比如更改了a[0011],我們接着要修改c[0011],c[0100],c[1000],這是很容易從圖上看出來的,但是你可能會問,他們之間有申明必然聯系嗎?每次求解總不能總要拿圖來看吧?其實從0011——>0100——>1000的變化都是進行“去尾”操作,又是自己造的詞--'',我來解釋下,就是把尾部應該去掉的1都去掉轉而換到更高位的1,記住每次變換都要有一個高位的1產生,所以0100是不能變換到0101的,因為沒有新的高位1產生,這個變換過程恰好是可以借助我們的lowbit進行的,k +=lowbit(k)。

       好吧,現在更新的次序都有了,可能又會產生新的疑問了:為什么它非要是這種關系啊?這就要追究到之前我們說c8可以看作a1~a8的左半邊和+右半邊和……的內容了,為什么c[0011]會影響到c[0100]而不會影響到c[0101],這就是之前說的c[0100]的求解實際上是這樣分段的區間 c[0001]~c[0001] 和區間c[0011]~c[0011]的和,數字太小,可能這樣不太理解,在比如c[0100]會影響c[1000],為什么呢?因為c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的數變化並會直接作用於c[1000],因為它的尾部1不能一下在跳兩級在產生兩次高位1,是通過c[0110]間接影響的,但是,c[0100]卻可以跳一級產生一次高位1。

         可能上面說的你比較繞了,那么此時你只需注意:c的構成性質(其實是分組性質)決定了c[0011]只會直接影響c[0100],而c[0100]只會直接影響[1000],而下表之間的關系恰好是也必須是k +=lowbit(k)。此時我們就是寫出跟新維護樹的代碼:

1 void add(int k,int num)  
2 {  
3        while(k<=n)  
4         {  
5             tree[k]+=num;  
6             k+=k&-k;  
7         }  
8 }  

 

       有了上面的基礎,說求和就比較簡單了。比如求0001~0110的和就直接c[0100]+c[0110],分析方法與上面的恰好逆過來,而且寫法也是逆過來的,具體就不累述了:

 

 1 int read(int k)//1~k的區間和  
 2 {  
 3        int sum=0;  
 4         while(k)  
 5         {  
 6             sum+=tree[k];  
 7             k-=k&-k;  
 8         }  
 9         return sum;  
10 }  

下面給出一道模版題吧!

POJ 2352

Stars
Time Limit: 1000MS   Memory Limit: 65536K
Total Submissions: 45080   Accepted: 19567

Description

Astronomers often examine star maps where stars are represented by points on a plane and each star has Cartesian coordinates. Let the level of a star be an amount of the stars that are not higher and not to the right of the given star. Astronomers want to know the distribution of the levels of the stars.

For example, look at the map shown on the figure above. Level of the star number 5 is equal to 3 (it's formed by three stars with a numbers 1, 2 and 4). And the levels of the stars numbered by 2 and 4 are 1. At this map there are only one star of the level 0, two stars of the level 1, one star of the level 2, and one star of the level 3.

You are to write a program that will count the amounts of the stars of each level on a given map.

Input

The first line of the input file contains a number of stars N (1<=N<=15000). The following N lines describe coordinates of stars (two integers X and Y per line separated by a space, 0<=X,Y<=32000). There can be only one star at one point of the plane. Stars are listed in ascending order of Y coordinate. Stars with equal Y coordinates are listed in ascending order of X coordinate.

Output

The output should contain N lines, one number per line. The first line contains amount of stars of the level 0, the second does amount of stars of the level 1 and so on, the last line contains amount of stars of the level N-1.

Sample Input

5
1 1
5 1
7 1
3 3
5 5

Sample Output

1
2
1
1
0

Hint

This problem has huge input data,use scanf() instead of cin to read data to avoid time limit exceed.

Source

題意:
就是求每個小星星左小角的星星的個數。坐標按照Y升序,Y相同X升序的順序給出
由於y軸已經排好序,可以按照x坐標建立一維樹狀數組
 1 #include <stdio.h>
 2 #include <string.h>
 3 const int MAXN=32005;
 4 const int MINN=15005;
 5 int tree[MAXN];//下標為橫坐標
 6 int level[MINN];//下標為等級數
 7 /*int lowerbit(int x)
 8 {
 9     return x&-x;
10 }*/
11 void add(int k,int num)
12 {
13     while(k<=MAXN)
14     {
15         tree[k]+=num;
16         k+=k&-k;
17     }
18 }
19 int read(int k)//1~k的區間和
20 {
21     int sum=0;
22     while(k)
23     {
24         sum+=tree[k];
25         k-=k&-k;
26     }
27     return sum;
28 }
29 int main()
30 {
31     int n,x,y,i;
32     memset(tree,0,sizeof(tree));
33     memset(level,0,sizeof(level));
34     while(scanf("%d",&n)!=EOF)
35     {
36         for(i=1;i<=n;i++)
37         {
38             scanf("%d%d",&x,&y);
39             int temp=read(x+1);//加入x+1,是為了避免0,X是可能為0的
40             level[temp]++;
41             add(x+1,1);
42         }
43         for(i=0;i<n;i++)
44             printf("%d\n",level[i]);
45     }
46     return 0;
47 }
 
          

 

 
         
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM