目录

题目描述

750. 角矩形的数量

题意分析

给一个只含 $0$ 和 $1$ 的矩阵 grid,要求统计其中「角矩形」的个数。角矩形的定义是:取四个格子,它们的值都是 $1$,并且这四个格子恰好构成一个轴对齐矩形的四个角——也就是说它们分布在两条不同的行两条不同的列的交点上。矩形内部和边上的其他格子是什么值完全不管。

定义本身给出了最强的信号:一个角矩形被「两行 + 两列」这四条线唯一确定,而不是被四个坐标独立确定。这意味着问题的自由度只有两维(选哪两行、选哪两列),而不是四维,直接把朴素的四重枚举压到二重枚举加计数。

第二个信号是值域只有 $0$ 和 $1$。这让「某一列在某两行上是否都为 $1$」变成一个纯布尔判断,可以用简单的相乘或与运算表达,也为后续用位运算加速留了空间。

约束方面:行数 m 和列数 n 都在 $1$ 到 $200$ 之间,矩阵里 $1$ 的总数不超过 $6000$,答案保证在 $32$ 位整数范围内。$m^2 n$ 大约是 $200^2 \times 200 = 8 \times 10^6$,完全可以接受;而四重枚举是 $m^2 n^2$ 约 $1.6 \times 10^9$,会超时。这组数字正是在暗示「按行对枚举」这条路。

边界上要覆盖:只有一行或只有一列(凑不出两条不同的行/列,答案必为 $0$);某两行的公共 $1$ 列数为 $0$ 或 $1$(此时组合数为 $0$,不能贡献负数);整张矩阵全是 $1$(答案是行对数乘列对数,用来验算公式);矩阵中 $1$ 极其稀疏。

解法:枚举行对 + 组合计数

核心思路

最直白的暴力是四重循环:枚举上边行 r1、下边行 r2、左列 c1、右列 c2,检查四个交点是否都是 $1$。复杂度 $O(m^2 n^2)$,在 $200 \times 200$ 的规模下约 $1.6 \times 10^9$ 次判断,必然超时。

瓶颈在于:内层的两重列枚举做了大量重复劳动。固定 r1r2 之后,判断「列 c 上这两行是否都为 $1$」这件事对每一列是独立的,而四重循环把每一列的这个判断重复做了 $O(n)$ 遍(因为它对每个 (c1, c2) 组合都要重新检查两端)。

关键观察:固定两行之后,问题退化成一个纯计数问题。设这两行中「同一列上都是 $1$」的列有 common 条,那么任取其中两条列,都必然能和这两行围出一个合法的角矩形;反之任何以这两行为上下边的角矩形,它的左右两列也必然都属于这 common 条。所以这对行的贡献恰好是从 common 条列里选两条的组合数 $\binom{common}{2} = \frac{common \times (common - 1)}{2}$,一次算式代替了内层的 $O(n^2)$ 枚举。

于是维护的量非常简单:对每一对行 $(r_1, r_2)$(要求 $r_1 < r_2$),common 表示满足 grid[r1][c] == 1 && grid[r2][c] == 1 的列数;答案是所有行对的 $\binom{common}{2}$ 之和。这里不需要 dp 数组,common 只是一个在每对行内部从零开始重新累加的局部计数器。

为什么每个角矩形不会被重数或漏数?因为枚举时强制 $r_1 < r_2$,所以「上下两行」的选法与无序行对一一对应;而组合数 $\binom{common}{2}$ 本身就是无序地选两列。两者相乘,每个角矩形恰好在它自己那对行、那对列上被数一次,既不重也不漏。

至于要不要枚举列对而不是行对——两者对称,选行数较小的那一维做外层平方可以省常数,但在 $m, n \le 200$ 的规模下没有必要,保持代码简单更重要。

