樹狀數組詳解——本質上就是空間換時間,可以解決大部分基於區間上的更新以及求和問題 尼瑪,專用算法


 

943. 區間和查詢 - Immutable

 

中文

 

English

 

給一個整數數組 nums,求出下標從 ij 的元素和(i ≤ j)ij對應的元素也包括在內。

 

樣例

樣例1

輸入: nums = [-2, 0, 3, -5, 2, -1]
sumRange(0, 2)
sumRange(2, 5)
sumRange(0, 5)
輸出:
1
-1
-3
解釋: 
sumRange(0, 2) -> (-2) + 0 + 3 = 1
sumRange(2, 5) -> 3 + (-5) + 2 + (-1) = -1
sumRange(0, 5) -> (-2) + 0 + 3 + (-5) + 2 + (-1) = -3

樣例2

輸入: 
nums = [-4, -5]
sumRange(0, 0)
sumRange(1, 1)
sumRange(0, 1)
sumRange(1, 1)
sumRange(0, 0)
輸出: 
-4
-5
-9
-5
-4
解釋: 
sumRange(0, 0) -> -4
sumRange(1, 1) -> -5
sumRange(0, 1) -> (-4) + (-5) = -9
sumRange(1, 1) -> -5
sumRange(0, 0) -> -4

 

注意事項

  1. 你可以認為給出的數組不會發生變化。
  2. 會調用非常多次 sumRange 函數。

這題只需要求出給定數組的前綴和數組prefixSum,對於查詢[st,ed],輸出prefixSum[ed]-prefixSum[st-1]即可

class NumArray:

    def __init__(self, nums):
        """
        :type nums: List[int]
        """
        self.sum = [0]
        for i in nums:
            self.sum += self.sum[-1] + i,

    def sumRange(self, i, j):
        """
        :type i: int
        :type j: int
        :rtype: int
        """
        return self.sum[j + 1] - self.sum[i]

 

665. 平面范圍求和 -不可變矩陣

中文
English

給一 二維矩陣,計算由左上角 (row1, col1) 和右下角 (row2, col2) 划定的矩形內元素和.

樣例

樣例1

輸入:
[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]
sumRegion(2, 1, 4, 3)
sumRegion(1, 1, 2, 2)
sumRegion(1, 2, 2, 4)
輸出:
8
11
12
解釋:
給出矩陣
[
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) = 2 + 0 + 1 + 1 + 0 + 1 + 0 + 3 + 0 = 8
sumRegion(1, 1, 2, 2) = 6 + 3 + 2 + 0 = 11
sumRegion(1, 2, 2, 4) = 3 + 2 + 1 + 0 + 1 + 5 = 12

樣例2

輸入:
[[3,0],[5,6]]
sumRegion(0, 0, 0, 1)
sumRegion(0, 0, 1, 1)
輸出:
3
14
解釋:
給出矩陣
[
  [3, 0],
  [5, 6]
]
sumRegion(0, 0, 0, 1) = 3 + 0 = 3
sumRegion(0, 0, 1, 1) = 3 + 0 + 5 + 6 = 14

注意事項

  1. 你可以假設矩陣不變
  2. 對函數 sumRegion 的調用次數有很多次
  3. 你可以假設 row1 ≤ row2 並且 col1 ≤ col2

不妨設dp[i][j]表示(0,0)(i,j)的子矩陣和。
轉移方程為:dp[i][j]=dp[i][j−1]+dp[i][j−1]−dp[i][j]+a[i][j]

 

class NumMatrix(object):

    # @param {int[][]} matrix a 2D matrix
    def __init__(self, matrix):
        # Write your code here

        if len(matrix) == 0 or len(matrix[0]) == 0:
            return 
        
        n = len(matrix)
        m = len(matrix[0])
        
        self.dp  = [[0] * (m + 1) for _ in range(n + 1)]
        for r in range(n):
            for c in range(m):
                self.dp[r + 1][c + 1] = self.dp[r + 1][c] + self.dp[r][c + 1] + \
                    matrix[r][c] - self.dp[r][c]

        
    # @param {int} row1 an integer
    # @param {int} col1 an integer
    # @param {int} row2 an integer
    # @param {int} row2 an integer
    # @return {int} the sum of region
    def sumRegion(self, row1, col1, row2, col2):
        # Write your code here
        return self.dp[row2 + 1][col2 + 1] - self.dp[row1][col2 + 1] - \
            self.dp[row2 + 1][col1] + self.dp[row1][col1]
        

 

