前言
最近被我大哥安利了一道算法題, 這道題說難, 還不至於我做不出來, 說簡單吧, 我還想不到最優解, 等把最優解告訴我之後, 我還正好能理解. 我甚至曾經怯怯的認爲, 這題就是我哥專門給我找的, 嘿嘿, 心中說不出的小歡喜.
題來了, 此題出自力扣, 原題鏈接:
https://leetcode-cn.com/problems/count-submatrices-with-all-ones/
描述: 給你一個只包含 0 和 1 的 rows * columns
矩陣 mat
,請你返回有多少個 子矩形 的元素全部都是 1 。
例子:
輸入:mat = [[1,0,1],
[1,1,0],
[1,1,0]]
輸出:13
解釋:
有 6 個 1x1 的矩形。
有 2 個 1x2 的矩形。
有 3 個 2x1 的矩形。
有 1 個 2x2 的矩形。
有 1 個 3x1 的矩形。
矩形數目總共 = 6 + 2 + 3 + 1 + 1 = 13 。
題意清晰明瞭, 開始嘗試解題(使用 C 來進行解題).
方案一
首先直觀上最先想到的, 就是窮舉了. 一力破十會. 將所有出現的情況遍歷一遍, 然後就能得出總數了. 思路如下:
- 利用
i
,j
將二維數組的所有節點遍歷一遍 - 利用
m
,n
將以[i][j]
爲左上頂點的子矩陣遍歷一遍 - 判斷
i
,j
,m
,n
四個變量確定的矩陣是否爲全1矩陣
代碼實現:
int numSubmat(int** mat, int matSize, int* matColSize){
int result = 0;
// 遍歷所有節點
for (int i = 0; i < matSize; i++) {
for (int j = 0; j < *matColSize; j++) {
// 遍歷當前節點爲左上頂點的所有子矩陣
for (int m = i; m < matSize; m++) {
for (int n = j; n < *matColSize; n++) {
// 判斷當前子矩陣是否爲全1矩陣
int isOk = 1;
for (int p = i; p <= m; p++) {
for (int q = j; q <= n; q++) {
if(mat[p][q] != 1){
isOk = 0;
break;
}
}
if(!isOk) break;
}
// 計算總數
if(isOk) result++;
}
}
}
}
return result;
}
隨手寫個測試用:
#include <stdio.h>
#include <stdlib.h>
int numSubmat(int** mat, int matSize, int* matColSize);
int main() {
// 定義數組長度
int matSize = 3, matColSize = 3;
// 分配數組空間
int **mat = (int **)malloc(matSize*sizeof(int*));
// 動態分配內容
for (int i = 0; i < matSize; i++) {
mat[i] = (int *)malloc(matColSize*sizeof(int));
}
// 給數組填內容, 這裏可以接收鍵盤數組
mat[0][0] = 1;
mat[0][1] = 0;
mat[0][2] = 1;
mat[1][0] = 1;
mat[1][1] = 1;
mat[1][2] = 0;
mat[2][0] = 1;
mat[2][1] = 1;
mat[2][2] = 0;
int result = numSubmat(mat, matSize, &matColSize);
printf("%d", result);
return 0;
}
執行過後, OK, 麼的問題. 看一下時間複雜度呢? 一眼就看到了函數裏的六層循環, 麼的說, O(n^6).
這時, 我大哥說他的時間複雜度是 O(n^3). 那我這小心情, 必須整出來, 再想.
方案二
上面的六層循環中, 能不能想辦法去掉一層呢? 有. 在最後判斷是否全1的循環中, 如果左上的數字是0, 那必然沒有全1子矩陣了
再如果向下找的時候, 碰到0, 那下一列的時候也沒必要超過這裏了, 因爲子矩陣至少有一個0了, 如下圖:
在向右遍歷的時候同理, 這樣, 我們就可以確定, 所有遍歷到的值都是1, 可以將判斷全1的兩層循環去掉. nice. 修改代碼如下:
int numSubmat(int** mat, int matSize, int* matColSize){
int result = 0;
// 遍歷所有節點
for (int i = 0; i < matSize; i++) {
for (int j = 0; j < *matColSize; j++) {
if(mat[i][j] == 0) continue;
int thisMaxColSize = *matColSize; // 當前向右最大值
// 遍歷當前節點爲左上頂點的所有子矩陣
for (int m = i; m < matSize; m++) {
for (int n = j; n < thisMaxColSize; n++) {
if(mat[m][n] == 1) result++;
// 記錄向右的最大值
else thisMaxColSize = n;
}
}
}
}
return result;
}
OK, 經過測試完全麼的問題. 再看看現在的時間複雜度. O(n^4); 比剛纔的六次方, 直接降了兩個數量級. 但是比我大哥還差點意思哈.
方案三
打擾了, 沒有想到O(n^3)的解法. 經過我哥的一番指點, 可以說是豁然開朗. 思路不變. 上面的四層循環, 有沒有什麼辦法能再減少一層呢?
想一下, 我們在第四層循環中, 向右遍歷, 找的是什麼? 是連續1的個數, 如果我們不用向右遍歷, 直接就知道了這個連續1的個數, 那是不是就可以把這一層也省了呢?
那麼問題來了, 如何不遍歷就知道呢? 預處理. 在所有的遍歷之前, 先進行一次遍歷, 把每個節點向右的連續1個數計算好. 這個思路有點妙啊. 廢話不多說, 再來:
int min(int a, int b){
return a > b ? b : a;
}
int numSubmat(int** mat, int matSize, int* matColSize){
// 進行預處理, 將每個節點向右的連續1個數算好(從右下向左上處理)
for (int i = matSize - 1; i >= 0; i--) {
for (int j = *matColSize - 1; j >= 0 ; j--) {
if(mat[i][j] == 0) continue;
// 最右側不處理
if(j == *matColSize-1) continue;
// 每個節點的數字等於右邊的加1
mat[i][j] = mat[i][j+1] + 1;
}
}
int result = 0;
// 遍歷所有節點
for (int i = 0; i < matSize; i++) {
for (int j = 0; j < *matColSize; j++) {
if(mat[i][j] == 0) continue;
int thisMaxColSize = *matColSize; // 當前向右最大值
// 遍歷當前節點爲左上頂點的所有子矩陣
for (int m = i; m < matSize; m++) {
// 記錄向右的最大值
thisMaxColSize = min(thisMaxColSize, mat[m][j]);
result += thisMaxColSize;
}
}
}
return result;
}
再看時間複雜度, 終於, O(n^3).
還有沒有比三次方更快的解法呢? 可能..大概..或許有吧. 但是我想了好久也沒有想到.
以上, 其實到第二個方案我都想到了, 但是最後一步怎麼都沒邁出去, 原因歸結爲做的少, 遇到的少. 算法題偶爾做做還挺好的, 也不需要很高深的數學知識, 還可以鍛鍊思維, 蠻有趣的, 之後可以抽時間來看看, 嘿嘿.