計算矩陣中全1子矩陣的個數

前言

最近被我大哥安利了一道算法題, 這道題說難, 還不至於我做不出來, 說簡單吧, 我還想不到最優解, 等把最優解告訴我之後, 我還正好能理解. 我甚至曾經怯怯的認爲, 這題就是我哥專門給我找的, 嘿嘿, 心中說不出的小歡喜.

題來了, 此題出自力扣, 原題鏈接:

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 來進行解題).

方案一

首先直觀上最先想到的, 就是窮舉了. 一力破十會. 將所有出現的情況遍歷一遍, 然後就能得出總數了. 思路如下:

  1. 利用i, j 將二維數組的所有節點遍歷一遍
  2. 利用m, n將以[i][j]爲左上頂點的子矩陣遍歷一遍
  3. 判斷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了, 如下圖:

image-20200710234204779

在向右遍歷的時候同理, 這樣, 我們就可以確定, 所有遍歷到的值都是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).


還有沒有比三次方更快的解法呢? 可能..大概..或許有吧. 但是我想了好久也沒有想到.

以上, 其實到第二個方案我都想到了, 但是最後一步怎麼都沒邁出去, 原因歸結爲做的少, 遇到的少. 算法題偶爾做做還挺好的, 也不需要很高深的數學知識, 還可以鍛鍊思維, 蠻有趣的, 之後可以抽時間來看看, 嘿嘿.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章