LeetCode 391. 完美矩形
题目描述



题意分析
每个小矩形的边都平行于坐标轴,要求它们恰好拼成一个完整的大矩形:内部不能有空隙,也不能有重叠,共用边或角点是允许的。
若能完美覆盖,目标大矩形一定由所有小矩形的最小左、下边界和最大右、上边界确定。只比较面积还不够,因为重叠增加的面积可能与空隙减少的面积相抵消。
解法:面积校验 + 角点抵消
核心思路
[!blue]
同时检查面积和角点。遍历所有矩形,记录外框
minX、minY、maxX、maxY,并累加每个小矩形的面积。小矩形面积之和必须等于外框面积,重叠部分在这个和中会被重复计算。对每个矩形的四个角点做一次集合切换:不存在就加入,已经存在就删除。处理结束后,集合中恰好保留出现奇数次的坐标。完美拼接时,四个外角各出现一次,其他拼接位置的角点出现次数都是偶数,因此最终必须只剩外框的四个具体角点。
这两个条件的充分性可以通过划分小格理解。用所有横纵边界把外框划成小格,每格内部的覆盖次数固定。取格内一点,统计横纵坐标都不超过该点的角点:包含该点的矩形只有左下角满足条件,不包含该点的矩形则贡献
0、2或4个。因此这些角点总数的奇偶,恰好等于该格覆盖次数的奇偶。当剩余奇数角点只有外框四角时,内部每个小格的覆盖次数都和完整外框一样为奇数,也就至少被覆盖一次,排除了空隙。此时总面积又恰好等于外框面积,任何小格都不能被多覆盖,否则面积和必然超出。所以所有小格只能恰好覆盖一次,重叠也被排除。划分小格只用于说明正确性,代码无需建立网格。
面积使用宽整数,先转换坐标再相减、相乘。角点键需要完整区分横纵坐标:Java 用带分隔符的字符串,Go 直接用长度为
2的数组作为键。
解题步骤
- 初始化外框边界、面积和及空的角点集合。
- 遍历每个矩形,更新四条外框边界,累加宽乘高,并对四个角点分别切换集合成员。
- 计算外框面积。若面积和不同,或集合中不是恰好
4个点,返回false。- 检查剩下的四个点是否分别为外框的左下、左上、右下、右上角;全部满足才返回
true。
代码实现
class Solution {
// 所有小矩形的面积和必须等于外包大矩形面积,面积使用宽整数,并在相减之前转换坐标。
public boolean isRectangleCover(int[][] rectangles) {
if (rectangles.length == 0) {
return false;
}
long area = 0;
int minX = rectangles[0][0];
int minY = rectangles[0][1];
int maxX = rectangles[0][2];
int maxY = rectangles[0][3];
Set<String> corners = new HashSet<>();
for (int[] rectangle : rectangles) {
int x1 = rectangle[0];
int y1 = rectangle[1];
int x2 = rectangle[2];
int y2 = rectangle[3];
minX = Math.min(minX, x1);
minY = Math.min(minY, y1);
maxX = Math.max(maxX, x2);
maxY = Math.max(maxY, y2);
// 先扩大坐标类型再相减,避免差值在窄整数中先溢出
area += ((long) x2 - x1) * ((long) y2 - y1);
toggle(corners, x1, y1);
toggle(corners, x1, y2);
toggle(corners, x2, y1);
toggle(corners, x2, y2);
}
long coverArea = ((long) maxX - minX) * ((long) maxY - minY);
if (area != coverArea || corners.size() != 4) {
return false;
}
return corners.contains(key(minX, minY))
&& corners.contains(key(minX, maxY))
&& corners.contains(key(maxX, minY))
&& corners.contains(key(maxX, maxY));
}
private void toggle(Set<String> corners, int x, int y) {
String k = key(x, y);
// 出现一次切换一次,最终仅保留奇数次角点
if (corners.contains(k)) {
corners.remove(k);
return;
}
corners.add(k);
}
private String key(int x, int y) {
return x + "," + y;
}
}
func isRectangleCover(rectangles [][]int) bool {
// 所有小矩形的面积和必须等于外包大矩形面积,面积使用宽整数,并在相减之前转换坐标。
if len(rectangles) == 0 {
return false
}
minX, minY := rectangles[0][0], rectangles[0][1]
maxX, maxY := rectangles[0][2], rectangles[0][3]
area := int64(0)
corners := make(map[[2]int]bool)
toggle := func(x, y int) {
point := [2]int{
x,
y,
}
// 出现一次切换一次,最终仅保留奇数次角点
if corners[point] {
delete(corners, point)
return
}
corners[point] = true
}
for _, r := range rectangles {
x1, y1 := r[0], r[1]
x2, y2 := r[2], r[3]
if x1 < minX {
minX = x1
}
if y1 < minY {
minY = y1
}
if x2 > maxX {
maxX = x2
}
if y2 > maxY {
maxY = y2
}
// 先扩大坐标类型再相减,避免差值在窄整数中先溢出
area += (int64(x2) - int64(x1)) * (int64(y2) - int64(y1))
toggle(x1, y1)
toggle(x1, y2)
toggle(x2, y1)
toggle(x2, y2)
}
coverArea := (int64(maxX) - int64(minX)) * (int64(maxY) - int64(minY))
if area != coverArea || len(corners) != 4 {
return false
}
if !corners[[2]int{
minX,
minY,
}] || !corners[[2]int{
minX,
maxY,
}] {
return false
}
if !corners[[2]int{
maxX,
minY,
}] || !corners[[2]int{
maxX,
maxY,
}] {
return false
}
return true
}
复杂度分析
- 时间复杂度:期望 $O(n)$,每矩形常数次哈希操作。
- 空间复杂度:$O(n)$,最坏保存线性数量角点。
关键点总结
[!green]
- 面积统计每一层覆盖,角点集合只统计覆盖结构的奇偶,两种信息共同排除空隙和重叠。
- 切换集合等价于记录出现次数的奇偶,不需要保存完整计数。
- 验证的是外框四个具体坐标,不能只判断剩余点数为
4。
易错点总结
[!yellow]
- 只比较面积无法排除空隙与重叠相抵;只检查角点奇偶也无法排除奇数层重复覆盖。
- 坐标范围允许面积超过
int,应先把坐标转成long或int64再相减、相乘,不能等窄整数运算完成后才转换。- 字符串键中的横纵坐标必须有分隔符,否则不同坐标可能拼成相同文本。
- 每个矩形的四角都必须参与切换,不能把共享角点只处理一次。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 223. 矩形面积 | 中等 | 面积计算是基础,但总面积相等还不足以独自排除重叠与空洞,本题需额外验证边界结构。 |
| 850. 矩形面积 II | 困难 | 同样处理多个轴对齐矩形,原题计算并集面积,本题判断它们是否无重叠无空隙地覆盖一个矩形。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!