極大子矩陣問題
又名: 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: 淺談用極大化思想解決最大子矩陣問題
- 文章2:演算法筆記求面積最大的全1 矩陣 這個文章可以說是很生動的解釋了這個問題
思路
-
文章1中提到的懸線的思想。
-
有效豎線:除了兩個端點外,不覆蓋任何障礙點的豎直線段。
懸線:上端點覆蓋了一個障礙點或達到整個矩形上端的有效豎線。
-
通過枚舉所有的懸線,就可以枚舉出所有的極大子矩形。由於每個懸線都與它底部的那個點一一對應,所以懸線的個數=(n-1)×m(以矩形中除了頂部的點以外的每個點爲底部,都可以得到一個懸線,且沒有遺漏)。如果能做到對每個懸線的操作時間都爲O(1),那麼整個算法的複雜度就是O(NM)。
-
-
文章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