解题步骤

  • 取出 m = grid.lengthn = grid[0].length,答案累加器 answer 初始化为 $0$。理由:n 只需读一次,放在循环外避免重复取长度;题目保证矩阵非空,所以 grid[0] 可以安全访问。

  • 外层 for (int r1 = 0; r1 < m; r1++),内层 for (int r2 = r1 + 1; r2 < m; r2++)。理由:r2r1 + 1 开始,一次性保证了两行不同且不会把 $(a, b)$ 和 $(b, a)$ 数两遍;如果写成 r2 = 0 再判 r1 != r2,答案会正好翻倍。

  • 在每对行的开头把 common 重置为 $0$。理由:common 的语义是「当前这对行的公共 $1$ 列数」,它必须随行对更换而重新统计;忘记重置会让计数一路累积,答案偏大到离谱。

  • 最内层遍历所有列 c,当 grid[r1][c] == 1 && grid[r2][c] == 1common++。理由:两个条件必须同时成立才说明这一列能同时给上下两行提供角点;用 && 短路还能在上行为 $0$ 时省掉一次访问。

  • 列循环结束后执行 answer += common * (common - 1) / 2。理由:这一步把 $O(n^2)$ 的列对枚举替换成常数时间的组合公式;common 为 $0$ 或 $1$ 时该式自然得 $0$,不需要额外判断。

  • 先乘后除的顺序不能颠倒:必须写 common * (common - 1) / 2 而不是 common / 2 * (common - 1)。理由:commoncommon - 1 中必有一个是偶数,所以乘积一定能被 $2$ 整除;但先除会在 common 为奇数时因整数截断丢掉 $0.5$,结果偏小。

  • 全部行对遍历完后返回 answer。理由:每个角矩形只在其唯一的行对上被计入一次,累加完即为总数。

  • grid = [[1,0,0,1,0],[0,0,1,0,1],[0,0,0,1,0],[1,0,1,0,1]] 走一遍($m = 4$,$n = 5$)。行对 $(0,1)$:逐列比较,列 $0$ 是 1/0、列 $1$ 是 0/0、列 $2$ 是 0/1、列 $3$ 是 1/0、列 $4$ 是 0/1,没有一列两行同时为 $1$,common = 0,贡献 $0$。行对 $(0,2)$:列 $3$ 上是 1/1,其余不匹配,common = 1,贡献 $\frac{1 \times 0}{2} = 0$——只有一条公共列时凑不出矩形,公式自动给 $0$。行对 $(0,3)$:列 $0$ 上是 1/1,列 $3$ 上是 1/0common = 1,贡献 $0$。行对 $(1,2)$:列 $2$ 是 1/0,列 $4$ 是 1/0,列 $3$ 是 0/1common = 0,贡献 $0$。行对 $(1,3)$:列 $2$ 上是 1/1,列 $4$ 上是 1/1common = 2,贡献 $\frac{2 \times 1}{2} = 1$——这就是由第 $1$、$3$ 行与第 $2$、$4$ 列围成的那个角矩形。行对 $(2,3)$:列 $3$ 是 1/0common = 0,贡献 $0$。总计 answer = 1,与期望一致。

代码实现

class Solution {
    public int countCornerRectangles(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int answer = 0;

        for (int r1 = 0; r1 < m; r1++) {
            for (int r2 = r1 + 1; r2 < m; r2++) {
                int common = 0;
                for (int c = 0; c < n; c++) {
                    if (grid[r1][c] == 1 && grid[r2][c] == 1) {
                        common++;
                    }
                }
                answer += common * (common - 1) / 2;
            }
        }

        return answer;
    }
}
func countCornerRectangles(grid [][]int) int {
    m := len(grid)
    n := len(grid[0])
    answer := 0

    for r1 := 0; r1 < m; r1++ {
        for r2 := r1 + 1; r2 < m; r2++ {
            common := 0

            for c := 0; c < n; c++ {
                if grid[r1][c] == 1 && grid[r2][c] == 1 {
                    common++
                }
            }

            answer += common * (common - 1) / 2
        }
    }

    return answer
}

复杂度分析

  • 时间复杂度:$O(m^2 n)$,其中 $m$、$n$ 是矩阵的行数与列数。外两层枚举行对共 $\frac{m(m-1)}{2}$ 次,每次扫一遍所有列做 $O(n)$ 的比较,组合数计算是常数时间。相比四重枚举的 $O(m^2 n^2)$,把内层列对枚举换成一次公式,正好省掉一个 $n$。
  • 空间复杂度:$O(1)$。除了 commonanswer 两个整型变量外没有任何辅助结构,输入矩阵是只读的、不做拷贝也不做预处理表。

