夜深人靜寫算法(三) - 樹狀數組
原文出處: menjitianya
目錄
一、從圖形學算法說起
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中的”模糊”那個操作。
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,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)和一個值r (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、樹狀數組華麗登場
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)){ <strong>s += c[i][j];</strong> } 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 i = x; i <= n; i += lowbit(i)){ C[i] += v; } }
5、lowbit函數O(1)實現
上文提到的兩個函數sum(x)和add(x, v)都是用遞歸實現的,并且都用到了一個函數叫lowbit(x),表示的是,其中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, 則 {Xi = 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,其它的Xi和Yi相加為1,想想補碼是怎么計算的就明白了。
那么 x & (-x) 也就顯而易見了,由兩部分組成 (1)(k個0),表示成十進制為 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個元素的排列 X0 X1 X2 … Xn-2 Xn-1,對于某個 Xi 元素,如果想知道以它為”首”的逆序對的對數( 形如(XiXj) 的逆序對),就是需要知道 Xi+1 … Xn-2 Xn-1 這個子序列中小于Xi 的元素的個數。
那么我們只需要對這個排列從后往前枚舉,每次枚舉到 Xi 元素時,執行cnt += sum(Xi-1),然后再執行add(Xi, 1),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 ★★★☆☆ 逆序模型,交叉統計問題
Magic Ball Game ★★★☆☆ 樹上的統計
Super Mario ★★★☆☆ 降維思想
Median Filter ★★★☆☆ 模糊算法
Disharmony Trees ★★★☆☆ 統計問題
Find the non sub ★★★☆☆ 動態規劃+樹狀數組
Traversal ★★★☆☆ 動態規劃+樹狀數組
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 ★★★★☆ 二分模型