題目要求
Given a non-empty 2D matrix matrix and an integer k, find the max sum of a rectangle in the matrix such that its sum is no larger than k.
Example:
Input: matrix = [[1,0,1],[0,-2,3]], k = 2
Output: 2
Explanation: Because the sum of rectangle [[0, 1], [-2, 3]] is 2,
and 2 is the max number no larger than k (k = 2).
Note:
1. The rectangle inside the matrix must have an area > 0.
2. What if the number of rows is much larger than the number of columns?
現有一個由整數構成的矩陣,問從中找到一個子矩陣,要求該子矩陣中各個元素的和爲不超過k的最大值,問子矩陣中元素的和爲多少?
注:後面的文章中將使用[左上角頂點座標,右下角頂點座標]
來表示一個矩陣,如[(1,2),(3,4)]
表示左上角頂掉座標爲(1,2),右下角頂點座標爲(3,4)的矩陣。用S[(1,2),(3,4)]
表示該矩陣的面積。頂點的座標系以數組的起始點作爲起點,向下爲x軸正方向,向右爲y軸正方向。
思路一:暴力循環
如果我們將矩陣中的每個子矩陣都枚舉出來,並計算其元素和,從而得出小於K的最大值即可。
這裏通過一個額外的二維數組S緩存了[(0,0), (i,j)]
的矩形的面積,可以通過O(n^2)的時間複雜度完成計算,即S[i][j] = matrix[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1]
, 則矩形[(r1,c1),(r2,c2)]
的面積爲S[r2][c2] -S[r1-1][c2] - S[r2][c1-1] + S[r1-1][c1-1]
。這種算法的時間複雜度爲O(N^4),因爲需要定位矩形的四個頂點,一共需要四圈循環,代碼如下:
public int maxSumSubmatrix(int[][] matrix, int k) {
int row = matrix.length;
if(row == 0) return 0;
int col = matrix[0].length;
if(col == 0) return 0;
//rectangle[i][j]記錄頂點爲[0,0],[i,j]的矩形的面積
int[][] rectangle = new int[row][col];
for(int i = 0 ; i<row ; i++) {
for(int j = 0 ; j<col ; j++) {
int area = matrix[i][j];
if(i>0) {
area += rectangle[i-1][j];
}
if(j>0) {
area += rectangle[i][j-1];
}
//減去重複計算的面積
if(i>0 && j>0) {
area -= rectangle[i-1][j-1];
}
rectangle[i][j] = area;
}
}
int result = Integer.MIN_VALUE;
for(int startRow = 0 ; startRow<row; startRow++) {//矩形的起點行
for(int endRow = startRow ; endRow<row ; endRow++) {//矩形的結束行
for(int startCol = 0 ; startCol<col ; startCol++) {//矩形的起始列
for(int endCol = startCol ; endCol<col ; endCol++) {//矩形的結束列
int area = rectangle[endRow][endCol];
if(startRow > 0) {
area -= rectangle[startRow-1][endCol];
}
if(startCol > 0) {
area -= rectangle[endRow][startCol-1];
}
if(startRow > 0 && startCol > 0) {
area += rectangle[startRow-1][startCol-1];
}
if (area <= k)
result = Math.max(result, area);
}
}
}
}
return result;
}
思路二:利用二分法思路進行優化
對算法從時間上優化的核心思路就是儘可能的減少比較或是計算的次數。上面一個思路的我們可以理解爲以row1和row2分別作爲子矩陣的上邊界和下邊界,以col2作爲右邊界,要求找到一個左邊界col1,使得其劃分出來的子矩陣中元素的和爲小於等於k的最大值,即
max(S[(row1,0),(row2, col2)] - S[(row1,0),(row2, col1)])
&& col1 < col2
&& S[(row1,0),(row2, col2)] - S[(row1,0),(row2, col1)]<k`。
換句話說,假如將col2左側的所有以最左側邊爲起點的子矩陣按照元素和從小到大排隊,即將子矩陣(row1, 0), (row2, colx) 其中colx < col2
按照元素和從小到大排序,此時只需要在該結果中找到一個矩陣,其值爲大於等於S[(row1,0),(row2, col2)] - k
的最小值。此時得出的矩陣元素和差最大。這裏採用TreeSet來實現O(logN)的元素查找時間複雜度。
代碼如下:
public int maxSumSubmatrix2(int[][] matrix, int k) {
int row = matrix.length;
if(row == 0) return 0;
int col = matrix[0].length;
if(col == 0) return 0;
//rectangle[i][j]記錄頂點爲[0,0],[i,j]的矩形的面積
int[][] rectangle = new int[row][col];
for(int i = 0 ; i<row ; i++) {
for(int j = 0 ; j<col ; j++) {
int area = matrix[i][j];
if(i>0) {
area += rectangle[i-1][j];
}
if(j>0) {
area += rectangle[i][j-1];
}
//減去重複計算的面積
if(i>0 && j>0) {
area -= rectangle[i-1][j-1];
}
rectangle[i][j] = area;
}
}
int result = Integer.MIN_VALUE;
for(int startRow = 0 ; startRow < row ; startRow++) {
for(int endRow = startRow ; endRow < row ; endRow++) {
//記錄從startRow到endRow之間所有以最左側邊爲起點的矩形的面積
TreeSet<Integer> treeSet = new TreeSet<Integer>();
treeSet.add(0);
for(int endCol = 0 ; endCol < col ; endCol++) {
int area = rectangle[endRow][endCol];
if(startRow > 0) {
area -= rectangle[startRow-1][endCol];
}
//可以減去的左側矩形的最小面積,即大於S[(row1,0),(row2, col2)] - k的最小值
Integer remain = treeSet.ceiling(area - k);
if(remain != null) {
result = Math.max(result, area - remain);
}
treeSet.add(area);
}
}
}
return result;
}
思路三:分治法
從上面兩種思路,我們可以將題目演化成另一種思路,即對於任意以row1和row2作爲上下邊界的子矩陣,將其中每一列的元素的和記爲sum[colx](0<=colx<col)
,則生成一個長度爲col的整數數組sum。需要從該整數數組中找到一個連續的子數組,使得該子數組的和最大且不超過k。
連續子數組的和是一道非常經典的動態規劃的問題,它可以在nlogn的時間複雜度內解決。這裏採用歸併排序的思路來進行解決。本質上將數組以中間位置分割爲左子數組和右子數組,分別求左子數組內和右子數組內最大的連續子數組和,並且在遞歸的過程中將左右子數組中的元素分別從小到大排序。接着判斷是否有橫跨中間節點的子數組滿足題目要求,因爲左右子數組分別有序,因此一旦遇到一個右邊界,其和左邊界構成的矩陣的元素的和超過k,就可以停止右指針的移動。因此每次中間結果的遍歷只需要O(N)的時間複雜度。
代碼如下:
public int maxSumSubmatrix3(int[][] matrix, int k) {
int row = matrix.length;
if(row == 0) return 0;
int col = matrix[0].length;
if(col == 0) return 0;
int result = Integer.MIN_VALUE;
int[] sums = new int[row+1];//sums[i]記錄startCol到endCol列之間,0行到i行構成的矩陣的面積
for(int startCol = 0 ; startCol<col ; startCol++) {
int[] sumInRow = new int[row];//sumInRow[i]記錄startCol到endCol列之間第i行所有元素的和
for(int endCol = startCol; endCol<col ; endCol++) {
for(int endRow = 0 ; endRow<row ; endRow++) {
sumInRow[endRow] += matrix[endRow][endCol];
sums[endRow+1] = sums[endRow] + sumInRow[endRow];
}
//對startCol到endCol列之間所有的矩陣元素和構成的數組通過分治法找到最大的連續子數組
result = Math.max(result, mergeSort(sums, k, 0, sums.length));
if(result == k) return k;
}
}
return result;
}
public int mergeSort(int[] sums, int k, int start, int end) {
//矩陣數組至少包含一個元素
if(end <= start + 1) return Integer.MIN_VALUE;
int mid = start + (end - start)/2, cacheIndex = 0;
//對左側遞歸計算,此時sums數組[start,mid)之間的元素已經有序
int ans = mergeSort(sums, k, start, mid);
if(ans == k) return k;
//對右側遞歸計算,此時sums數組[mid,end)之間的元素已經有序
ans = Math.max(ans, mergeSort(sums, k, mid, end));
//緩存sums數組[start,end)之間排序的結果
int[] sortedSubSums = new int[end - start];
if(ans == k) return k;
for(int i = start, j = mid, m = mid ; i<mid ; i++) {
while(j<end && sums[j] - sums[i] <= k) j++;//找到第一個滿足[i,j)之間的元素和大於k,[i,j-1)之間的元素和小於等於k的連續子數組
if(j > mid) {
ans = Math.max(sums[j-1] - sums[i], ans);
if (ans == k) return k;
}
while(m<end && sums[m] < sums[i]) sortedSubSums[cacheIndex++] = sums[m++];//排序,通過每次將中間位置右側比左側當前位置小的元素全部複製有序數組緩存中
sortedSubSums[cacheIndex++] = sums[i];
}
System.arraycopy(sortedSubSums, 0, sums, start, cacheIndex);
return ans;
}