题目来源
850. 矩形面积 II - 力扣(LeetCode)
题目描述
给你一个轴对齐的二维数组 rectangles 。
对于 rectangle[i] = [x1, y1, x2, y2],其中(x1,y1)是矩形 i 左下角的坐标, (xi1, yi1) 是该矩形 左下角 的坐标, (xi2, yi2) 是该矩形 右上角 的坐标。
计算平面中所有 rectangles 所覆盖的 总面积 。任何被两个或多个矩形覆盖的区域应只计算 一次 。
返回 总面积 。因为答案可能太大,返回 10^9 + 7 的 模 。
示例
示例 1:
输入:rectangles = [[0,0,2,2],[1,0,2,3],[1,0,3,1]] 输出:6 解释:如图所示,三个矩形覆盖了总面积为 6 的区域。 从(1,1)到(2,2),绿色矩形和红色矩形重叠。 从(1,0)到(2,3),三个矩形都重叠。
示例 2:
输入:rectangles = [[0,0,1000000000,1000000000]] 输出:49 解释:答案是 1018 对 (109 + 7) 取模的结果, 即 49 。
提示
- 1 <= rectangles.length <= 200
- rectanges[i].length = 4
- 0 <= xi1, yi1, xi2, yi2 <= 10^9
题目解析
本题如果从 ”面“ 的角度去思考,比如:所有矩形的面积 - 矩形交集部分的面积 = 最终面积。
两个矩形的交集很容易求解,比如下面图示
虽然矩形交集很容易求解,但是想要求出所有交集,则需要让每个矩形和剩余其他矩形尝试比较,得出交集。同时求出交集矩形后,这些交集矩形也是可能互相重叠的 。。。交集的交集矩形也是可能互相重叠的。。。这样是无穷无尽的求解。因此这个思路不可取。
本题如果从 ”线“ 的角度去思考,如下图所示,从假设有一条扫描线 x = c(x1 ≤ c ≤ x4),从左向右扫描,每扫描到一个位置,则判断该位置是否有矩形覆盖,如果有矩形覆盖,比如:
- 图1 ~ 图3 中扫描线只覆盖到了矩形[左下角(x1,y1),右上角(x2,y2)],因此矩形覆盖的高度为 ( y2 - y1),对应扫描线扫描出的矩形面积 = (x3 - x1) * ( y2 - y1)
- 图4 ~ 图5 中扫描线覆盖了两个矩形,分别是 [左下角(x1,y1),右上角(x2,y2)] 和 [左下角(x3,y3),右上角(x4,y4)],因此矩形覆盖的高度区间也有两个: [y1, y2] 和 [y3, y4],而这两个区间又是具有重叠部分的,因此我们可以转化为区间问题,利用区间问题解法,求解出所有区间的不重叠长度之和 height 。具体求解过程在下面。那么扫描线扫描出来的面积为 (x2 - x3) * h。
- 首先,排序区间,按照起始位置升序,如果起始位置相同,则按照结束位置降序
- 然后,遍历区间,假设当前区间是 [start, end],上一个区间是 [last_start, last_end],
若 last_end >= end,那么说明当前区间被上一个区间完全覆盖,可以继续忽略当前区间(因为当前区间的长度已经在上一个区间被统计过了)
若 last_end < end,那么当前区间的非重叠部分为 [max(start, last_end), end],统计该部分长度:height += end - max(start, last_end),并更新last_end = end - 最后,我们就求出了区间组所有区间覆盖的不重叠长度了。
上面这种思路就是 ”扫描线算法“,扫描线法可以将 "面" 的问题,分解为 "线" 的问题,将 "矩形(面)交集问题" 降解为 "区间(线)交集问题"。
C源码实现
#define MAX_N 200
#define MOD (1e9 + 7)
int cmp(const void* a, const void* b) { return *(int*)a - *(int*)b; }
int cmp2(const void* a, const void* b) {
int* A = (int*)a;
int* B = (int*)b;
return A[0] != B[0] ? A[0] - B[0] : B[1] - A[1];
}
int rectangleArea(int** rectangles, int rectanglesSize, int* rectanglesColSize) {
// 统计所有矩形的左边边、右边边所在位置的x坐标
int listX[MAX_N];
int listX_size = 0;
for (int i = 0; i < rectanglesSize; i++) {
listX[listX_size++] = rectangles[i][0]; // 矩形左边边x坐标位置
listX[listX_size++] = rectangles[i][2]; // 矩形右边边x坐标位置
}
// 所有x坐标升序(每个x视为一条扫描线)
qsort(listX, listX_size, sizeof(int), cmp);
// 记录所有矩形并集面积
long ans = 0;
for (int i = 1; i < listX_size; i++) {
// 前一个扫描线x坐标
int preX = listX[i - 1];
// 当前扫描线x坐标
int curX = listX[i];
// 相邻两个扫描线的距离
long width = curX - preX;
// 距离为0, 则跳过
if (width == 0)
continue;
// 将在[x1,x2]区间上的矩形片段(垂直方向高度区间)收集起来
int lines[MAX_N][2];
int lines_size = 0;
// 遍历每个矩形
for (int j = 0; j < rectanglesSize; j++) {
// 矩形左上角坐标(x1,y1), 矩形右下角坐标(x2,y2)
int x1 = rectangles[j][0], y1 = rectangles[j][1],
x2 = rectangles[j][2], y2 = rectangles[j][3];
// 如果矩形包含了 [x1, x2] 区间
if (x1 <= preX && curX <= x2) {
// 那么该矩形在 水平方向区间[x1, x2] 对应的 垂直方向区间为 [y2, y1]
lines[lines_size][0] = y1;
lines[lines_size][1] = y2;
lines_size++;
}
}
// 将处于水方向区间 [x1, x2] 的所有垂直方向区间排序:按照起始位置升序, 如果起始位置相同, 则按照结束位置降序,这样排序的目的是保证排序后,前面的区间尽可能可以覆盖后面的区间
qsort(lines, lines_size, sizeof(lines[0]), cmp2);
// 记录lines多个区间,求长度之和,(重叠部分只计算一次)
long height = 0;
int last_end = -1;
for (int j = 0; j < lines_size; j++) {
int start = lines[j][0];
int end = lines[j][1];
// 如果 last_end >= end, 则当前区间被上一个区间完全覆盖,因此可以跳过
// 如果 last_end < end
if (last_end < end) {
// 则当前区间的不重叠部分是 [max(start, last_end), end]
height += end - (int)fmax(start, last_end);
// 更新last_end
last_end = end;
}
}
// 当前扫描线扫描到的面积为 width * height
ans += width * height;
ans %= (int)MOD;
}
return (int)ans;
}
C++源码实现
#define MOD (1E9 + 7)
class Solution {
public:
int rectangleArea(vector<vector<int>>& rectangles) {
// 统计所有矩形的左边边、右边边所在位置的x坐标
vector<int> listX;
for (vector<int>& rect : rectangles) {
listX.emplace_back(rect[0]); // 矩形左边边x坐标位置
listX.emplace_back(rect[2]); // 矩形右边边x坐标位置
}
// 所有x坐标升序(每个x视为一条扫描线)
sort(listX.begin(), listX.end());
// 记录所有矩形并集面积
long ans = 0;
for (int i = 1; i < listX.size(); i++) {
// 前一个扫描线x坐标
int preX = listX[i - 1];
// 当前扫描线x坐标
int curX = listX[i];
// 相邻两个扫描线的距离
long width = curX - preX;
// 距离为0, 则跳过
if (width == 0)
continue;
// 将在[x1,x2]区间上的矩形片段(垂直方向高度区间)收集起来
vector<vector<int>> lines;
// 遍历每个矩形
for (vector<int>& rect : rectangles) {
// 矩形左下角坐标(x1,y1), 矩形右上角坐标(x2,y2)
int x1 = rect[0], y1 = rect[1], x2 = rect[2], y2 = rect[3];
// 如果矩形包含了 [x1, x2] 区间
if (x1 <= preX && curX <= x2) {
// 那么该矩形在 水平方向区间[x1, x2] 对应的 垂直方向区间为 [y1, y2]
lines.emplace_back(vector<int>{y1, y2});
}
}
// 将处于水方向区间 [x1, x2]
// 的所有垂直方向区间排序:按照起始位置升序, 如果起始位置相同,
// 则按照结束位置降序,这样排序的目的是保证排序后,前面的区间尽可能可以覆盖后面的区间
sort(lines.begin(), lines.end(),
[](vector<int>& lineA, vector<int>& lineB) {
if (lineA[0] != lineB[0]) {
return lineA[0] < lineB[0];
} else {
return lineA[1] > lineB[1];
}
});
// 记录lines多个区间,求长度之和,(重叠部分只计算一次)
long height = 0;
int last_end = INT_MIN;
for (vector<int>& line : lines) {
int start = line[0];
int end = line[1];
// 如果 last_end >= end,
// 则当前区间被上一个区间完全覆盖,因此可以跳过 如果 last_end <
// end
if (last_end < end) {
// 则当前区间的不重叠部分是 [max(start, last_end), end]
height += end - max(start, last_end);
// 更新last_end
last_end = end;
}
}
// 当前扫描线扫描到的面积为 width * height
ans += width * height;
ans %= (int) MOD;
}
return (int) ans;
}
};
Java源码实现
class Solution {
public int rectangleArea(int[][] rectangles) {
// 统计所有矩形的左边边、右边边所在位置的x坐标
ArrayList<Integer> listX = new ArrayList<>();
for (int[] rect : rectangles) {
listX.add(rect[0]);
listX.add(rect[2]);
}
// 所有x坐标升序(每个x视为一条扫描线)
listX.sort((a, b) -> a - b);
// 记录所有矩形并集面积
long ans = 0;
for (int i = 1; i < listX.size(); i++) {
// 前一个扫描线x坐标
int preX = listX.get(i - 1);
// 当前扫描线x坐标
int curX = listX.get(i);
// 相邻两个扫描线的距离
int width = curX - preX;
// 距离为0, 则跳过
if (width == 0)
continue;
// 将在[x1,x2]区间上的矩形片段(垂直方向高度区间)收集起来
ArrayList<int[]> lines = new ArrayList<>();
// 遍历每个矩形
for (int[] rect : rectangles) {
// 矩形左下角坐标(x1,y1), 矩形右上角坐标(x2,y2)
int x1 = rect[0], y1 = rect[1], x2 = rect[2], y2 = rect[3];
// 如果矩形包含了 [x1, x2] 区间
if (x1 <= preX && curX <= x2) {
// 那么该矩形在 水平方向区间[x1, x2] 对应的 垂直方向区间为 [y1, y2]
lines.add(new int[] { y1, y2 });
}
}
// 将处于水方向区间 [x1, x2] 的所有垂直方向区间排序:按照起始位置升序, 如果起始位置相同,则按照结束位置降序,
// 这样排序的目的是保证排序后,前面的区间尽可能可以覆盖后面的区间
lines.sort((lineA, lineB) -> lineA[0] != lineB[0] ? lineA[0] - lineB[0] : lineB[1] - lineA[1]);
// 记录lines多个区间,求长度之和,(重叠部分只计算一次)
int height = 0;
int last_end = -1;
for (int[] line : lines) {
int start = line[0];
int end = line[1];
// 如果 last_end >= end, 则当前区间被上一个区间完全覆盖,因此可以跳过
// 如果 last_end < end
if (last_end < end) {
// 则当前区间的不重叠部分是 [max(start, last_end), end]
height += end - Math.max(start, last_end);
// 更新last_end
last_end = end;
}
}
// 当前扫描线扫描到的面积为 width * height
ans += (long) width * height;
ans %= (int) (1e9 + 7);
}
return (int) ans;
}
}
Python源码实现
class Solution(object):
def rectangleArea(self, rectangles):
"""
:type rectangles: List[List[int]]
:rtype: int
"""
# 统计所有矩形的左边边、右边边所在位置的x坐标
listX = []
for rect in rectangles:
listX.append(rect[0]) # 矩形左边边x坐标位置
listX.append(rect[2]) # 矩形右边边x坐标位置
# 所有x坐标升序(每个x视为一条扫描线)
listX.sort()
# 记录所有矩形并集面积
ans = 0
for i in range(1, len(listX)):
# 前一个扫描线x坐标
preX = listX[i - 1]
# 当前扫描线x坐标
curX = listX[i]
# 相邻两个扫描线的距离
width = curX - preX
# 距离为0, 则跳过
if width == 0:
continue
# 将在[x1,x2]区间上的矩形片段(垂直方向高度区间)收集起来
lines = []
# 遍历每个矩形
# 矩形左下角坐标(x1,y1),矩形右上角坐标(x2,y2)
for x1, y1, x2, y2 in rectangles:
# 如果矩形包含了 [x1, x2] 区间
if x1 <= preX and curX <= x2:
# 那么该矩形在 水平方向区间[x1, x2] 对应的 垂直方向区间为 [y1, y2]
lines.append((y1, y2))
# 将处于水方向区间 [x1, x2] 的所有垂直方向区间排序:按照起始位置升序, 如果起始位置相同, 则按照结束位置降序,这样排序的目的是保证排序后,前面的区间尽可能可以覆盖后面的区间
lines.sort(key=lambda line: (line[0], -line[1]))
# 记录lines多个区间,求长度之和,(重叠部分只计算一次)
height = 0
# 题目说坐标范围 [-100, 100], 因此对应 [?, -101] 的区间必然不会和任何区间相交
last_end = -1
# 如果 last_end >= end, 则当前区间被上一个区间完全覆盖,因此可以跳过
# 如果 last_end < end
for start, end in lines:
if last_end < end:
# 则当前区间的不重叠部分是 [max(start, last_end), end]
height += end - max(start, last_end)
# 更新last_end
last_end = end
# 当前扫描线扫描到的面积为 width * height
ans += width * height
return ans % 1000000007
JavaScript源码实现
/**
* @param {number[][]} rectangles
* @return {number}
*/
var rectangleArea = function (rectangles) {
// 统计所有矩形的左边边、右边边所在位置的x坐标
const listX = [];
for (let rect of rectangles) {
listX.push(rect[0]); // 矩形左边边x坐标位置
listX.push(rect[2]); // 矩形右边边x坐标位置
}
// 所有x坐标升序(每个x视为一条扫描线)
listX.sort((a, b) => a - b);
// 记录所有矩形并集面积
let ans = 0n;
for (let i = 1; i < listX.length; i++) {
// 前一个扫描线x坐标
const preX = listX[i - 1];
// 当前扫描线x坐标
const curX = listX[i];
// 相邻两个扫描线的距离
const width = curX - preX;
// 距离为0, 则跳过
if (width == 0) continue;
// 将处于[x1,x2]区间上的矩形片段(垂直方向高度区间)收集起来
const lines = [];
// 遍历每个矩形
// 矩形左下角坐标(x1,y1),矩形右上角坐标(x2,y2)
for (let [x1, y1, x2, y2] of rectangles) {
// 如果矩形有片段处于 [x1, x2] 区间
if (x1 <= preX && curX <= x2) {
// 那么该矩形在 水平方向区间[x1, x2] 对应的 垂直方向区间为 [y1, y2]
lines.push([y1, y2]);
}
}
// 将处于水方向区间 [x1, x2] 的所有垂直方向区间排序:按照起始位置升序, 如果起始位置相同, 则按照结束位置降序,这样排序的目的是保证排序后,前面的区间尽可能可以覆盖后面的区间
lines.sort((lineA, lineB) =>
lineA[0] != lineB[0] ? lineA[0] - lineB[0] : lineB[1] - lineA[1]
);
// 记录lines多个区间,求长度之和,(重叠部分只计算一次)
let height = 0;
let last_end = -1;
for (let [start, end] of lines) {
// 如果 last_end >= end, 则当前区间被上一个区间完全覆盖,因此可以跳过
// 如果 last_end < end
if (last_end < end) {
// 则当前区间的不重叠部分是 [max(start, last_end), end]
height += end - Math.max(start, last_end);
// 更新last_end
last_end = end;
}
}
// 当前扫描线扫描到的面积为 width * height
ans += BigInt(width) * BigInt(height);
}
return Number(ans % 1000000007n);
};