840. 可變范圍求和

 

中文

 

English

 

給定一個整數數組 nums, 然后你需要實現兩個函數:

  • update(i, val) 將數組下標為i的元素修改為val
  • sumRange(l, r) 返回數組下標在[l,r][l, r][l,r]區間的元素的和

 

樣例

樣例 1:

輸入:
  nums = [1, 3, 5]
  sumRange(0, 2)
  update(1, 2)
  sumRange(0, 2)
輸出: 
  9
  8

樣例 2:

輸入: 
  nums = [0, 9, 5, 7, 3]
  sumRange(4, 4)
  sumRange(2, 4)
  update(4, 5)
  update(1, 7)
  update(0, 8)
  sumRange(1, 2)
輸出: 
  3
  15
  12

 

注意事項

  1. 數組只能通過update函數進行修改。
  2. 你可以假設 update 函數與 sumRange 函數的調用數量是均勻的。
class NumArray(object):
    
    def __init__(self, nums):
        """
        :type nums: List[int]
        """
        self.arr = nums # 
        self.n = len(nums)
        self.bit = [0] * (self.n + 1)
        for i in range(self.n):
            self.add(i, self.arr[i])

    def update(self, i, val):
        """
        :type i: int
        :type val: int
        :rtype: void
        """
        self.add(i, val - self.arr[i])
        self.arr[i] = val

    def sumRange(self, i, j):
        """
        :type i: int
        :type j: int
        :rtype: int
        """
        return self.sum(j) - self.sum(i - 1)
        
    def lowbit(self, x):
        return x & (-x)
    
    def add(self, idx, val):
        idx += 1
        while idx <= self.n:
            self.bit[idx] += val
            idx += self.lowbit(idx)
    
    def sum(self, idx):
        idx += 1
        res = 0
        while idx > 0:
            res += self.bit[idx]
            idx -= self.lowbit(idx)
        return res

 就是使用樹狀數組來求解。

其中,bit表示Binary Indexed Tree

又名:Fenwick Tree 中文名:狀數組 簡寫:BIT 基於信息來實現——
Log(n) 修改任意位置
Log(n) 查詢任意區

Binary Indexed Tree 上就是一個有部分區段累加和數

 總結在前:

把原先我累加的方式從:
for (int i = index; i >= 0; i = i - 1) sum += arr[i];
改成了
for (int i = index+1; i >= 1; i = i - lowbit(i)) sum += bit[i];lowbit是核心和關鍵!!!

樹狀數組詳解

先來看幾個問題吧。

1.什么是樹狀數組?

顧名思義,就是用數組來模擬樹形結構唄。那么衍生出一個問題,為什么不直接建樹?答案是沒必要,因為樹狀數組能處理的問題就沒必要建樹。和Trie樹的構造方式有類似之處。

2.樹狀數組可以解決什么問題

可以解決大部分基於區間上的更新以及求和問題。就是上面的算法題目。

3.樹狀數組和線段樹的區別在哪里

樹狀數組可以解決的問題都可以用線段樹解決,這兩者的區別在哪里呢?樹狀數組的系數要少很多,就比如字符串模擬大數可以解決大數問題,也可以解決1+1的問題,但沒人會在1+1的問題上用大數模擬。

4.樹狀數組的優點和缺點

修改和查詢的復雜度都是O(logN),而且相比線段樹系數要少很多,比傳統數組要快,而且容易寫。

缺點是遇到復雜的區間問題還是不能解決,功能還是有限。


一、樹狀數組介紹

樹狀數組可以解決什么樣的問題:

這里通過一個簡單的題目展開介紹,先輸入一個長度為n的數組,然后我們有如下兩種操作:

  1. 輸入一個數m,輸出數組中下標1~m的前綴和
  2. 對某個指定下標的數進行值的修改

多次執行上述兩種操作


尋常方法
對於一個的數組,如果需要求1~m的前綴和我們可以將其從下標1開始對m個數進行求和,對於n次操作,時間復雜度是O(n^2),對於值的修改,我們可以直接通過下標找到要修改的數,n次操作時間復雜度為O(n),在數組n開得比較大的時候,求前綴和的效率顯得低了

  • 那么有人提出了一種優化的方式:
    初始我們用一個數組A的保存每個位置的初始值,然后用一個輔助數組B存放的是下標為i的時候A數組的前i個的和(前綴和),那么當我們需要查詢m個數的前綴和的時候只要直接使用下標對B數組進行查詢即可,n次查詢,時間復雜度為O(n),而此時,對於單點更新值的維護消耗,由原來的O(n)變成了O(n^2),因為每一次與更新單點值都會對后面的已經計算好的B數組前綴和的值造成影響,需要不斷更新B數組的值,n次更新維護的消耗自然就變成了O(n^2),更新的效率變得低下

樹狀數組
那么是否有一種方法可以讓查詢和更新的時間復雜度都小一些呢,至少可以令人接受,這里將介紹樹狀數組如何處理前綴和查詢和單點更新的問題,對於n次操作,時間復雜度都為O(nlogn)


注意觀察箭頭高度!!!其中,【1,8】表示sum(a[1~8]),【5,6】表示sum(a[5~6])

圖2.jpg

如圖,對於一個長度為n的數組,A數組存放的是數組的初始值,引入一個輔助數組C(我們通過C數組建立樹狀數組)

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

找規律:

C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i];   //k為i的二進制中從最低位到高位連續零的長度

例如i = 8(1000)時候,k = 3,可自行驗證。

 

這個怎么實現求和呢,比如我們要找前7項和,那么應該是SUM = C[7] + C[6] + C[4];

 

 

我們稱C[i]的值為下標為i的數所管轄的數的和,C[8]存放的就是被編號8所管轄的那些數的和(有8個),而下標為i的數所管轄的元素的個數則為2^k個(k為i的二進制的末尾0的個數)舉兩個例子查詢下標m==8和m==5所管轄的數的和

  • 8 = 1000,末尾3個0,故k == 3,所管轄的個數為2^3 == 8,C8是8個數的和
  • 5 = 0101,末尾沒有0,故k == 0,所管轄的個數為2^0 == 1,C5是一個數的和(它本身A5)

