极大子矩阵问题
又名: 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