夜深人靜寫算法(三)- 樹狀數組

目錄  

一、從圖形學算法說起
      1、Median Filter 概述
      2、r pixel-Median Filter 算法
      3、一維模型
      4、數據結構的設計
      5、樹狀數組華麗登場

二、細說樹狀數組
       1、樹 or 數組?
       2、結點的含義
       3、求和操作
       4、更新操作
       5、lowbit函數O(1)實現
       6、小結

三、樹狀數組的經典模型
      1、PUIQ模型
      2、IUPQ模型
      3、逆序模型
      4、二分模型
      5、再說Median Filter
      6、多維樹狀數組模型
         
四、樹狀數組題集整理

一、從圖形學算法說起
      1、Median Filter概述
      Median Filter 在現代的圖形處理中是非常基礎並且廣泛應用的算法 (翻譯叫 中值濾波器,爲了不讓人一看到就覺得是高深的東西,還是選擇了使用它的英文名,更加能讓人明白是個什麼東西) ,如圖一-1-1,基本就能猜到這是一個什麼樣的算法了,可以簡單的認爲是PS中的"模糊"那個操作。
      
圖一-1-1
      2、r pixel-Median Filter算法
      首先對於一張寬爲W,高爲H的圖片,每個像素點存了一個顏色值,這裏爲了把問題簡化,先討論黑白圖片,黑白圖片的每個像素值可以認爲是一個[0, 255]的整數(如圖一-1-2,爲了圖片看起來不是那麼的密密麻麻,像素值的範圍取了[0, 10])。
      r pixel-Median Filter 算法描述如下:
      對於每個第i行第j列的像素點p(i, j),像四周擴展一個寬和高均爲(2r + 1)的矩形區域,將矩形區域內的像素值按非降序排列後,用排在最中間的那個數字取代原來的像素點p(i, j)的值( 邊界的那圈不作考慮 ),下文會將排在最中間的那個數字稱爲這個序列的中位數
      