而對於輸入的數m,我們要求編號為m的數的前綴和A1~Am(這里假設樹狀數組已經建立,即C1~C8的值已經求出,別着急,在本文的最下方會做出建立樹狀數組的過程講解,因為現在是在求前綴和,就假設C數組已經可用了吧)舉兩個例子m==7和m==6(sum(i)表示求編號為i的前綴和

  • m==7 sum(7) = C7 + C6 + C4
    那么我們是怎么得到編號7是由哪幾個C[i]求和得到呢(C4, C6, C7怎么得到的),這里有介紹一種巧妙的方法:
    對於查詢的m,將它轉換成二進制后,不斷對末尾的1的位置進行-1的操作,直到全部為0停止
    7的二進制為0111(C7得到),那么先對0111的末尾1的位置-1,得到0110 == 6(C6得到),再對0110末尾1位置-1,得到0100 == 4(C4得到),最后對0100末尾1位置-1后得到0000(結束信號),計算停止,至此C7,C6,C4全部得到,求和后就是m == 7時它的前綴和
  • m==6 sum(6) = C6 + C4
    m == 6時也是一樣,先轉成2進制等於0110,經過兩次變換后為0100(C4)和0000(結束信號),那么求和后同樣也得到了預計的結果

這里要介紹一個高效的方法,lowbit(int m),這是一個函數,它的作用是求出m的二進制表示的末尾1的位置,對於要查詢m的前綴和,m = m - lowbit(m)代表不斷對二進制末尾1進行-1操作,不斷執行直到m == 0結束,就能得到前綴和由哪幾個Cm構成,十分巧妙,lowbit也是樹狀數組的核心

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

關於m&(-m)很多童鞋可能感到困惑,那么就不得不提及一下負數在計算機內存中的存儲形式,負數在計算機中是以補碼的形式存儲的,如13的二進制表示為1101,那么-13的二進制而將13二進制按位取反,然后末尾+1,即0010 + 0001 = 0011,那么1101 & 0011== 0001,很顯然得到m == 13二進制末尾1的位置是2的0次方位,將13 - 0001 == 12,再對12執行lowbit操作,1100 & 0100 == 0100,也很輕易得到了m == 12時二進制末尾1的位置是2的2次方位,將12 - 0100 == 8,再對8執行lowbit操作,0100 & 1100 == 0100,得到m == 8時二進制位是2的2次方位,8 - 0100 == 0(結束操作),通過循環得到的13,12,8,則sum(13) == C13 + C12 + C8

求前綴和的代碼

int ans = 0; int getSum(int m){ while(m > 0){ ans += C[m]; m -= lowbit(m); } }

對於n次前綴和的查詢,時間復雜度為O(nlogn)
接下來講解單點更新值
對於輸入編號為x的值,要求為它的值附加一個value值,我們把圖再一次拿下來
圖3.jpg

假設x==2,value==5,那么我們先找到A[2]的位置,通過觀察我們得知,如果修改了A[2]的值,那么管轄A[2]的C[2],C[4],C[8]的前綴和都要加上value(所有的祖先節點),那么和查詢類似,我們如何得到C2的所有祖先節點呢(因為C2和A2的下標相同所以更新時查詢從C[x]開始),依舊是上述的巧妙的方法,但是我們把它倒過來
對於要更新x位置的值,我們把x轉換成二進制,不斷對二進制最后一個1的位置+1,直到達到數組下標的最大值n結束

  • 對於給出的例子x==2,假設數組下標上限n==8,x轉換成二進制后等於0010(C2),對末尾1的位置進行+1,得到0100(C4),對末尾的1的位置進行+1,得到1000(C8),循環結束,對C2,C4,C8的前綴和都要加上value,當然不能忘記對A[2]的值+value,單點更新值過程結束

給出代碼

void update(int x, int value){ A[x] += value; //不能忘了對A數組進行維護,盡善盡美嘛 while(x <= n){ C[x] += value; x += lowbit(x); } }

對於n次更新操作,時間復雜度同樣為O(nlogn)

這里有一個注意事項,我們對於求前綴和與單點更新時,樹狀數組C是拿來直接使用的,那么問題來了,樹什么時候建立好的,我怎么不知道??

事實上,對於一個輸入的數組A,我們一次讀取的過程,就可以想成是一個不斷更新值的過程(把A1~An從0更新成我們輸入的A[i]),所以一邊讀入A[i],一邊將C[i]涉及到的祖先節點值更新,完成輸入后樹狀數組C也就建立成功了

  • 完整代碼如下:
#include<stdio.h> #include<string.h> int a[10005]; int c[10005]; int n; int lowbit(int x){ return x&(-x); } int getSum(int x){ int ans = 0; while(x > 0){ ans += c[x]; x -= lowbit(x); } return ans; } void update(int x, int value){ a[x] += value; while(x <= n){ c[x] += value; x += lowbit(x); } } int main(){ while(scanf("%d", &n)!=EOF){ //用於測試n == 8 memset(a, 0, sizeof(a)); memset(c, 0, sizeof(c)); for(int i = 1; i <= n; i++){ scanf("%d", &a[i]); //a[i]的值根據具體題目自己安排測試可以1,2,3,4,5,6,7,8 update(i, a[i]); //輸入的過程就是更新的過程 } int ans = getSum(n-1); //用於測試輸出n-1的前綴和 輸出28 printf("%d\n", ans); } return 0; } 

 


免責聲明!

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



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