極大子矩陣問題

極大子矩陣問題

又名: 01矩陣問題 極大全1矩陣

題目

Description

給定一個矩形區域,每一個位置上都是1或0,求該矩陣中每一個位置上都是1的最大子矩形區域中的1的個數。

Input

輸入第一行爲測試用例個數。每一個用例有若干行,第一行爲矩陣行數n和列數m,下面的n行每一行是用空格隔開的0或1。

Output

輸出一個數值。

Sample Input 1

1
3 4
1 0 1 1
1 1 1 1
1 1 1 0

Sample Output 1

6
參考文章
  1. 文章1: 淺談用極大化思想解決最大子矩陣問題
  2. 文章2:演算法筆記求面積最大的全1 矩陣 這個文章可以說是很生動的解釋了這個問題
思路
  1. 文章1中提到的懸線的思想。

    1. 有效豎線:除了兩個端點外,不覆蓋任何障礙點的豎直線段。

      懸線:上端點覆蓋了一個障礙點或達到整個矩形上端的有效豎線。

    2. 通過枚舉所有的懸線,就可以枚舉出所有的極大子矩形。由於每個懸線都與它底部的那個點一一對應,所以懸線的個數=(n-1)×m(以矩形中除了頂部的點以外的每個點爲底部,都可以得到一個懸線,且沒有遺漏)。如果能做到對每個懸線的操作時間都爲O(1),那麼整個算法的複雜度就是O(NM)。

  2. 文章2中提到的最好的演算法,利用一個棧的結構,可以方便快速的找到子矩陣

    利用一個 stack ,宛如判斷括號對稱,找出矩形的左右邊界。

注意

代碼的理解請參考文章2中最好的演算法,有一個直觀的圖表.

其中有一個注意點,在最好的驗算法第13-3的位置

13-3.
「高度1」放入堆疊。可以想成:「高度1」比目前堆疊頂端還大。
注意到,「位置1」沿用上一個彈出的位置

這個沿用上一個彈出的位置是什麼呢

比如: 3 2 3 0 2 1 這個序列 在棧中有3的時候,遇到了2,因此要彈出3 ,計算的面積爲3 *(2-1) =3

然後放入2,然後在遇到3,則放入3,

然後遇到0的時候,彈出3 面積爲3 *(4-3) =3,然後要彈出2,這裏注意:

要計算的一定是 2*(4 -1) =6, 而不是2 * (4-2) = 4 因此在存入2的下標的時候,要更新爲之前彈出的3的下標.

但是

當遍歷到0的時候,要重置那個之前記錄下來彈出的下標,因爲0相當於一個障礙點,

比如說繼續遍歷,遇到0 ,又遇到2,此時棧空,放入2,又遇到1,因爲1<2,彈出2,這時候計算面積 2* (6-5)=2 ,如果遇到0的時候沒有清空下標的話,則計算的是2*(6-1)=10 就出現了錯誤

代碼
if __name__ == '__main__':

    # read data
    case_num = [int(x) for x in input().split(" ")][0]  #測試用例個數
    while case_num > 0:

        temp = [int(x) for x in input().split(" ")]  
        m = temp[0]  # 矩陣長
        n = temp[1]  # 矩陣寬
        matrix = []  # 原始矩陣

        for i in range(m):
            row = [int(x) for x in input().split(" ")]
            matrix.append(row)
        h_arr = [[0] * n for _ in range(m)]  
        h_arr[0] = matrix[0] # 初始化豎直條(懸線)矩陣
        # print(h)

        # # 初始化豎直條(懸線)長度
        for i in range(1, m):
            for j in range(n):
                if matrix[i][j] == 1:
                    h_arr[i][j] = h_arr[i - 1][j] + 1

		# 核心算法:
        # 計算答案
        ans = 0  #
        stack = []

        for i in range(m - 1, -1, -1):
            for j in range(n):
                cur = h_arr[i][j]
                if len(stack) == 0:
                    stack.append((cur, j))
                    # print(cur)
                else:
                    pre_j = None
                    while len(stack) != 0 and cur < stack[-1][0]:  # 如果當前值小於棧頂,就一直彈棧
                        # print("{}, {}, stack : {}".format(i, j, stack))
                        h, pre_j = stack.pop()
                        area = h * (j - pre_j)  # 每次彈棧後都計算面積
                        ans = max(ans, area)
                        # print("area :{}".format(area))
                    # 如果當前值大於棧頂,就加入到棧中
                    # 如果棧中沒元素並且當前值不爲0
                    if cur != 0:
                        # 注意pre_j和j的區別
                        if len(stack) == 0:  # 如果空棧了,則添加的是上一次彈棧的j值, 即: 沿用上一個彈出的位置。
                            stack.append((cur, pre_j))
                        elif cur > stack[-1][0]:
                            stack.append((cur, j))  # 如果沒空棧,則添加的是這次的j值
                    else:
                        # 如果當前遇到了0,則要更新pre_j值,即:重置上一次的座標
                        pre_j = j
                if j == n - 1:  # 最後一輪結束後若棧中有剩餘
                    while len(stack) != 0:
                        h, pre_j = stack.pop()
                        area = h * (j + 1 - pre_j)  # 每次彈棧後都計算面積
                        ans = max(ans, area)
                        # print("final {}".format(area))
        print(ans)
        case_num -= 1

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