关键点总结

  • 几何计数题的第一步是找出决定一个图形的最小自由度。角矩形看似要定四个点,实际只要定两行两列,识别出这一点就把 $O(m^2n^2)$ 直接降到 $O(m^2n)$。同样的思路适用于「数平行四边形」「数轴对齐正方形」等题。
  • 「枚举一部分维度 + 对剩余维度用组合公式」是降维计数的通用模板:固定住让问题变简单的那几维,剩下的如果彼此互不约束,就能用 $\binom{k}{2}$ 之类的闭式代替枚举。判断依据是「任取两个都合法」,本题的 common 条列正好满足。
  • 枚举无序对时用 j = i + 1 起手,而不是全枚举再去重。这既避免了重复计数,也省掉一次相等判断,是竞赛与面试里都通用的写法。
  • 整数运算中 $\binom{k}{2}$ 要写成 k * (k - 1) / 2,先乘后除。理由是相邻两数必有一偶,乘积必然整除 $2$;先除会截断出错。
  • 面试视角:拼多多、网易考这题的目的是看能不能主动做复杂度降维。标准答题节奏是先说出四重枚举的 $O(m^2n^2)$ 并指出会超时,再说「固定两行后左右两列的选择互不干扰,可以用组合数一次算完」,最后写十行代码。如果被追问还能不能更快,可以答:把每一行压成一个 $200$ 位的位图(long[4]),行对的公共列数就是两个位图按位与之后的 popcount,常数能降到约 $\frac{1}{64}$;或者反过来枚举列对并借助哈希表统计,在 $1$ 很稀疏时更优——本题限制 $1$ 的总数不超过 $6000$,正是给这条稀疏优化留的口子。

易错点总结

  • 错误写法:内层循环写成 for (int r2 = 0; r2 < m; r2++) 再用 if (r1 == r2) continue; 过滤 → 用例 [[1,1],[1,1]] → 行对 $(0,1)$ 与 $(1,0)$ 各算一次,答案变成 $2$,正确答案是 $1$。
  • 错误写法:把 common 声明在两层行循环之外,只初始化一次 → 用例 [[1,1],[1,1],[1,1]] → 第二对、第三对行的计数在前面的基础上继续累加,common 一路涨到 $6$,答案变成 $1 + 3 + 15 = 19$,正确答案是 $3$。
  • 错误写法:组合数写成 common / 2 * (common - 1) 先除后乘 → 用例 某对行的 common = 3 → 正确贡献是 $3$,但先除得 $1$ 再乘 $2$ 得 $2$,整数截断导致少算。
  • 错误写法:组合数写成 common * common / 2common * (common + 1) / 2 → 用例 common = 2 → 前者得 $2$、后者得 $3$,正确答案是 $1$;选两列是 $\binom{k}{2}$ 不是 $\binom{k+1}{2}$,也不含自己配自己。
  • 错误写法:公共列判断写成 grid[r1][c] == grid[r2][c] → 用例 [[0,0],[0,0]] → 两行在两列上都相等(都是 $0$),common = 2,贡献 $1$,但一个 $1$ 都没有,正确答案是 $0$;必须要求两者都等于 1
  • 错误写法:公共列判断写成 grid[r1][c] + grid[r2][c] >= 1(误用「或」的语义)→ 用例 [[1,0],[0,1]] → 两列都满足,common = 2,答案算成 $1$,正确答案是 $0$。
  • 错误写法:没有对 common < 2 的情况做保护却把公式写成了减法形式如 common * common - common 之后再除 → 用例 common = 0 → 结果虽仍是 $0$,但若误写成 (common - 1) * (common - 2) / 2 之类的偏移公式,common = 0 时会得到 $1$,凭空多出一个不存在的矩形。
  • 错误写法:把 n 写成 grid.length 而不是 grid[0].length → 用例 非方阵如 $2$ 行 $5$ 列的矩阵 → 列循环只跑 $2$ 次,右侧三列的 $1$ 全被忽略,答案偏小;若矩阵是 $5$ 行 $2$ 列则直接数组越界。
  • 错误写法:直接照搬四重枚举 for r1, for r2, for c1, for c2 判断四点 → 用例 $200 \times 200$ 且 $1$ 较密集的矩阵 → 约 $1.6 \times 10^9$ 次判断,远超时限。
  • 错误写法:以为「角矩形要求内部或边上其他格子也是 1」而在统计时额外校验 → 用例 [[1,0,1],[0,0,0],[1,0,1]] → 四个角都是 $1$ 但中间是 $0$,被错误排除,答案为 $0$,正确答案是 $1$。

相似题目

题目 难度 考察点
1074. 元素和为目标值的子矩阵数量 困难 同样枚举一对边界再把剩余维度压成一维,但用前缀和加哈希表而非组合公式
1277. 统计全为 1 的正方形子矩阵 中等 要求整块全为 $1$ 而非只看四角,靠动态规划递推边长而不是枚举行对
1139. 最大的以 1 为边界的正方形 中等 只要求四条边全为 $1$,需预处理行列方向的连续长度前缀
221. 最大正方形 中等 求最大面积而非计数,状态定义为「以该点为右下角的最大边长」
85. 最大矩形 困难 按行压缩成柱状图后用单调栈求最大面积,是矩阵问题降维的另一条主线