【850. 矩形面积 II】

来源:力扣(LeetCode)

描述:

我们给出了一个(轴对齐的)二维矩形列表 rectangles 。 对于 rectangle[i] = [x1, y1, x2, y2],其中 (x1,y1) 是矩形 i 左下角的坐标, (xi1, yi1) 是该矩形 左下角 的坐标, (xi2, yi2) 是该矩形 右上角 的坐标。

计算平面中所有 rectangles 所覆盖的 总面积 。任何被两个或多个矩形覆盖的区域应只计算 一次

返回 总面积 。因为答案可能太大,返回 109 + 7 的 模 。

示例 1:

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 <= 109
  • 矩形叠加覆盖后的总面积不会超越 263 - 1 ,这意味着可以用一个 64 位有符号整数来保存面积结果。

方法一:离散化 + 扫描线 + 使用简单数组实时维护

思路与算法

  我们先解释扫描线的概念:想象一条竖直的直线从平面的最左端扫到最右端,在扫描的过程中,直线上的一些线段会被给定的矩形覆盖。将这些覆盖的线段长度进行积分,就可以得到矩形的面积之和。每个矩形有一个左边界和一个右边界,在扫描到矩形的左边界时,覆盖的长度可能会增加;在扫描到矩形的右边界时,覆盖的长度可能会减少。如果给定了 n 个矩形,那么覆盖的线段长度最多变化 2n 次,此时我们就可以将两次变化之间的部分合并起来,一起计算:即这一部分矩形的面积,等于覆盖的线段长度,乘以扫描线在水平方向移动过的距离。

  因此,我们可以首先将所有矩形的左右边界按照横坐标进行排序,这样就确定了扫描线扫描的顺序。随后我们遍历这些左右边界,一次性地处理掉一批横坐标相同的左右边界,对应地增加或者减少覆盖的长度。在这之后,下一个未遍历到的坐右边界的横坐标,减去这一批左右边界的横坐标,就是扫描线在水平方向移动过的距离。

  那么我们如何维护「覆盖的线段长度」呢?这里同样可以使用到离散化的技巧(扫描线就是一种离散化的技巧,将大范围的连续的坐标转化成 2n 个离散的坐标)。由于矩形的上下边界也只有 2n 个,它们会将 yy 轴分成 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],即可得到「覆盖的线段长度」。

代码:

class Solution {
public:
    int rectangleArea(vector<vector<int>>& rectangles) {
        int n = rectangles.size();
        vector<int> hbound;
        for (const auto& rect: rectangles) {
            // 下边界
            hbound.push_back(rect[1]);
            // 上边界
            hbound.push_back(rect[3]);
        }
        sort(hbound.begin(), hbound.end());
        hbound.erase(unique(hbound.begin(), hbound.end()), hbound.end());
        int m = hbound.size();
        // 「思路与算法部分」的 length 数组并不需要显式地存储下来
        // length[i] 可以通过 hbound[i+1] - hbound[i] 得到
        vector<int> seg(m - 1);

        vector<tuple<int, int, int>> sweep;
        for (int i = 0; i < n; ++i) {
            // 左边界
            sweep.emplace_back(rectangles[i][0], i, 1);
            // 右边界
            sweep.emplace_back(rectangles[i][2], i, -1);
        }
        sort(sweep.begin(), sweep.end());

        long long ans = 0;
        for (int i = 0; i < sweep.size(); ++i) {
            int j = i;
            while (j + 1 < sweep.size() && get<0>(sweep[i]) == get<0>(sweep[j + 1])) {
                ++j;
            }
            if (j + 1 == sweep.size()) {
                break;
            }
            // 一次性地处理掉一批横坐标相同的左右边界
            for (int k = i; k <= j; ++k) {
                auto&& [_, idx, diff] = sweep[k];
                int left = rectangles[idx][1], right = rectangles[idx][3];
                for (int x = 0; x < m - 1; ++x) {
                    if (left <= hbound[x] && hbound[x + 1] <= right) {
                        seg[x] += diff;
                    }
                }
            }
            int cover = 0;
            for (int k = 0; k < m - 1; ++k) {
                if (seg[k] > 0) {
                    cover += (hbound[k + 1] - hbound[k]);
                }
            }
            ans += static_cast<long long>(cover) * (get<0>(sweep[j + 1]) - get<0>(sweep[j]));
            i = j;
        }
        return ans % static_cast<int>(1e9 + 7);
    }
};

