最大黑方阵

标签: 数组 动态规划 矩阵

难度: Medium

给定一个方阵,其中每个单元(像素)非黑即白。设计一个算法,找出 4 条边皆为黑色像素的最大子方阵。

返回一个数组 [r, c, size] ,其中 rc 分别代表子方阵左上角的行号和列号,size 是子方阵的边长。若有多个满足条件的子方阵,返回 r 最小的,若 r 相同,返回 c 最小的子方阵。若无满足条件的子方阵,返回空数组。

示例 1:

输入:
[
   [1,0,1],
   [0,0,1],
   [0,0,1]
]
输出: [1,0,2]
解释: 输入中 0 代表黑色,1 代表白色,标粗的元素即为满足条件的最大子方阵

示例 2:

输入:
[
   [0,1,1],
   [1,0,1],
   [1,1,0]
]
输出: [0,0,1]

提示:

  • matrix.length == matrix[0].length <= 200

Submission

运行时间: 76 ms

内存: 17.7 MB

class Solution:
    def findSquare(self, matrix: List[List[int]]) -> List[int]:
        m, n = len(matrix), len(matrix[0])
        row = [[0] * n for _ in range(m)]
        col = [[0] * n for _ in range(m)]
        res = [m-1, n-1, float("-inf")]
        # 状态转移
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    row[i][j] = row[i][j-1] + 1 if j else 1
                    col[i][j] = col[i-1][j] + 1 if i else 1
                    cur_l = min(row[i][j], col[i][j])
                    while cur_l > res[-1]:
                        x = i - cur_l + 1
                        y = j - cur_l + 1
                        if row[x][j] >= cur_l and col[i][y] >= cur_l:
                            res = [x, y, cur_l]
                            break
                        cur_l -= 1
        return res if res[-1] != float("-inf") else []

Explain

这个题解使用动态规划的思路解决问题。首先,定义两个矩阵 row 和 col,其中 row[i][j] 表示从 (i, j) 位置向左延伸的连续黑色像素的最大数目,col[i][j] 表示从 (i, j) 位置向上延伸的连续黑色像素的最大数目。然后,对于每个为黑色的像素 (即 matrix[i][j] == 0),计算以该点为右下角的最大可能的黑色子方阵。为此,获取从该点向左和向上延伸的黑色像素的最小值(因为方阵需要两边长度相等),并验证这一长度是否可以构成一个有效的子方阵。验证方法是检查顶部水平边和左侧垂直边是否同样拥有至少这么长的连续黑色像素。如果找到更大的黑色子方阵,更新结果数组 res。这种方法确保了最终找到的是最大的且最左上角的黑色子方阵。

时间复杂度: O(m*n*min(m, n))

空间复杂度: O(m*n)

class Solution:
    def findSquare(self, matrix: List[List[int]]) -> List[int]:
        m, n = len(matrix), len(matrix[0])
        row = [[0] * n for _ in range(m)]  # 存储从当前点向左连续黑色像素数
        col = [[0] * n for _ in range(m)]  # 存储从当前点向上连续黑色像素数
        res = [m-1, n-1, float('-inf')]  # 初始化结果为无效值
        # 状态转移
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:  # 仅考虑黑色像素
                    row[i][j] = row[i][j-1] + 1 if j else 1  # 更新左侧连续黑色像素计数
                    col[i][j] = col[i-1][j] + 1 if i else 1  # 更新上方连续黑色像素计数
                    cur_l = min(row[i][j], col[i][j])  # 确定最大可能的方阵边长
                    while cur_l > res[-1]:  # 只有在找到更大的方阵时才进行检查
                        x = i - cur_l + 1
                        y = j - cur_l + 1
                        if row[x][j] >= cur_l and col[i][y] >= cur_l:  # 检查顶部和左侧边界
                            res = [x, y, cur_l]
                            break
                        cur_l -= 1
        return res if res[-1] != float('-inf') else []  # 检查是否有有效方阵

Explore

在这个问题中,我们寻找的是最大的全黑色子方阵,即方阵内的所有像素都是黑色(值为0)。因此,只有当 `matrix[i][j]` 为 0 时,该位置才可能是某个黑色子方阵的一部分。如果 `matrix[i][j]` 是 1,即白色像素,那么它不能是任何黑色子方阵的一部分,从这个位置开始向左或向上的连续黑色像素数自然为 0。更新 `row` 和 `col` 数组时忽略白色像素是因为它们不会对寻找最大黑色子方阵产生贡献。

从当前可能的最大边长开始向下逐渐检查的策略是为了尽快找到最大的子方阵。因为我们的目标是找到最大的黑色子方阵,如果从最大边长开始检查,并且立即找到一个有效的方阵,就可以直接确定这是当前位置可以形成的最大方阵,无需检查更小的方阵。如果从最小边长开始检查,则可能需要多次验证直到找到最大边长,这样做效率较低。此外,从大到小的检查可以在发现较大的子方阵时立即停止进一步检查,从而提高算法的执行效率。

在题解的实现中,由于是按照从左到右、从上到下的顺序遍历矩阵的每个元素,并且只有在找到更大的方阵时才更新结果数组 `res`,所以这种策略确保了如果有多个相同大小的最大方阵时,选择的是最左上角的方阵。这是因为一旦在某个位置找到了目前为止的最大方阵,随后遍历到的任何具有相同边长的方阵都不会更新结果,除非它们有更大的边长。因此,这种更新策略确保了最终的结果是最左上角的最大方阵。