LeetCode 850. 矩形面积 II

850. 矩形面积 II

给你一个轴对齐的二维数组 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],即可得到「覆盖的线段长度」。

这个问题是一个典型的扫描线算法的应用场景。扫描线算法通常用于计算一系列图形的覆盖区域,例如计算一系列线段、矩形或其他图形覆盖的总面积。以下是解决这个问题的详细步骤:

  1. 理解问题:我们需要计算所有给定矩形的覆盖区域,注意重叠部分只能计算一次。

  2. 收集边界:首先,我们需要收集所有矩形的左右边界,因为这些边界是扫描线算法中的关键事件点。

  3. 排序:将所有边界按照x坐标进行排序,以确定扫描线的扫描顺序。

  4. 初始化:初始化一个数组seg来记录每个线段在y轴上被覆盖的次数。同时,初始化答案ans为0。

  5. 处理事件:遍历所有排序后的边界,对于每个边界,根据它是矩形的左边界还是右边界,来增加或减少seg数组中相应线段的覆盖次数。

  6. 累加面积:在处理完一批具有相同x坐标的边界后,累加所有被覆盖线段的长度,乘以这批边界的宽度(即当前边界的x坐标与上一批边界x坐标的差值),得到这部分的面积,并将结果加到答案中。

  7. 模运算:由于结果可能很大,我们需要对结果进行10^9 + 7的模运算。

  8. 返回结果:最后返回答案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),即为扫描线需要使用的空间。

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值