统计全为 1 的正方形子矩阵 图解题解
这道题到底在问什么
- 输入
- matrix = [[0,1,1,1],[1,1,1,1],[0,1,1,1]]
- 输出
- 15(包含 1x1、2x2、3x3)
- 输入
- matrix = [[1,0,1],[1,1,0],[1,1,0]]
- 输出
- 7
- 输入
- matrix = [[1]]
- 输出
- 1(单个 1)
最优解:为什么这么做
一句话答案:LeetCode 1277 统计全为 1 的正方形子矩阵:dp[i][j]=以 (i,j) 为右下角的最大正方形边长=min(上、左、左上)+1;边长值即该格贡献的正方形个数,全表求和即答案。O(m·n) 时间、O(1) 空间。
统计全 1 正方形,连 1x1 都要数吗
给一张只含 0 和 1 的矩阵,数出所有全是 1 的正方形子矩阵有多少个。边长 1、2x2、3x3 都算,只要盖住的格子全是 1 就计一个。以 matrix=[[0,1,1,1],[1,1,1,1],[0,1,1,1]] 为例,这是一张 m 行 n 列的矩阵(m=3、n=4),答案 15,要的是总个数。矩阵里用 (r,c) 指第 r 行第 c 列,行列号都从 0 数起。
边长最大 min(m,n),逐尺寸验为什么算不过账
一张 m×n 矩阵里,边长 k 的正方形有 (m-k+1)(n-k+1) 个,k 从 1 试到 min(m,n)。逐个摆出来验证是不是全 1,哪怕验大一号时只补看新添的一行一列、不整块重扫,总成本也到 O(m·n·min(m,n)²) 量级——大 O 记号描述矩阵变大时计算量怎么涨。
每格记一个数,为什么全表一加就是答案
与其枚举正方形,不如给每格记一个数。定义 dp[r][c] 为「以 (r,c) 为右下角能凑出的最大全 1 正方形边长」,这个「把局部结果存下、后面直接取用不重算」的数就是动态规划里的状态。它还藏第二层意思:以 (r,c) 收尾、边长 1 到 dp[r][c] 的正方形各一个,恰好贡献 dp[r][c] 个,全表相加就是总数。0 格当不了右下角,dp 记 0。
凭什么取上、左、左上三者最小再加一
一个是 1 的格子想当边长 k 正方形的右下角,正上、正左、左上必须都已能撑出边长至少 k-1 的正方形,少一个都塌。所以 dp[r][c]=min(dp[r-1][c], dp[r][c-1], dp[r-1][c-1])+1,这条由三个邻居推出本格的式子就是转移式(由已算好的格子推出当前格)。谁最小就卡死本格能撑多大。首行首列的 1 没有左上空间,只能记 1。
拿示例 15 那张表逐格填一遍
从左上往右下逐格填。首行首列的 1 各记 1,答案累到 4。dp[1][1] 三邻居上 1、左 1、左上 0,取最小 0 加一得 1;同理 dp[1][2] 上 1、左 1、左上 1 得 2、dp[1][3] 上 1、左 2、左上 1 得 2。(2,0) 是 0 记 0;dp[2][1] 得 1;dp[2][2] 上 2、左 1、左上 1 得 2;dp[2][3] 三邻居都是 2 得 3。全表十二格 0、1、1、1、1、1、2、2、0、1、2、3 相加正好 15。
累加时每个 1 只记 1,答案为什么会缩水
把 ans 写成「每见一个 1 就加 1」是最常见的写错:只数了 1x1,嵌在里面的 2x2、3x3 全漏了,示例从 15 缩成 10(即 1 的个数),累加的必须是 dp 值即边长本身。每格只看三个已算好的邻居,遍历一遍 O(m·n) 时间;原地改 matrix 当 dp 表,额外空间 O(1)(不许改输入则另开表 O(m·n))。边界:全 0 矩阵答案 0,单格 [[1]] 是 1;0 格必须钉死为 0、不参与 min 加一,先判 matrix[r][c] 为 1 且 r、c 大于 0 再改写。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3一句话套路:每个 1 格子看它正上方、正左方、左上方三个邻居,谁最小就卡住了能凑多大的正方形,取最小值加一。下面逐格填表。
- 4先看骨架:行头是行下标 r0 到 r2,列头是列下标 c0 到 c3。表里现在显示的是原始输入(0 或 1)。我们会从左上往右下,逐格把每个 1 改写成「以它为右下角的最大正方形边长」,同时累加答案。
- 5格子 (0,0) 本身是 0,没法当任何全 1 正方形的右下角,dp[0][0]=0,贡献 0。答案累计仍是 0。
- 6格子 (0,1) 是 1,但它在首行,上方或左侧没有空间,最大只能凑出 1x1 正方形,dp[0][1]=1,贡献 1。答案累计 = 1。
- 7格子 (0,2) 是 1,但它在首行,上方或左侧没有空间,最大只能凑出 1x1 正方形,dp[0][2]=1,贡献 1。答案累计 = 2。
- 8格子 (0,3) 是 1,但它在首行,上方或左侧没有空间,最大只能凑出 1x1 正方形,dp[0][3]=1,贡献 1。答案累计 = 3。
- 9格子 (1,0) 是 1,但它在首列,上方或左侧没有空间,最大只能凑出 1x1 正方形,dp[1][0]=1,贡献 1。答案累计 = 4。
- 10算 dp[1][1]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[0][1]=1,左方 dp[1][0]=1,左上 dp[0][0]=0。能凑出的正方形被这三个里最小的那个卡住。
- 11三者最小值是 0,加一得 dp[1][1]=1。这格作为右下角能凑出边长 1 到 1 的正方形,共 1 个,累加进答案。答案累计 = 5。
- 12算 dp[1][2]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[0][2]=1,左方 dp[1][1]=1,左上 dp[0][1]=1。能凑出的正方形被这三个里最小的那个卡住。
- 13三者最小值是 1,加一得 dp[1][2]=2。这格作为右下角能凑出边长 1 到 2 的正方形,共 2 个,累加进答案。答案累计 = 7。
- 14算 dp[1][3]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[0][3]=1,左方 dp[1][2]=2,左上 dp[0][2]=1。能凑出的正方形被这三个里最小的那个卡住。
- 15三者最小值是 1,加一得 dp[1][3]=2。这格作为右下角能凑出边长 1 到 2 的正方形,共 2 个,累加进答案。答案累计 = 9。
- 16格子 (2,0) 本身是 0,没法当任何全 1 正方形的右下角,dp[2][0]=0,贡献 0。答案累计仍是 9。
- 17算 dp[2][1]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[1][1]=1,左方 dp[2][0]=0,左上 dp[1][0]=1。能凑出的正方形被这三个里最小的那个卡住。
- 18三者最小值是 0,加一得 dp[2][1]=1。这格作为右下角能凑出边长 1 到 1 的正方形,共 1 个,累加进答案。答案累计 = 10。
- 19算 dp[2][2]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[1][2]=2,左方 dp[2][1]=1,左上 dp[1][1]=1。能凑出的正方形被这三个里最小的那个卡住。
- 20三者最小值是 1,加一得 dp[2][2]=2。这格作为右下角能凑出边长 1 到 2 的正方形,共 2 个,累加进答案。答案累计 = 12。
- 21算 dp[2][3]:本格是 1,点亮它的三个邻居(蓝格)。上方 dp[1][3]=2,左方 dp[2][2]=2,左上 dp[1][2]=2。能凑出的正方形被这三个里最小的那个卡住。
- 22三者最小值是 2,加一得 dp[2][3]=3。这格作为右下角能凑出边长 1 到 3 的正方形,共 3 个,累加进答案。答案累计 = 15。
- 23整张表填满了。每个格子里的数就是「以它为右下角的全 1 正方形个数」,把全表的 dp 值加起来 = 15,正是答案。绿格就是所有能当右下角的格子。
- 24复盘整条思路:dp[r][c] = 以 (r,c) 为右下角的最大全 1 正方形边长,也等于该格贡献的正方形个数;1 格取上、左、左上三者最小值加一,0 格记 0;逐格相加即总数 15。
⚠️ 容易写错的地方
✗ 错:把 dp 当成「正方形个数」直接累加邻居
✓ 对:dp 存的是边长,答案是把所有边长值相加
边长 dp[r][c] 时,边长 1 到 dp[r][c] 都成立,恰好 dp[r][c] 个,累加的就是边长
✗ 错:内部转移漏看左上,或首行首列越界
✓ 对:取上、左、左上三者最小值;r==0 或 c==0 时保持原值
漏看左上会漏掉对角线限制;首行首列做 r-1/c-1 会负下标越界
✗ 错:本格是 0 还去算 min
✓ 对:只有本格是 1 才转移,0 格固定为 0
0 格不可能当全 1 正方形的右下角,强行加一会凭空造出正方形
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def countSquares(self, matrix: List[List[int]]) -> int:
m, n = len(matrix), len(matrix[0])
ans = 0
for r in range(m):
for c in range(n):
if matrix[r][c] and r and c:
matrix[r][c] = min(matrix[r-1][c], matrix[r][c-1], matrix[r-1][c-1]) + 1
ans += matrix[r][c]
return ansC++
#include <vector>
using namespace std;
class Solution {
public:
int countSquares(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size(), ans = 0;
for (int r = 0; r < m; ++r) for (int c = 0; c < n; ++c) {
if (matrix[r][c] && r && c) matrix[r][c] = min({matrix[r-1][c], matrix[r][c-1], matrix[r-1][c-1]}) + 1;
ans += matrix[r][c];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int countSquares(int[][] matrix) {
int m = matrix.length, n = matrix[0].length, ans = 0;
for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) {
if (matrix[r][c] == 1 && r > 0 && c > 0) matrix[r][c] = Math.min(matrix[r - 1][c], Math.min(matrix[r][c - 1], matrix[r - 1][c - 1])) + 1;
ans += matrix[r][c];
}
return ans;
}
}复杂度
时间
O(m·n)
每个格子只看三个已算好的邻居,常数次计算,遍历一遍矩阵
空间
O(1)
原地改写 matrix 当 dp 表,不额外开空间;若不许改输入则 O(m·n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计全为 1 的正方形子矩阵 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和 LeetCode 221 最大正方形是什么关系?+
两题共用同一条转移式 dp[r][c]=min(上、左、左上)+1,dp 都表示以该格为右下角的最大全 1 正方形边长。差别只在最后一步怎么用这张表:221 求最大正方形的面积,取全表 dp 的最大值再平方;1277 求全 1 正方形的个数,把全表 dp 值直接相加。会了 221,1277 只需把「取最大再平方」换成「全部求和」,连转移都不用改。
为什么一格的边长值就等于它贡献的正方形个数?+
设 dp[r][c]=3,说明以 (r,c) 为右下角最大能凑出边长 3 的全 1 正方形。那么边长 1、2、3 的正方形各有且仅有一个,都以 (r,c) 收尾且都是全 1(大正方形全 1,套在它右下角的小正方形自然也全 1),一共 3 个。推广开,dp[r][c] 是几,以这格收尾的正方形就有几个。每个正方形的右下角是唯一的,按右下角分堆不重不漏,所以全表求和就是总数。
题目不许改动输入 matrix 怎么办?+
参考代码为了省空间直接在 matrix 上原地改写,把它当 dp 表用。若不允许改输入,就另开一张同样大小的 dp 数组,转移和累加逻辑完全一样,空间从 O(1) 变成 O(m·n)。还能进一步省:转移只用到上一行和本行左边,保留两行(或滚动一行)就够,空间压到 O(n),但代码要更小心地维护左上角那个值。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 统计全为 1 的正方形子矩阵 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。