前綴和是一種重要的預處理,能大大下降查詢的時間複雜度。咱們能夠簡單理解爲「數列的前 n 項的和」。這個概念其實很容易理解,即一個數組中,第 n 位存儲的是數組前 n 個數字的和。python
經過一個例子來進行說明會更清晰。題目描述:有一個長度爲 N 的整數數組 A,要求返回一個新的數組 B,其中 B 的第 i 個數 B[i]是原數組 A 前 i 項和。git
這道題實際就是讓你求數組 A 的前綴和。對 [1,2,3,4,5,6] 來講,其前綴和能夠是 pre=[1,3,6,10,15,21]。咱們可使用公式 pre[𝑖]=pre[𝑖−1]+nums[𝑖]獲得每一位前綴和的值,從而經過前綴和進行相應的計算和解題。其實前綴和的概念很簡單,但困難的是如何在題目中使用前綴和以及如何使用前綴和的關係來進行解題。實際的題目更多不是直接讓你求前綴和,而是你須要本身使用前綴和來優化算法的某一個性能瓶頸。github
而若是數組是正數的話,前綴和數組會是一個單調不遞減序列,所以前綴和 + 二分也會是一個考點,不過這種題目難度通常是力扣的困難難度。關於這個知識點,我會在以後的二分專題方作更多介紹。算法
上面提到的例子是一維數組的前綴和,簡稱一維前綴和。那麼二維前綴和實際上就是二維數組上的前綴和了。一維數組的前綴和也是一個一維數組,一樣地,二維數組的前綴和也是一個二維的數組。數組
好比對於以下的一個二維矩陣:app
1 2 3 4 5 6 7 8
定義二維前綴和矩陣 $pres$,$pres{x,y} = \sum\limits_{i=1}^x \sum\limits_{j=1}^y a_{i,j}$。通過這樣的處理,上面矩陣的二維前綴和就變成了:性能
1 3 6 10 6 14 24 36
那麼如何用代碼計算二維數組的前綴和呢?簡單的二維前綴和的求解方法是基於容斥原理的。優化
好比咱們想求如圖中灰色部分的和。spa
一種方式就是用下圖中兩個綠色部分的矩陣加起來(之因此用綠色部分相加是由於這兩部分已經經過上面預處理計算好了,能夠在 $O(1)$ 的時間獲得),這樣咱們就會多加一塊區域,這塊區域就是如圖黃色部分,咱們再減去黃色部分就行了,最後再加上當前位置自己就好了。code
好比咱們想要求 $sum_{i,j}$,則能夠經過 $sum_{i - 1,j} + sum_{i,j - 1} - sum_{i - 1,j - 1} + a_{i,j}$ 的方式來實現。這樣我就能夠經過 $O(m * n)$ 的預處理計算二維前綴和矩陣(m 和 n 分別爲矩陣的長和寬),再經過 $O(1)$ 的時間計算出任意小矩陣的和。其底層原理就是上面提到的容斥原理,你們能夠經過畫圖的方式來感覺一下。
然而實際上,咱們也可不構建一個前綴和數組,而是直接原地修改。
一維前綴和一樣能夠採用這一技巧。
好比咱們能夠先不考慮行之間的關聯,而是預先計算出每一行的前綴和。對於計算每一行的前綴和就是一維前綴和啦。接下來經過固定兩個列的端點的方式計算每一行的區域和。代碼上,咱們能夠經過三層循環來實現, 其中兩層循環用來固定列端點,另外一層用於枚舉全部行。
其實也能夠反過來。即固定行的左右端點並枚舉列,下面的題目會提到這一點。
代碼表示:
# 預先構建行的前綴和 for row in matrix: for i in range(n - 1): row[i + 1] += row[i]
好比矩陣:
1 2 3 4 5 6 7 8
則會變爲:
1 3 6 10 5 11 18 26
接下來:
# 固定列的兩個端點,即枚舉全部列的組合 for i in range(n): for j in range(i, n): pres = [0] pre = 0 # 枚舉全部行 for k in range(m): # matrix[k] 其實已是上一步預處理的每一行的前綴和了。所以 matrix[k][j] - (matrix[k][i - 1] 就是每一行 [i, j] 的區域和。 pre += matrix[k][j] - (matrix[k][i - 1] if i > 0 else 0) pres.append(pre)
上面代碼作的事情形象來看,就是先在水平方向計算前綴和,而後在豎直方向計算前綴和,而不是同時在兩個方向計算。
若是把 [i, j] 的區域和看出是一個數的話,問題就和一維前綴和同樣了。代碼:
for i in range(n): for j in range(i, n): pres = [0] pre = 0 # 枚舉全部行 for k in range(m): # 其中 a 爲[i, j] 的區域和 pre += a pres.append(pre)
有了上面的知識,咱們就能夠來解決下面兩道題。雖然下面兩道題的難度都是 hard,不過整體難度並不高。這兩道題之因此是 hard, 是由於其考察了不止一個知識點。這也是 hard 題目的一種類型,即同時考察多個知識點。
https://leetcode-cn.com/probl...
給定一個非空二維矩陣 matrix 和一個整數 k,找到這個矩陣內部不大於 k 的最大矩形和。 示例: 輸入: matrix = [[1,0,1],[0,-2,3]], k = 2 輸出: 2 解釋: 矩形區域 [[0, 1], [-2, 3]] 的數值和是 2,且 2 是不超過 k 的最大數字(k = 2)。 說明: 矩陣內的矩形區域面積必須大於 0。 若是行數遠大於列數,你將如何解答呢?
前面提到了因爲非負數數組的二維前綴和是一個非遞減的數組,所以經常和二分結合考察。實際上即便數組不是非負的,咱們仍然有可能構建一個有序的前綴和,從而使用二分,這道題就是一個例子。
首先咱們能夠用上面提到的技巧計算二維數組的前綴和,這樣咱們就能夠計算快速地任意子矩陣的和了。注意到上面咱們計算的 pres 數組是一個一維數組,但矩陣其實可能爲負數,所以不知足單調性。這裏咱們能夠手動維護 pres 單調遞增,這樣就可使用二分法在 $logN$ 的時間求出以當前項 i 結尾的不大於 k 的最大矩形和,那麼答案就是全部的以任意索引 x 結尾的不大於 k 的最大矩形和的最大值。
之因此能夠手動維護 pres 數組單調增也可獲得正確結果的緣由是題目只須要求子矩陣和,而不是求具體的子矩陣。
代碼上,當計算出 pres 後,咱們其實須要尋找大於等於 pre - k 的最小數 x。這樣矩陣和 pre - x 才能知足 pre - x <= k,使用最左插入二分模板便可解決。
Python3 Code:
class Solution: def maxSumSubmatrix(self, matrix: List[List[int]], K: int) -> int: m, n = len(matrix), len(matrix[0]) ans = float("-inf") for row in matrix: for i in range(n - 1): row[i + 1] += row[i] for i in range(n): for j in range(i, n): pres = [0] pre = 0 for k in range(m): pre += matrix[k][j] - (matrix[k][i - 1] if i > 0 else 0) # 尋找大於等於 pre - k 的最小數,且這個數不能比 pre 大。好比 pre = 10, k = 3,就要找大於等於 7 的最小數,這個數不能大於 10。 # 爲了達到這個目的,可使用 bisect_left 來完成。(使用 bisect_right 不包含等號) idx = bisect.bisect_left(pres, pre - K) # 若是 i == len(pre) 表示 pres 中的數都小於 pre - k,也就是說無解 if idx < len(pres): # 由 bisect_left 性質可知 pre - pres[i] >= 0 ans = max(ans, pre - pres[idx]) idx = bisect.bisect_left(pres, pre) pres[idx:idx] = [pre] # 或者將上面兩行代碼替換爲 bisect.insort(pres, pre) return -1 if ans == float("-inf") else ans
複雜度分析
令 n 爲數組長度。
題目給了一個 follow up:若是行數遠大於列數,你將如何解答呢? 實際上,若是行數遠大於列數,由複雜度分析可知空間複雜度會很高。咱們能夠將行列兌換,這樣空間複雜度是 $O(n)$。換句話說,咱們能夠經過行列的調換作到空間複雜度爲 $O(min(m, n))$。
https://leetcode-cn.com/probl...
給出矩陣 matrix 和目標值 target,返回元素總和等於目標值的非空子矩陣的數量。 子矩陣 x1, y1, x2, y2 是知足 x1 <= x <= x2 且 y1 <= y <= y2 的全部單元 matrix[x][y] 的集合。 若是 (x1, y1, x2, y2) 和 (x1', y1', x2', y2') 兩個子矩陣中部分座標不一樣(如:x1 != x1'),那麼這兩個子矩陣也不一樣。 示例 1: 輸入:matrix = [[0,1,0],[1,1,1],[0,1,0]], target = 0 輸出:4 解釋:四個只含 0 的 1x1 子矩陣。 示例 2: 輸入:matrix = [[1,-1],[-1,1]], target = 0 輸出:5 解釋:兩個 1x2 子矩陣,加上兩個 2x1 子矩陣,再加上一個 2x2 子矩陣。 提示: 1 <= matrix.length <= 300 1 <= matrix[0].length <= 300 -1000 <= matrix[i] <= 1000 -10^8 <= target <= 10^8
和上面題目相似。不過這道題是求子矩陣和恰好等於某個目標值的數目。
咱們不妨先對問題進行簡化。好比題目要求的是一維數組中,子數組(連續)的和等於目標值 target 的數目。咱們該如何作?
這很容易,咱們只須要:
因爲僅僅是求數目,不涉及到求具體的子矩陣信息,所以使用相似上面的解法求出二維前綴和。接下來,使用和一維前綴和一樣的方法便可求出答案。
Python3 Code:
class Solution: def numSubmatrixSumTarget(self, matrix, target): m, n = len(matrix), len(matrix[0]) for row in matrix: for i in range(n - 1): row[i + 1] += row[i] ans = 0 for i in range(n): for j in range(i, n): c = collections.defaultdict(int) cur, c[0] = 0, 1 for k in range(m): cur += matrix[k][j] - (matrix[k][i - 1] if i > 0 else 0) ans += c[cur - target] c[cur] += 1 return ans
複雜度分析
和上面同樣,咱們能夠將行列對換,這樣空間複雜度是 $O(n)$。換句話說,咱們能夠經過行列的調換作到空間複雜度爲 $O(min(m, n))$。
力扣的小夥伴能夠關注我,這樣就會第一時間收到個人動態啦~
以上就是本文的所有內容了。你們對此有何見解,歡迎給我留言,我有時間都會一一查看回答。更多算法套路能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 40K star 啦。你們也能夠關注個人公衆號《力扣加加》帶你啃下算法這塊硬骨頭。