圖一-1-2
       如圖一-1-2,r = 1,紅框代表(2, 3) (下標從0計數,行優先)這個像素點所選取的2r + 1的矩形區域,將內中數字進行非降序排列後,得到[0 1 1 2 3 4 6 7 9],所以(2, 3)這個像素點的值將從 6 變成 3。
       這樣就可以粗略得到一個時間複雜度爲O(n^4logn )的算法(枚舉每個像素點,對於每個像素點取矩形窗口元素排序後取中值)。n代表了圖片的尺寸,也就是當圖片越大,這個算法的執行效率就越低,而且增長迅速。那麼如何將這個算法進行優化呢?如果對於二維的情況不是很容易下手的話,不妨先從一維的情況進行考慮。

      3、一維模型
      將問題轉化成一維,可以描述成:給定n(n <= 100000)個範圍在[0, 255]的數字序列a[i] (1 <= i < = n)和一個值(2r+1 <= n),對於所有的a[k] (r+1 <= k <= n-r),將它變成 a[k-r ... k+r] 中的中位數。
   a[1...7] = [1 7 6 4 3 2 1]   r = 2
   d[3] =median( [1 7 6 4 3] ) = 4
   d[4] =median( [7 6 4 3 2] ) = 4
   d[5] =median( [6 4 3 2 1] ) = 3
      所以原數組就會變成a[1..7] = [1 7 4 4 3 2 1]
      那麼總共需要計算的元素爲n-2r,取這些元素的左右r個元素的值需要2r+1次操作,(n-2r)*(2r+1) 當r = (n-1)/4 時取得最大值,爲 (n+1)^2 / 4,再加上排序的時間複雜度,所以最壞情況的時間複雜度爲O( n^2 logn )。n的範圍不允許這麼高複雜度的算法,嘗試進行優化。
      考慮第i個元素的2r+1區域a[ i-r ... i+r ]和第i+1個元素的2r+1區域a[ i+1-r ... i+1+r ],後者比前者少了一個元素a[i-r],多了一個元素a[i+1+r],其它元素都是一樣的,那麼在計算第i+1個元素的情況時如何利用第i個元素的情況就成了問題的關鍵。
        4、數據結構的設計
      我們現在假設有這樣一種數據結構,可以支持以下三種操作:
      1、插入(Insert)將一個數字插入到該數據結構中;
      2、刪除(Delete),將某個數字從該數據結構中刪除;
      3、詢問(Query),詢問該數據結構中存在數字的中位數;
      如果這三個操作都能在O( log(n) )或者O(1)的時間內完成,那麼這個問題就可以完美解決了。具體做法是:
      首先將a[1...2r+1]這些元素都插入到該數據結構中,然後詢問中位數替換掉a[r+1],再刪除a[1],插入a[2r+2],詢問中位數替換掉a[r+2],以此類推,直到計算完第n-r個元素。所有操作都在O( log(n) ) 時間內完成的話,總的時間複雜度就是O( nlogn )。
      我們來看什麼樣的數據結構可以滿足這三條操作都在O( log(n) )的時間內完成,考慮每個數字的範圍是[0, 255],如果我們將這些數字映射到一個線性表中(即 HASH表),插入和刪除操作都可以做到O(1)。
      具體得,用一個輔助數組d[256],插入a[i]執行的是d[ a[i] ] ++,刪除a[i]執行的是 d[ a[i] ] --;詢問操作是對d數組進行順序統計,順序枚舉i,找到第一個滿足sum{d[j] | 1 <= j <= i} >= r+1的 i 就是所求中位數,這樣就得到了一個時間複雜度爲O(Kn)的算法,其中K是數字的值域(這裏討論的問題值域是256)。
      相比之前的算法,這種方法已經前進了一大步,至少n的指數下降了大於一個數量級,但是也帶來了一個問題,如果數字的值域很大,複雜度還是會很大,所以需要更好的算法支持。
       5、樹狀數組華麗登場
      這裏引入一種數據結構 - 樹狀數組 ( Binary Indexed Tree,BIT,二分索引樹 ),它只有兩種基本操作,並且都是操作線性表的數據的:
      1、add( i, 1 )      (1<=i<=n)                       對第i個元素的值自增1           O(logn)
      2、sum( i )         (1<=i<=n)                       統計[1...i]元素值的和             O(logn)
      試想一下,如果用HASH來實現這兩個函數,那麼1的複雜度是O(1),而2的複雜度就是O(n)了,而樹狀數組實現的這兩個函數可以讓兩者的複雜度都達到O(logn),具體的實現先賣個關子,留到第二節着重介紹。
      有了這兩種操作,我們需要將它們轉化成之前設計的數據結構的那三種操作,首先:
      1、插入(Insert),對應的是 add(i, 1),時間複雜度O( logn )
      2、刪除(Delete), 對應的是 add(i, -1), 時間複雜度O( logn )
      3、詢問(Query), 由於sum( i )能夠統計[1...i]元素值的和,換言之,它能夠得到我們之前插入的數據中小於等於i的數的個數,那麼如果能夠知道sum(i) >= r + 1的最小的i,那這個i就是所有插入數據的中位數了(因爲根據上文的條件,插入的數據時刻保證有2r+1個)。因爲sum(i)是關於 i 的遞增函數,所以基於單調性我們可以二分枚舉i (1 <= i <= n),得到最小的 i 滿足sum(i) >= r + 1,每次的詢問複雜度就是 O( logn * logn )。 一個logn是二分枚舉的複雜度,另一個logn是sum函數的複雜度。
      這樣一來,一維的Median Filter模型的整體時間複雜度就降到了O(n * logn * logn),已經是比較高效的算法了。
      接下來就是要來說說樹狀數組的具體實現了。