执行用时:8 ms, 在所有 C++ 提交中击败了76.39%的用户
内存消耗:8.2 MB, 在所有 C++ 提交中击败了91.42%的用户
复杂度分析
时间复杂度:O(n2),其中 n 是矩形的个数。
空间复杂度:O(n),即为扫描线需要使用的空间。

方法二:离散化 + 扫描线 + 使用线段树实时维护

思路与算法

方法一中对于数组 seg 的所有操作都可以使用线段树进行维护。线段树中需要存储:

  • 该节点对应的区间被完整覆盖的次数;

  • 该节点对应的区间被覆盖的线段长度。

线段树需要支持:

  • 区间增加 1;

  • 区间减少 1,并且保证每个被增加 1 的区间在之后一定会减少 1;

对于所有非 0 的位置,根据它们的权值进行求和。

代码:

struct Segtree {
    int cover;
    int length;
    int max_length;
};

class Solution {
public:
    int rectangleArea(vector<vector<int>>& rectangles) {
        int n = rectangles.size();
        for (const auto& rect: rectangles) {
            // 下边界
            hbound.push_back(rect[1]);
            // 上边界
            hbound.push_back(rect[3]);
        }
        sort(hbound.begin(), hbound.end());
        hbound.erase(unique(hbound.begin(), hbound.end()), hbound.end());
        int m = hbound.size();
        // 线段树有 m-1 个叶子节点,对应着 m-1 个会被完整覆盖的线段,需要开辟 ~4m 大小的空间
        tree.resize(m * 4 + 1);
        init(1, 1, m - 1);

        vector<tuple<int, int, int>> sweep;
        for (int i = 0; i < n; ++i) {
            // 左边界
            sweep.emplace_back(rectangles[i][0], i, 1);
            // 右边界
            sweep.emplace_back(rectangles[i][2], i, -1);
        }
        sort(sweep.begin(), sweep.end());

        long long ans = 0;
        for (int i = 0; i < sweep.size(); ++i) {
            int j = i;
            while (j + 1 < sweep.size() && get<0>(sweep[i]) == get<0>(sweep[j + 1])) {
                ++j;
            }
            if (j + 1 == sweep.size()) {
                break;
            }
            // 一次性地处理掉一批横坐标相同的左右边界
            for (int k = i; k <= j; ++k) {
                auto&& [_, idx, diff] = sweep[k];
                // 使用二分查找得到完整覆盖的线段的编号范围
                int left = lower_bound(hbound.begin(), hbound.end(), rectangles[idx][1]) - hbound.begin() + 1;
                int right = lower_bound(hbound.begin(), hbound.end(), rectangles[idx][3]) - hbound.begin();
                update(1, 1, m - 1, left, right, diff);
            }
            ans += static_cast<long long>(tree[1].length) * (get<0>(sweep[j + 1]) - get<0>(sweep[j]));
            i = j;
        }
        return ans % static_cast<int>(1e9 + 7);
    }

    void init(int idx, int l, int r) {
        tree[idx].cover = tree[idx].length = 0;
        if (l == r) {
            tree[idx].max_length = hbound[l] - hbound[l - 1];
            return;
        }
        int mid = (l + r) / 2;
        init(idx * 2, l, mid);
        init(idx * 2 + 1, mid + 1, r);
        tree[idx].max_length = tree[idx * 2].max_length + tree[idx * 2 + 1].max_length;
    }

    void update(int idx, int l, int r, int ul, int ur, int diff) {
        if (l > ur || r < ul) {
            return;
        }
        if (ul <= l && r <= ur) {
            tree[idx].cover += diff;
            pushup(idx, l, r);
            return;
        }
        int mid = (l + r) / 2;
        update(idx * 2, l, mid, ul, ur, diff);
        update(idx * 2 + 1, mid + 1, r, ul, ur, diff);
        pushup(idx, l, r);
    }

    void pushup(int idx, int l, int r) {
        if (tree[idx].cover > 0) {
            tree[idx].length = tree[idx].max_length;
        }
        else if (l == r) {
            tree[idx].length = 0;
        }
        else {
            tree[idx].length = tree[idx * 2].length + tree[idx * 2 + 1].length;
        }
    }

private:
    vector<Segtree> tree;
    vector<int> hbound;
};

执行用时:4 ms, 在所有 C++ 提交中击败了97.85%的用户
内存消耗:8.5 MB, 在所有 C++ 提交中击败了78.11%的用户
复杂度分析
时间复杂度:O(nlogn),其中 n 是矩形的个数。
空间复杂度:O(n),即为线段树需要使用的空间。
author:LeetCode-Solution

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值