1504. 统计全 1 子矩形
大约 6 分钟
--- 

1504. 统计全 1 子矩形
- 标签:栈、数组、动态规划、矩阵、单调栈
- 难度:中等
题目链接
题目大意
描述:给定一个 的二进制矩阵 。
要求:返回矩阵中全部由 组成的子矩形数量。
说明:
- 。
- 为 或 。
示例:
- 示例 1:

输入:mat = [[1,0,1],[1,1,0],[1,1,0]]
输出:13
解释:
有 6 个 1x1 的矩形。
有 2 个 1x2 的矩形。
有 3 个 2x1 的矩形。
有 1 个 2x2 的矩形。
有 1 个 3x1 的矩形。
矩形数目总共 = 6 + 2 + 3 + 1 + 1 = 13。- 示例 2:

输入:mat = [[0,1,1,0],[0,1,1,1],[1,1,1,0]]
输出:24
解释:
有 8 个 1x1 的子矩形。
有 5 个 1x2 的子矩形。
有 2 个 1x3 的子矩形。
有 4 个 2x1 的子矩形。
有 2 个 2x2 的子矩形。
有 2 个 3x1 的子矩形。
有 1 个 3x2 的子矩形。
矩形数目总共 = 8 + 5 + 2 + 4 + 2 + 2 + 1 = 24。解题思路
思路 1:逐行压缩 + 单调栈 DP
1. 核心思想
将问题转化为直方图中全 1 子矩形计数问题。
对于每一行,把该行及以上连续的 视为直方图的高度。 表示第 列从当前行向上连续 的个数。
遍历每一行,统计以当前行为底边的所有全 子矩形数量,累加即可。
关键子问题:给定直方图 (每个柱子的高度),如何统计所有全 子矩形?
2. 子矩形计数递推公式
定义 为以当前行第 列为右下角的全 子矩形数量。即子矩形的底边在某行第 列结束(可以向左延伸),顶部由 的最小值决定。
对于 数组, 的计算依赖于左侧第一个高度 的位置 :
- 以 为右下角的子矩形,可以分为两类:
- 底边右端跨越到 列(含)左侧的子矩形:这些子矩形的计数就是 。
- 底边右端在 到 之间的子矩形:最小高度为 ,宽度为 ,共 个。
因此递推公式为:
3. 单调栈优化
使用单调递增栈快速找到左侧第一个高度 当前高度的位置。栈中存储柱子下标,保证 。
4. 具体步骤
第 1 步:初始化 ,。
第 2 步:遍历每一行 :
- 更新 :
- 如果 ,。
- 如果 ,。
- 用单调栈计算 数组:
- 清空栈。
- 遍历 从 到 :
- 当栈非空且 时弹出。
- 如果栈空:。
- 如果栈非空:。
- 将 入栈。
- 。
第 3 步:返回 。
5. 举例说明
以 为例:
第 0 行:
- :栈空,,入栈 ,
- :,弹出 。栈空,,入栈 ,
- :,不弹出。,入栈 ,
本行贡献 。
第 1 行:
- :栈空,,入栈 ,
- :,弹出 。栈空,,入栈 ,
- :,弹出 。栈空,,入栈 ,
本行贡献 。累计 。
第 2 行:
- :栈空,,入栈 ,
- :,弹出 。栈空,,入栈 ,
- :,弹出 。栈空,,入栈 ,
本行贡献 。累计 。
最终结果 。
6. 正确性理解
每个 代表以 为右下角的全 子矩形数量:
- 左侧第一个 的位置是 ,意味着在 范围内, 就是最小高度。
- 以 为右下角、底边在 范围内结束的子矩形,高度最多为 ,共有 个。
- 底边跨越到 左侧的子矩形数量已经包含在 中(因为高度限制由柱 处的值决定,不受 影响)。
- 如果栈为空,说明 是当前行最矮的,底边从 到 的子矩形高度均为 ,共 个。
思路 1:代码
from typing import List
class Solution:
def numSubmat(self, mat: List[List[int]]) -> int:
m, n = len(mat), len(mat[0])
heights = [0] * n
ans = 0
for i in range(m):
# 更新高度数组
for j in range(n):
if mat[i][j] == 1:
heights[j] += 1
else:
heights[j] = 0
# 单调栈计算以当前行为底边的子矩形数量
stack = []
dp = [0] * n
for j in range(n):
while stack and heights[stack[-1]] >= heights[j]:
stack.pop()
if stack:
dp[j] = dp[stack[-1]] + heights[j] * (j - stack[-1])
else:
dp[j] = heights[j] * (j + 1)
stack.append(j)
ans += dp[j]
return ans思路 1:复杂度分析
- 时间复杂度:,每行单调栈均摊 。
- 空间复杂度:,高度数组、栈和 DP 数组。
思路 2:逐行 DP(枚举底边起点)
1. 核心思想
更直观的方法:对于每一行,枚举所有可能的子矩形底边区间 ,维护该区间在当前行的最小高度。累加最小高度即得到以该区间为底边的子矩形数量。
2. 具体步骤
遍历每一行 ,更新 。对于每个位置 ,从 向左扩展左边界 ,同时维护区间 的最小高度 ,累加 。
from typing import List
class Solution:
def numSubmat(self, mat: List[List[int]]) -> int:
m, n = len(mat), len(mat[0])
heights = [0] * n
ans = 0
for i in range(m):
for j in range(n):
heights[j] = heights[j] + 1 if mat[i][j] else 0
for j in range(n):
min_h = heights[j]
for l in range(j, -1, -1):
min_h = min(min_h, heights[l])
if min_h == 0:
break
ans += min_h
return ans思路 2:复杂度分析
- 时间复杂度:。 时最坏约 次操作,可以接受。
- 空间复杂度:。