1. 题目到底在问什么
1.1 还原题面,先搞懂输入输出
这题编号是 3047,难度标的是中等,我是在周赛里第一次遇到的。题面看起来很长,核心内容其实一句话就能说清楚:给你一组左下角坐标和一组右上角坐标,它们按顺序一一对应成若干个轴对齐矩形,然后任意挑两个矩形出来,求它们交集区域里能放下的最大正方形的面积。如果没有交集,这个矩形对的贡献就是 0,最后把所有矩形对的答案取最大值返回。
坐标用 bottomLeft 和 topRight 两个二维数组给出,每个数组的元素都是一个长度为 2 的坐标点。比如 bottomLeft[i] = [x1, y1],topRight[i] = [x2, y2],就表示第 i 个矩形的左下角在 (x1, y1),右上角在 (x2, y2)。矩形边和坐标轴平行,这也是后面所有推导能简化的大前提。也就是说,这不是让你求两个不规则图形的交集,而是两个"横平竖直"矩形的交集。
有基础的朋友到这里应该已经能猜到解法的大方向了:两个轴对齐矩形的交集,一定还是一个轴对齐矩形(或者空矩形、退化成一条线),所以只要能把交集矩形求出来,剩下的问题就是在交集矩形里找一个最大的正方形,这个正方形的边长上限就是交集矩形短边的长度。
1.2 数据范围决定了你能用什么算法
做算法题第一件事永远是看数据范围。这个题两个数组的长度都是 n,我记得范围给到 1000 左右。坐标范围倒是比较大,0 <= xl < xr <= 10^9,对 y 坐标同理。这个坐标范围是后面所有 int 溢出问题的根源,必须提前警惕。
n 只有 1000,意味着最暴力的做法,两层循环枚举所有矩形对,也只需要跑大约 1000 * 999 / 2 = 499500 次,也就是 50 万次左右。这个数量级在现代 CPU 上就是一瞬间的事,完全不需要上什么线段树、扫描线、排序优化。所以这道题表面上是"中等",实际算法难度也就是入门级,真正容易失分的地方全在细节处理和数据类型上。
我一开始做这题的时候,还在想有没有必要先对矩形按 x 坐标排序,再用扫描线维护当前重叠区间。后来算了一下复杂度,发现纯暴力就过了,而且代码短得多,调试起来也省心。在竞赛里,能用简单解法过的题就不要炫技,这是我一直以来的原则。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心突破口:交集矩形怎么算
2.1 两矩形交集的四个坐标公式
轴对齐矩形用左下角和右上角两个点表示有一个天然的好处:判断两个矩形是否相交,以及求相交区域,可以直接对坐标取 max 和 min,不需要任何分类讨论。
假设矩形 A 的左下角是 (x1, y1),右上角是 (x2, y2);矩形 B 的左下角是 (x3, y3),右上角是 (x4, y4)。那么交集矩形的左下角坐标就是:
- intersectLeft = max(x1, x3)
- intersectBottom = max(y1, y3)
交集矩形的右上角坐标就是:
- intersectRight = min(x2, x4)
- intersectTop = min(y2, y4)
这个公式的直观理解是:交集在 x 方向的起点,必须是两个矩形都想覆盖的起点,所以取较大的那个左边界;终点必须是两个矩形都能延伸到的终点,所以取较小的那个右边界。y 方向同理。这个思路用生活中的例子类比就是:两个人约定时间碰头,开始时间取两人中较晚的,结束时间取两人中较早的,如果开始时间已经晚于结束时间,那这次碰头就约不成了。
2.2 交出来的东西到底长什么样
得到四个坐标之后,判断有没有交集只需要看两个不等式:
- intersectLeft < intersectRight
- intersectBottom < intersectTop
这两个不等式必须同时成立,才算有面积上的交集。如果某个方向上是 >=,说明两个矩形在这个方向上没有真正的重叠。比如 intersectLeft == intersectRight,那就是两个矩形恰好共享一条竖直边,交集退化成一条线段,面积是 0,不满足题意。同理,如果相等发生在 y 方向,交集就是一条水平线段,同样没有面积。
有人可能会问,为什么题目里要强调"最大正方形面积",而不是直接求交集面积?因为正方形这个条件让答案变成了取交集矩形的短边。交集矩形的宽是 intersectRight - intersectLeft,高是 intersectTop - intersectBottom,能放下的正方形的边长不能超过宽,也不能超过高,所以边长的最大值就是:
- side = min(交集矩形宽, 交集矩形高)
这个结论非常关键。只要把交集矩形算出来,最终答案就是 side * side。不需要再去考虑正方形在交集矩形里怎么摆放,因为它一定和坐标轴平行,而轴对齐正方形只要边长不超过矩形的宽和高,就一定能放进去。这一点必须想明白,否则容易在"正方形能不能斜着放"这种问题上卡住。
2.3 为什么这个题不需要复杂优化
这道题在最坏情况下需要枚举所有矩形对,也就是 O(n^2) 的复杂度。n = 1000 时只有 50 万次操作,即使每轮循环里有若干次比较和乘法,总耗时也远低于 1 秒。所以完全不需要额外的数据结构。
之前我见过有人试图用线段树或者离散化来做,最后代码写了几百行,反而因为边界条件太多出了 bug。对于这种数据范围温和的题,暴力枚举就是最合理的方案。复杂度低、代码短、不容易错,这在比赛中就是最大的优势。
不过,如果 n 放大到 10^5,那 O(n^2) 就肯定不行了,可能需要考虑按 x 坐标排序后维护滑动窗口,或者用更高级的几何扫描方法。但那是另一个问题了,本题远到不了那个程度。
3. 从思路到代码:两个版本完整实现
3.1 C++ 代码逐行拆解
先上我最终提交的 C++ 版本,这个版本代码量很小,但每一步都有讲究:
cpp复制class Solution {
public:
long long largestSquareArea(vector<vector<int>>& bottomLeft, vector<vector<int>>& topRight) {
int n = bottomLeft.size();
long long ans = 0;
for (int i = 0; i < n; ++i) {
long long x1 = bottomLeft[i][0], y1 = bottomLeft[i][1];
long long x2 = topRight[i][0], y2 = topRight[i][1];
for (int j = i + 1; j < n; ++j) {
long long x3 = bottomLeft[j][0], y3 = bottomLeft[j][1];
long long x4 = topRight[j][0], y4 = topRight[j][1];
long long xL = max(x1, x3);
long long yL = max(y1, y3);
long long xR = min(x2, x4);
long long yR = min(y2, y4);
if (xL < xR && yL < yR) {
long long side = min(xR - xL, yR - yL);
ans = max(ans, side * side);
}
}
}
return ans;
}
};
这里第一层循环取第 i 个矩形,第二层循环从 i + 1 开始取第 j 个矩形,这样既保证了每个矩形对只被计算一次,又避免了自己和自己求交集的荒谬情况。如果第二层循环从 0 开始,不仅会重复计算,还会在 i == j 时把矩形本身的面积当成交集面积,逻辑上就错了。
每次取坐标时,我先把 int 转成 long long,这是整个代码里最重要的一行隐性操作。bottomLeft 里的元素是 int 类型,坐标值本身最大 10^9,用 int 存没问题,但坐标相减之后的宽度和高度的平方就可能达到 10^18,这已经远远超出 int 的表示范围了。如果不在最开始转类型,运算过程中就会发生溢出,结果直接错误。
3.2 Python 代码实现
Python 不太需要担心整数溢出,但代码结构完全一致。我平时刷题比较常用 Python 验思路,正式比赛或写工程反而更倾向 C++,两套都放出来供参考:
python复制class Solution:
def largestSquareArea(self, bottomLeft: List[List[int]], topRight: List[List[int]]) -> int:
n = len(bottomLeft)
ans = 0
for i in range(n):
x1, y1 = bottomLeft[i]
x2, y2 = topRight[i]
for j in range(i + 1, n):
x3, y3 = bottomLeft[j]
x4, y4 = topRight[j]
x_left = max(x1, x3)
y_bottom = max(y1, y3)
x_right = min(x2, x4)
y_top = min(y2, y4)
if x_left < x_right and y_bottom < y_top:
side = min(x_right - x_left, y_top - y_bottom)
ans = max(ans, side * side)
return ans
Python 版本在语法上更清爽,x1, y1 = bottomLeft[i] 这个解包写法可以直接把坐标拆出来,可读性比 C++ 的连续下标访问好不少。两者的核心逻辑一模一样,没有任何语言层面的优化差异。
3.3 每一步都在解决什么问题
我分别解释一下代码中几个关键行为的意图。
long long x1 = bottomLeft[i][0] 这一步表面上看只是取数,实际上是在整个计算链路的起点就切断了溢出风险。因为后面所有的 max、min、减法、乘法,只要参与运算的变量里有任何一个还是 int,在 C++ 里的运算就会以 int 类型进行,结果可能溢出后才转成 long long,那就晚了。先转换、后运算,是最稳妥的写法。
if (xL < xR && yL < yR) 这一行排除了所有无效交集。注意这里用的是严格小于,而不是小于等于。如果是等于,说明交集在某个方向的宽度为 0,矩形退化成线段,面积就是 0,对答案没有贡献,直接跳过也不影响最终结果。但从逻辑严谨性出发,写严格小于更符合"有面积交集"的定义。
side = min(xR - xL, yR - yL) 这一行是整个题目的题眼。交集矩形的宽和高都算出来了,正方形的边长不可能超过任何一边,所以取较小值就是最大边长。ans = max(ans, side * side) 最终记录所有矩形对中的最大面积。
4. 几个容易踩的坑,逐个说清楚
4.1 int 溢出是最常见的 WA 来源
这个题如果 WA,十个里有八个都是栽在 int 溢出上。坐标范围最大 10^9,两个坐标相减得到的宽度最大也是 10^9,正方形面积就是 10^9 * 10^9 = 10^18。这个数已经超过 32 位 int 的上限约 21 亿,甚至接近 long long 上限 9.22 * 10^18 的十分之一,所以长期看用 long long(C++)/ int64(其他语言)存储面积是非常必要的。
我第一版代码里,因为偷懒直接用了 int 存 xL xR yL yR,最后返回的时候才发现编译器提示类型不匹配,改完类型后样例直接通过了。现在复盘一下就明白:如果当时样例里恰好有一组坐标很大的用例,这个错误就会变成一次惨痛的 WA。建议大家在写这类涉及坐标相乘的几何题时,从一开始就用 64 位整数,不要抱着"数据应该没那么大"的侥幸心理。
4.2 交集判断的边界条件别写反
判断交集的时候,最自然的写法是 xL < xR && yL < yR。但如果你反过来想,用"无交集"的条件去跳过,代码就成了:
cpp复制if (xL >= xR || yL >= yR) continue;
这两种写法本质等价,但第二种更容易让人犯糊涂,因为在连续判断 || 的时候,很容易把 >= 错写成 >,从而把"恰好共边"的情况也算成有交集。共边时面积为 0,虽然对最终答案没影响,但逻辑上不够严谨。如果哪天题目改成了求交集矩形的编号或者要求返回特殊值,这种边界错误就会立刻暴露。
我建议统一用"有交集"的正向判断,因为它的语义最直接:只有在两个方向上都严格有重叠,交集矩形才存在。这也是我在工程代码里更倾向的写法。
4.3 双层循环的起始位置
第二层循环写成 for (int j = i + 1; j < n; ++j) 可以避免重复枚举,也可以防止自己与自己配对。如果你用 for (int j = 0; j < n; ++j) 然后跳过 i == j,也能得到正确答案,但时间复杂度会翻倍,而且要多写一个判断。对于 n = 1000 来说都无所谓,但养成写 i + 1 的习惯,在更大的数据范围下能省一半时间,这个习惯值得提前培养。
还需要注意一点,题目是"任意两个矩形",也就是说取的两个矩形必须是不同的索引。如果两个矩形实际上是同一个坐标(两行数据完全相同),但它们来自不同的索引,仍然算两个矩形,它们之间的交集就是矩形本身,此时答案是允许取到的。这个语义在代码里天然就支持,因为只要 i != j,即使坐标完全相同也会正常计算。有朋友曾经把同坐标的矩形当成同一个,误以为要跳过,结果答案少了正确值,这种过度理解题意的情况也要避免。
4.4 返回类型别搞成 int
LeetCode 的函数签名里,返回类型是 long long。就算你在计算过程中全程用 long long,如果最后返回值写成了 int,编译器可能会截断高 32 位,导致答案变成负数或者奇怪的数字。所以在写函数签名的时候就要确认返回类型,不能到了最后一步才想起来。
写 Python 就没有这个烦恼,因为 Python 的整数是任意精度的,这也是为什么很多新手用 Python 刷题时不会遇到溢出问题,但一旦切到 C++ 或者 Java,就会在类型上翻车。我还是建议两边都练,因为工作中 C++ 虽然用得多,但面试时候 Python 快速验证思路的效率确实高。
5. 案例推导与过程验算
5.1 手算一组完整例子
我构造一组简单的数据来走一遍完整流程。假设有两个矩形:
- 矩形 A:左下角
(0, 0),右上角(4, 3) - 矩形 B:左下角
(2, 1),右上角(6, 5)
它们的交集坐标:
- xL = max(0, 2) = 2
- yL = max(0, 1) = 1
- xR = min(4, 6) = 4
- yR = min(3, 5) = 3
交集矩形的宽是 4 - 2 = 2,高是 3 - 1 = 2,这是一个 2x2 的正方形区域,所以最大正方形边长就是 2,面积是 4。
这个例子正好说明了"交集区域内的最大正方形"就是交集本身退化成正方形时的特殊情况。如果矩形 A 是 (0,0) 到 (5,4),矩形 B 是 (2,1) 到 (6,5),交集就是 (2,1) 到 (5,4),宽 5-2=3,高 4-1=3,答案仍然是 9。但把 B 改成 (2,1) 到 (6,6),交集变成 (2,1) 到 (5,4),宽 3 高 3,答案还是 9。只有当交集区域是长条形时,最大正方形才会严格小于交集面积。
5.2 长条形交集的取边逻辑
比如矩形 A 是 (0,0) 到 (10,2),矩形 B 是 (1,1) 到 (9,3),交集是 (1,1) 到 (9,2),宽 8 高 1。这时如果误以为答案是交集面积 8 * 1 = 8,就完全错了,因为题目要的是正方形,而不是任意矩形。正方形边长上限是 min(8, 1) = 1,面积是 1。
这就是为什么算法第一步必须先求交集矩形,然后再对宽和高取 min 得到边长。很多人一开始思路跑偏,去枚举所有可能的正方形边长,比如从大到小检查某个边长能否被交集区域包含,那样不仅代码复杂,还可能因为二分或扫描边界问题写错。事实上只要理解了"交集是矩形",答案就是一行 min(width, height) 的事。
5.3 用随机数据验证代码正确性
我习惯在写完这种几何题之后,自己写一个小脚本随机生成数据,再写一个暴力验证函数(直接枚举所有格子或所有可能的正方形位置)来交叉验证。
对这道题来说,暴力验证函数可以这样写:随机生成一组矩形后,枚举所有矩形对,算出交集矩形,再枚举所有可能的正方形起始位置和边长,检查是否完全落在交集矩形内。这样虽然慢,但逻辑足够低级,不容易出错。用它对拍几百组随机数据,如果和正式解法的结果完全一致,基本可以放心提交。
这个方法是我屡试不爽的调试技巧。很多边界情况我们靠眼睛看不出来,但随机数据可以让隐藏的 bug 自动现形。特别是像 xL < xR 还是 xL <= xR 这种细微差别,只要出现一组共边的数据,对比结果立刻分晓。
6. 从这题延伸出去的几个思考
6.1 如果求所有矩形的公共交集
先别急着关掉这道题。这个题虽然简单,但它延伸出去的问题很有意思。如果把"任意两个矩形"改成"所有矩形的公共交集",也就是求所有矩形重叠的部分里能放下的最大正方形,那解法其实更简单:只需要把所有矩形的左边界取 max,下边界取 max,右边界取 min,上边界取 min,得到一个全局交集矩形,然后同样取短边作为正方形边长。
这个变体的代码量比原题还少,因为不需要两层循环。但如果你没理解"交集区域是矩形"这个本质,遇到这种变体可能又会去想扫描线之类复杂的方案。所以原题的价值不只是在教会你一个 max/min 公式,更在于让你意识到轴对齐矩形求交的普遍规律。
6.2 如果矩形不是轴对齐的
再扩展一步。如果题目允许矩形任意旋转,那么任意两个矩形的交集就不一定是矩形了,可能是三角形、五边形、甚至更复杂的多边形。这时求最大内接正方形的难度会上升好几个量级,可能会涉及半平面交、旋转卡壳等计算几何算法,复杂度也远超普通中等题。
正因为这个原因,我始终觉得 LeetCode 上的几何题大多是"纸老虎",题面看着吓人,实际上只要抓住"轴对齐"这个核心约束,所有问题都能转化成简单的坐标比较和乘法运算。做题时先仔细审题,找到约束条件,比盲目套模板重要得多。
我在实际做这道题时还发现一个小技巧,就是先把所有坐标输入统一转换成 64 位整数,再开始写业务逻辑。这不是针对某一题的做法,而是一个通用习惯。现在很多几何题的坐标范围都很大,可能达到 10^9,两数相乘必然溢出 32 位。与其每次定位到具体的乘法再改类型,不如一开始就把坐标变量声明成 64 位,省掉后面一半的调试时间。这个习惯帮我在多次周赛中避开了"代码思路完全正确但就是 WA"的尴尬局面。