二、細說樹狀數組
      1、樹 or 數組 ?
      名曰樹狀數組,那麼究竟它是樹還是數組呢?數組在物理空間上是連續的,而樹是通過父子關係關聯起來的,而樹狀數組正是這兩種關係的結合,首先在存儲空間上它是以數組的形式存儲的,即下標連續;其次,對於兩個數組下標x,y(x < y),如果x + 2^k = y (k等於x的二進制表示中末尾0的個數),那麼定義(y, x)爲一組樹上的父子關係,其中y爲父結點,x爲子結點。
 
圖二-1-1
      如圖二-1-1,其中A爲普通數組,C爲樹狀數組(C在物理空間上和A一樣都是連續存儲的)。樹狀數組的第4個元素C4的父結點爲C8 (4的二進制表示爲"100",所以k=2,那麼4 + 2^2 = 8),C6和C7同理。C2和C3的父結點爲C4,同樣也是可以用上面的關係得出的,那麼從定義出發,奇數下標一定是葉子結點。

2、結點的含義
      然後我們來看樹狀數組上的結點Ci具體表示什麼,這時候就需要利用樹的遞歸性質了。我們定義Ci的值爲它的所有子結點的值 和 Ai 的總和,之前提到當i爲奇數時Ci一定爲葉子結點,所以有Ci = Ai  ( i爲奇數 )。從圖中可以得出:
      C1 = A1
      C2 = C1 + A2 = A1 + A2
      C3 = A3
      C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
      C5 = A5
      C6 = C5 + A6 = A5 + A6
      C7 = A7
      C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
      建議直接看C8,因爲它最具代表性。
      我們從中可以發現,其實Ci還有一種更加普適的定義,它表示的其實是一段原數組A的連續區間和。根據定義,右區間是很明顯的,一定是i,即Ci表示的區間的最後一個元素一定是Ai,那麼接下來就是要求Ci表示的第一個元素是什麼。從圖上可以很容易的清楚,其實就是順着Ci的最左兒子一直找直到找到葉子結點,那個葉子結點就是Ci表示區間的第一個元素。
      更加具體的,如果i的二進制表示爲 ABCDE1000,那麼它最左邊的兒子就是 ABCDE0100,這一步是通過結點父子關係的定義進行逆推得到,並且這條路徑可以表示如下:
      ABCDE1000 => ABCDE0100 => ABCDE0010 => ABCDE0001
      這時候,ABCDE0001已經是葉子結點了,所以它就是Ci能夠表示的第一個元素的下標,那麼我們發現,如果用k來表示i的二進制末尾0的個數,Ci能夠表示的A數組的區間的元素個數爲2^k因爲區間和的最後一個數一定是Ai,所以有如下公式:
      Ci  =  sum{ A[j] |  i - 2^k + 1 <= j <= i }    (幫助理解:將j的兩個端點相減+1 等於2^k)
      3、求和操作
      明白了Ci的含義後,我們需要通過它來求sum{ A[j] | 1 <= j <= i },也就是之前提到的sum(i)函數。爲了簡化問題,用一個函數lowbit(i)來表示2^k (k等於i的二進制表示中末尾0的個數)。那麼:
      sum(i) = sum{ A[j] | 1 <= j <= i }
                = A[1] + A[2] + ... + A[i] 
                = A[1] + A[2] + A[i-2^k] + A[i-2^k+1] + ... + A[i]
                = A[1] + A[2] + A[i-2^k] + C[i]
                = sum(i - 2^k) + C[i]
                = sum( i - lowbit(i) ) + C[i]
      由於C[i]已知,所以sum(i)可以通過遞歸求解,遞歸出口爲當i = 0時,返回0。sum(i)函數的函數主體只需要一行代碼:

    int sum(int x){
        return x ? C[x]+ sum( x - lowbit(x)):0;
     }

      觀察 i - lowbit(i),其實就是將i的二進制表示的最後一個1去掉,最多隻有log(i)個1,所以求sum(n)的最壞時間複雜度爲O(logn)。由於遞歸的時候常數開銷比較大,所以一般寫成迭代的形式更好。寫成迭代代碼如下:


    int sum(int x){
        int s =0;
        for(int i = x; i ; i -= lowbit(i)){
            s += c[i][j];
        }
        return s;    
    }


      4、更新操作
      更新操作就是之前提到的add(i, 1) 和 add(i, -1),更加具體得,可以推廣到add(i, v),表示的其實就是 A[i] = A[i] + v。但是我們不能在原數組A上操作,而是要像求和操作一樣,在樹狀數組C上進行操作。
      那麼其實就是求在Ai改變的時候會影響哪些Ci,看圖二-1-1的樹形結構就一目瞭然了,Ai的改變只會影響Ci及其祖先結點,即A5的改變影響的是C5、C6、C8;而A1的改變影響的是C1、C2、C4、C8。
      也就是每次add(i, v),我們只要更新Ci以及它的祖先結點,之前已經討論過兩個結點父子關係是如何建立的,所以給定一個x,一定能夠在最多log(n) (這裏的n是之前提到的值域) 次內更新完所有x的祖先結點,add(i, v)的主體代碼(去除邊界判斷)也只有一行代碼:

    void add(int x,int v){
        if(x <= n){
            C[x]+= v, add( x + lowbit(i), v );
        }
     }

    和求和操作類似,遞歸的時候常數開銷比較大,所以一般寫成迭代的形式更好。寫成迭代形式的代碼如下:
    
    void add(int x,int v){
        for(int = x; i <= n; i += lowbit(i)){
            C[i+= v;
        }
    }

        5、lowbit函數O(1)實現
      上文提到的兩個函數sum(x)和add(x, v)都是用遞歸實現的,並且都用到了一個函數叫lowbit(x),表示的是2k,其中k爲x的二進制表示末尾0的個數,那麼最簡單的實現辦法就是通過位運算的右移,循環判斷最後一位是0還是1,從而統計末尾0的個數,一旦發現1後統計完畢,計數器保存的值就是k,當然這樣的做法總的複雜度爲O( logn ),一個32位的整數最多可能進行31次判斷(這裏討論整數的情況,所以符號位不算)。
       這裏介紹一種O(1)的方法計算2k的方法。
       來看一段補碼小知識:
         清楚補碼的表示的可以跳過這一段,計算機中的符號數有三種表示方法,即原碼、反碼和補碼。三種表示方法均有符號位和數值位兩部分,符號位都是用0表示“正”,用1表示“負”,而數值位,三種表示方法各不相同。這裏只討論整數補碼的情況,在計算機系統中,數值一律用補碼來表示和存儲。原因在於,使用補碼,可以將符號位和數值域統一處理;同時,加法和減法也可以統一處理。整數補碼的表示分兩種:
                  正數:正數的補碼即其二進制表示;
                           例如一個8位二進制的整數+5,它的補碼就是 00000101 (標紅的是符號位,0表示"正",1表示“負”)
                  負數:負數的補碼即將其整數對應的二進制表示所有位取反(包括符號位)後+1;
                           例如一個8位二進制的整數-5,它的二進制表示是00000101,取反後爲11111010,再+1就是11111011,這就是它的補碼了。
         下面的等式可以幫助理解補碼在計算機中是如何工作的
                      +5 + (-5) = 00000101 + 11111011 = 1 00000000 (溢出了!!!) = 0 
         這裏的加法沒有將數值位和符號位分開,而是統一作爲二進制位進行計算,由於表示的是8進制的整數,所以多出的那個最高位的1會直接捨去,使得結果變成了0,而實際的十進制計算結果也是0,正確。
       補碼複習完畢,那麼來看下下面這個表達式的含義:           x & (-x)       (其中 x >= 0)
         首先進行&運算,我們需要將x和-x都轉化成補碼,然後再看&之後會發生什麼,我們假設 x 的二進制表示的末尾是連續的 k 個 0,令x的二進制表示爲  X0X1X2…Xn-2Xn-1,  則 {X= 0 | n-k <= i < n}, 這裏的X0表示符號位。
       x 的補碼就是由三部分組成:               (0)(X1X2…Xn-k-1)(k個0)   其中Xn-k-1爲1,因爲末尾是k個0,如果它爲0,那就變成k+1個0了。
      -x的補碼也是由三部分組成:      (1)(Y1Y2…Yn-k-1)(k個0)     其中Yn-k-1爲1,其它的XiYi相加爲1,想補碼是怎麼計算的就明白了。
   那麼 x & (-x) 也就顯而易見了,由兩部分組成 (1)(k0)表示成十進制爲 2k 啦。
      由於&的優先級低於-,所以代碼可以這樣寫:

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

        6、小結
   至此,樹狀數組的基礎內容就到此結束了,三個函數就詮釋了樹狀數組的所有內容,並且都只需要一行代碼實現,單次操作的時間複雜度爲O( log(n) ),空間複雜度爲O(n),所以它是一種性價比非常高的輕量級數據結構。
      樹狀數組解決的基本問題是 單點更新,成端求和。上文中的sum(x)求的是[1, x]的和,如果需要求[x, y]的和,只需要求兩次sum函數,然後相減得到,即sum(y) - sum(x-1)。
      下面一節會通過一些例子來具體闡述樹狀數組的應用場景。

三、樹狀數組的經典模型

       1、PUIQ模型
  【例題1】一個長度爲n(n <= 500000)的元素序列,一開始都爲0,現給出三種操作:
      1. add x v :    給第x個元素的值加上v;     (  a[x] += v )
      2. sub x v :    給第x個元素的值減去v;     (  a[x] -= v )
      3. sum x y:  詢問第x到第y個元素的和;  ( print sum{ a[i] | x <= i <= y } )
      
      這是樹狀數組最基礎的模型,1和2的操作就是對應的單點更新,3的操作就對應了成端求和。
      具體得,1和2只要分別調用add(x, v)和add(x, -v), 而3則是輸出sum(y) - sum(x-1)的值。
      我把這類問題叫做PUIQ模型(Point Update Interval Query 點更新,段求和)。
       
       2、IUPQ模型
  【例題2】一個長度爲n(n <= 500000)的元素序列,一開始都爲0,現給出兩種操作:
      1. add x y v :    給第x個元素到第y個元素的值都加上v;     (  a[i] += v, 其中 x <= i <= y )
      2. get x:         詢問第x個元素的值;                               (  print  a[x] )
      這類問題對樹狀數組稍微進行了一個轉化,但是還是可以用add和sum這兩個函數來解決,對於操作1我們只需要執行兩個操作,即add(x, v)和add(y+1, -v);而操作2則是輸出sum(x)的值。
      這樣就把區間更新轉化成了單點更新,單點求值轉化成了區間求和。
      我把這類問題叫做IUPQ模型(Interval Update Point Query 段更新,點求值)。

        3、逆序模型
     【例題3】給定一個長度爲n(n <= 500000)的排列a[i],求它的逆序對對數。1 5 2 4 3 的逆序對爲(5,2)(5,3)(5,4)(4,3),所以答案爲4。

       樸素算法,枚舉任意兩個數,判斷他們的大小關係進行統計,時間複雜度O(n2)。不推薦!
      來看一個給定n個元素的排列 XXX… Xn-2 Xn-1,對於某個 X元素,如果想知道以它爲"首"的逆序對的對數( 形如(XiXj) 的逆序對),就是需要知道 Xi+1 … Xn-2 Xn-1 這個子序列中小於 X的元素的個數。
      那麼我們只需要對這個排列從後往前枚舉,每次枚舉到 X元素時,執行cnt += sum(Xi-1),然後再執行add(Xi1),n個元素枚舉完畢,得到的cnt值就是我們要求的逆序數了。總的時間複雜度O(nlogn)。
      這個模型和之前的區別在於它不是將原數組的下標作爲樹狀數組的下標,而是將元素本身作爲樹狀數組的下標。逆序模型作爲樹狀數組的一個經典思想有着非常廣泛的應用。
     【例題4】
給定N(N <= 100000)個區間,定義兩個區間(Si, Ei)和(Sj, Ej)的'>'如下:
如果Si <= Sj and Ej <= Ei and Ei - Si > Ej - Sj,則 
(Si, Ei) > (Sj, Ej),現在要求每個區間有多少區間'>'它。
     將上述三個關係式化簡,可以得到 區間i > 區間j 的條件是:區間i 完全覆蓋 區間j,並且兩者不相等。
圖三-3-1
       對區間進行排序,排序規則爲:左端點遞增,如果左端點相同,則右端點遞減。
       然後枚舉區間,不斷插入區間右端點,因爲區間左端點是保持遞增的,所以對於某個區間(Si, Ei),只需要查詢樹狀數組中[Ei, MAX]這一段有多少已經插入的數據,就能知道有多少個區間是比它大的,這裏需要注意的是多個區間相等的情況,因爲有排序,所以它們在排序後的數組中一定是相鄰的,所以在遇到有相等區間的情況,需要"延遲"插入。等下一個不相等區間出現時才把之前保存下來的區間右端點進行插入。插入完畢再進行統計。
       這裏的插入即add(Ej, 1),統計則是sum(MAX) - sum(Ei - 1)  (其中j < i)。
       4、二分模型
     【例題5】給定N(N <= 100000)個編號爲1-N的球,將它們亂序丟入一個“神奇的容器”中,作者會在丟的同時詢問其中編號第K大的那個球,“神奇的容器”都能夠從容作答,並且將那個球給吐出來,然後下次又可以繼續往裏丟。
       現在要你來模擬這個神奇的容器的功能。可以抽象成兩種操作:
       1. put x                 向容器中放入一個編號爲x的球;
       2. query K             詢問容器中第K大的那個球,並且將那個球從容器中去除(保證K<容器中球的總數);
      
       這個問題其實就是一維Median Filter的原型了,只是那個問題的K = r+1,而這裏的K是用戶輸入的一個常量。所謂二分模型就是在求和的過程中,利用求和函數的單調性進行二分枚舉。
       對於操作1,只是單純地執行add(x, 1)即可;而對於操作2,我們要看第K大的數滿足什麼性質,由於這裏的數字不會有重複,所以一個顯而易見的性質就是一定有K-1個數大於它,假設它的值爲x,那麼必然滿足下面的等式:sum(N) - sum( x ) == K-1,然而,滿足這個等式的x可能並不止一個,來看下面的圖:
圖三-4-1
       圖三-4-1中灰色的格子表示容器中的球,分別爲2、3、7、8,然後我們需要求第3大的球,理論的球編號爲3,但是滿足上面等式的球的編號爲3、4、5、6。所以我們需要再加一個限制條件,即滿足上面等式的最小的x。於是我們二分枚舉x,當滿足sum(N) - sum( x ) <= K-1時,將右區間縮小(說明找的數x偏大,繼續往小的找),否則左區間增大(說明找的數x偏小,繼續往大的找),直到找到滿足條件的最小的x爲止。單次操作的時間複雜度爲O( logn * logn )。

       5、再說Median Filter       
      基於二分模型的一維Median Filter問題已經圓滿解決了,那麼最後讓我們回到二維的Median Filter問題上來。
圖三-5-1
      有了一維的基礎,對於二維的情況,其實也是一樣的,如圖三-5-1,圖中紅色的框爲(1, 1)這個像素點的(2r+1)矩形區域,橙色的框則是(1, 2)的,它們的差別其實只是差了兩列;同樣的,橙色框和黃色框也差了兩列,於是,我們可以從左向右枚舉,每次將這個矩形框向右推進一格,然後將"離開"框的那一列數據從樹狀數組中刪除,將"進入"框的那一列數據插入到樹狀數組中,然後統計中位數。
      當枚舉到右邊界時,將矩形框向下推進一格,然後迂迴向左,同樣按照之前的方案統計中位數,就這樣呈蛇字型迂迴前進(具體順序如圖所示的紅、橙、黃、綠、青、藍、紫),這樣就得到了一個O( n3lognlogn )的算法,比樸素算法下降了一個數量級。

       6、多維樹狀數組模型    
      【例題6】給定一個N*N(N <= 1000)的矩形區域,執行兩種操作:
        1. add x y v                                     在(x, y)加上一個值v;
        2. sum x1 y1 x2 y2                          統計矩形(x1, y1) - (x2, y2)中的值的和;

         PUIQ模型的二維版本。我們設計兩種基本操作:
        1. add(x, y, v)        在(x, y)這個格子加上一個值v;
        2. sum(x, y)           求矩形區域(1, 1) - (x, y)內的值的和,那麼(x1,y1)-(x2,y2)區域內的和可以通過四個求和操作獲得,即 sum(x2, y2) - sum(x2, y1 - 1) - sum(x1 - 1, y2) + sum(x1 - 1, y1 - 1)。 (利用容斥原理的基本思想)
        add(x, y, v)和sum(x, y)可以利用二維樹狀數組實現,二維樹狀數組可以理解成每個C結點上又是一棵樹狀數組(可以從二維數組的概念去理解,即數組的每個元素都是一個數組),具體代碼如下:

    void add(int x,int y,int v){
        for(int i = x; i <= n; i += lowbit(i)){
            for(int j = y; j <= n; j += lowbit(j)){ 
                c[i][j]+= v;
            }
        }
    }
    
    int sum(int x,int y){
        int s =0;
        for(int i = x; i ; i -= lowbit(i)){
            for(int j = y; j ; j -= lowbit(j)){ 
                s += c[i][j];
            }
        }
        return s;
     }

          仔細觀察即可發現,二維樹狀數組的實現和一維的實現極其相似,二維僅僅比一維多了一個循環,並且數據用二維數組實現。那麼同樣地,對於三維的情況,也只是在數組的維度上再增加一維,更新和求和時都各加一個循環而已。


四、樹狀數組題集整理
        Enemy Soilders                ☆☆      樹狀數組基礎
       Stars                          ★☆☆☆☆      降維
        Tunnel Warfare                ★★☆☆      二分模型
               Apple Tree                                              ☆☆      
               Mobile phones                                       ★★☆      二維PUIQ
               Minimum Inversion Number       ★★☆☆☆      樹狀數組求逆序數
        Ultra-QuickSort               ★★☆      求逆序數,經典樹狀數組
        Cows                          ★★☆☆☆      區間問題轉化成逆序求解
                MooFest                       ★★☆☆☆      轉化成求和
               Ping pong                                             ★★ 
               Conturbatio                                           ★★      其實並不需要樹狀數組,直接靜態求和就行O(1)
                Cube                                                     ★★☆☆      三維樹狀數組
               Japan                          ★★★☆☆      逆序模型,交叉統計問題
                Life is a Line                                         ★★★☆      逆序模型,幾何上的樹狀數組
        Magic Ball Game               ★★★☆☆      樹上的統計
        Super Mario                   ★★★☆☆      降維思想
        Median Filter                 ★★★☆☆      模糊算法
                Disharmony Trees              ★★★☆☆      統計問題
        Find the non sub              ★★★☆      動態規劃+樹狀數組
        Traversal                     ★★★      動態規劃+樹狀數組
       The Battle of Chibi            ★★★☆      動態規劃+樹狀數組
        KiKi's K-Number               ★★★☆      二分模型/K大數
                 Matrix                       ★★★☆☆      二維IUPQ
        Sum the K-th's               ★★      K大數 / 二分模型
        Kiki & Little Kiki 1         ★★      二分模型
        The k-th Largest Group       ★★☆      樹狀數組 + 並查集
        Inversion                    ★★★☆      逆序數   
        Harmony Forever              ★★★☆      二分模型
                                                                                                              

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章