极大子矩阵问题

极大子矩阵问题

又名: 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

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