给你一个轴对齐的二维数组 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 解释:答案是 10^18 对 (10^9 + 7) 取模的结果, 即 49 。
提示:
1 <= rectangles.length <= 200
rectanges[i].length = 4
0 <= xi1, yi1, xi2, yi2 <= 10^9
解法1:离散化 + 扫描线 + 排序
面对一个几何图形,计算机通过向某一个方向进行单线扫描,进行图形处理的方式叫做扫描线算法。
事件和事件处理程序
扫除线算法中最重要的就是对事件(Event)的处理。一个事件定义为当扫除线碰到点、边界、线等产生的进入或退出事件。
例如下图:
当扫描线碰到某一个矩形的左边界时将会产生一个进入事件,之后处理程序将对这个进入事件进行处理。
又或者当扫描线碰到某一个矩形的右边界时将会产生一个退出事件,之后处理程序将对这个退出事件进行处理。
一般的,我们将整个事件列表中的事件进行按照扫除线的扫描顺序进行排序,之后处理程序将会依次处理所有事件,而事件处理程序才是扫描线算法的核心。
我们先解释扫描线的概念:想象一条竖直的直线从平面的最左端扫到最右端,在扫描的过程中,直线上的一些线段会被给定的矩形覆盖。将这些覆盖的线段长度进行积分,就可以得到矩形的面积之和。每个矩形有一个左边界和一个右边界,在扫描到矩形的左边界时,覆盖的长度可能会增加;在扫描到矩形的右边界时,覆盖的长度可能会减少。如果给定了 n 个矩形,那么覆盖的线段长度最多变化 2n 次,此时我们就可以将两次变化之间的部分合并起来,一起计算:即这一部分矩形的面积,等于覆盖的线段长度,乘以扫描线在水平方向移动过的距离。
因此,我们可以首先将所有矩形的左右边界按照横坐标进行排序,这样就确定了扫描线扫描的顺序。随后我们遍历这些左右边界,一次性地处理掉一批横坐标相同的左右边界,对应地增加或者减少覆盖的长度。在这之后,下一个未遍历到的左右边界的横坐标,减去这一批左右边界的横坐标,就是扫描线在水平方向移动过的距离。
那么我们如何维护「覆盖的线段长度」呢?这里同样可以使用到离散化的技巧(扫描线就是一种离散化的技巧,将大范围的连续的坐标转化成 2n 个离散的坐标)。由于矩形的上下边界也只有 2n 个,它们会将 y 轴分成 2n+1 个部分,中间的 2n−1 个部分均为线段,会被矩形覆盖到(最外侧的 2 个部分为射线,不会被矩形覆盖到),并且每一个线段要么完全被覆盖,要么完全不被覆盖。因此我们可以使用两个长度为 2n−1 的数组 seg 和 length,其中 seg[i] 表示第 i 个线段被矩形覆盖的次数,length[i] 表示第 i 个线段的长度。当扫描线遇到一个左边界时,我们就将左边界覆盖到的线段对应的 seg[i] 全部加 1;遇到一个右边界时,我们就将右边界覆盖到的线段对应的 seg[i] 全部减 1。在处理掉一批横坐标相同的左右边界后,seg[i] 如果大于 0,说明它被覆盖,我们累加所有的 length[i],即可得到「覆盖的线段长度」。
这个问题是一个典型的扫描线算法的应用场景。扫描线算法通常用于计算一系列图形的覆盖区域,例如计算一系列线段、矩形或其他图形覆盖的总面积。以下是解决这个问题的详细步骤:
-
理解问题:我们需要计算所有给定矩形的覆盖区域,注意重叠部分只能计算一次。
-
收集边界:首先,我们需要收集所有矩形的左右边界,因为这些边界是扫描线算法中的关键事件点。
-
排序:将所有边界按照x坐标进行排序,以确定扫描线的扫描顺序。
-
初始化:初始化一个数组
seg
来记录每个线段在y轴上被覆盖的次数。同时,初始化答案ans
为0。 -
处理事件:遍历所有排序后的边界,对于每个边界,根据它是矩形的左边界还是右边界,来增加或减少
seg
数组中相应线段的覆盖次数。 -
累加面积:在处理完一批具有相同x坐标的边界后,累加所有被覆盖线段的长度,乘以这批边界的宽度(即当前边界的x坐标与上一批边界x坐标的差值),得到这部分的面积,并将结果加到答案中。
-
模运算:由于结果可能很大,我们需要对结果进行
10^9 + 7
的模运算。 -
返回结果:最后返回答案
ans
。
Java版:
class Solution {
public int rectangleArea(int[][] rectangles) {
final int MOD = (int) 1e9 + 7;
int n = rectangles.length;
Set<Integer> set = new HashSet();
List<int[]> sweep = new ArrayList<>();
for (int i = 0; i < n; i++) {
// 收集所有矩形的上下边界
set.add(rectangles[i][1]);
set.add(rectangles[i][3]);
// 创建扫描线事件列表
sweep.add(new int[]{rectangles[i][0], i, 1});
sweep.add(new int[]{rectangles[i][2], i, -1});
}
int[] hbound = set.stream().mapToInt(Integer::intValue).toArray();
// 对上下边界进行排序
Arrays.sort(hbound);
// 排序扫描线事件
Collections.sort(sweep, (a, b) -> {
if (a[0] != b[0]) {
return a[0] - b[0];
} else if (a[1] != b[1]) {
return a[1] - b[1];
} else {
return a[2] - b[2];
}
});
int m = hbound.length;
// 初始化seg数组,用于记录覆盖次数
int[] seg = new int[m - 1];
int s = sweep.size();
long ans = 0;
// 遍历扫描线事件
for (int i = 0; i + 1 < s; i++) {
int j = i;
// 找到具有相同x坐标的事件点的结束位置
while (j + 1 < s && sweep.get(i)[0] == sweep.get(j + 1)[0]) {
j++;
}
if (j + 1 == s) {
break;
}
// 计算当前事件点影响的线段
for (int k = i; k <= j; k++) {
int idx = sweep.get(k)[1];
int diff = sweep.get(k)[2];
int low = rectangles[idx][1];
int high = rectangles[idx][3];
for (int z = 0; z < m - 1; z++) {
if (hbound[z] >= low && hbound[z + 1] <= high) {
seg[z] += diff;
}
}
}
// 计算覆盖的线段长度
int cover = 0;
for (int k = 0; k < m - 1; k++) {
if (seg[k] > 0) {
cover += (hbound[k + 1] - hbound[k]);
}
}
// 累加面积
ans += (long) cover * (sweep.get(j + 1)[0] - sweep.get(j)[0]);
i = j;
}
return (int) (ans % MOD);
}
}
Python3版:
class Solution:
def rectangleArea(self, rectangles: List[List[int]]) -> int:
hbound = set()
sweep = list()
for i, rect in enumerate(rectangles):
# 收集所有矩形的上下边界
hbound.add(rect[1])
hbound.add(rect[3])
# 创建扫描线事件列表
sweep.append([rect[0], i, 1])
sweep.append([rect[2], i, -1])
# 对上下边界进行排序
hbound = sorted(hbound)
# 排序扫描线事件
sweep.sort()
m = len(hbound)
# 初始化seg数组,用于记录覆盖次数
seg = [0] * (m - 1)
ans = 0
i = 0
# 遍历扫描线事件
while i < len(sweep):
j = i
# 找到具有相同x坐标的事件点的结束位置
while j + 1 < len(sweep) and sweep[j + 1][0] == sweep[i][0]:
j += 1
if j + 1 == len(sweep):
break
# 计算当前事件点影响的线段
for k in range(i, j + 1):
_, idx, diff = sweep[k]
_, low, _, high = rectangles[idx]
for z in range(m - 1):
if hbound[z] >= low and hbound[z + 1] <= high:
seg[z] += diff
# 计算覆盖的线段长度
cover = sum(hbound[z + 1] - hbound[z] for z in range(m - 1) if seg[z] > 0)
# 累加面积
ans += cover * (sweep[j + 1][0] - sweep[j][0])
i = j + 1
return ans % (10**9 + 7)
复杂度分析
-
时间复杂度:O(n^2),其中 n 是矩形的个数。
-
空间复杂度:O(n),即为扫描线需要使用的空间。