扫描线&离散化&线段树解决矩形面积并-洛谷P5490

https://www.luogu.com.cn/problem/P5490

题目描述

n n n 个四边平行于坐标轴的矩形的面积并。

输入格式

第一行一个正整数 n n n

接下来 n n n 行每行四个非负整数 x 1 , y 1 , x 2 , y 2 x_1, y_1, x_2, y_2 x1,y1,x2,y2,表示一个矩形的四个端点坐标为 ( x 1 , y 1 ) , ( x 1 , y 2 ) , ( x 2 , y 2 ) , ( x 2 , y 1 ) (x_1, y_1),(x_1, y_2),(x_2, y_2),(x_2, y_1) (x1,y1),(x1,y2),(x2,y2),(x2,y1)

输出格式

一行一个正整数,表示 n n n 个矩形的并集覆盖的总面积。

输入输出样例 #1

输入 #1

2
100 100 200 200
150 150 250 255

输出 #1

18000

说明/提示

对于 20 % 20\% 20% 的数据, 1 ≤ n ≤ 1000 1 \le n \le 1000 1n1000
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 10 5 1 \le n \le {10}^5 1n105 0 ≤ x 1 < x 2 ≤ 10 9 0 \le x_1 < x_2 \le {10}^9 0x1<x2109 0 ≤ y 1 < y 2 ≤ 10 9 0 \le y_1 < y_2 \le {10}^9 0y1<y2109

C++ 代码实现

#include 
#include 
#include 

using namespace std;

struct Event {
    int x, y1, y2, type; // x 坐标, y 起点, y 终点, type=1(加入), type=-1(删除)
    Event(int x, int y1, int y2, int type) : x(x), y1(y1), y2(y2), type(type) {}
};

struct SegmentTree {
    // y 轴区间被覆盖次数机覆盖长度
    vector<int> cnt, len;
    // 存储离散化后的 y 坐标值,用于映射 y 轴上的真实值到线段树的索引
    vector<int> y_coords;

    SegmentTree(int size) {
        cnt.resize(size * 4);
        len.resize(size * 4);
    }

    void build(int l, int r, int idx) {
        cnt[idx] = len[idx] = 0;
        if (l + 1 == r) return;
        int mid = (l + r) / 2;
        build(l, mid, idx * 2);
        build(mid, r, idx * 2 + 1);
    }

    void update(int jobl, int jobr, int val, int l, int r, int idx) {
        if (jobl >= r || jobr <= l) return;
        if (jobl <= l && r <= jobr) {
            cnt[idx] += val;
        } else {
            int mid = (l + r) / 2;
            update(jobl, jobr, val, l, mid, idx * 2);
            update(jobl, jobr, val, mid, r, idx * 2 + 1);
        }

        // 计算当前区间的 y 方向被覆盖的长度
        if (cnt[idx] > 0) {
            len[idx] = y_coords[r] - y_coords[l];  // 计算该段区间长度
        } else {
            len[idx] = (l + 1 == r) ? 0 : (len[idx * 2] + len[idx * 2 + 1]);  // 合并子区间
        }
    }
};

int main() {
    int n;
    cin >> n;
    vector<Event> events;
    vector<int> y_coords;

    // 读取矩形数据
    for (int i = 0; i < n; i++) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        events.emplace_back(x1, y1, y2, 1);  // 左边界
        events.emplace_back(x2, y1, y2, -1); // 右边界
        y_coords.push_back(y1);
        y_coords.push_back(y2);
    }

    // 对 y 坐标进行离散化
    sort(y_coords.begin(), y_coords.end());
    y_coords.erase(unique(y_coords.begin(), y_coords.end()), y_coords.end());

    // 给事件按照 x 轴排序
    sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
        return a.x < b.x;
    });

    // 初始化线段树
    SegmentTree segTree(y_coords.size());
    segTree.y_coords = y_coords;
    segTree.build(0, y_coords.size() - 1, 1);

    long long area = 0;
    for (size_t i = 0; i < events.size() - 1; i++) {
        int x = events[i].x;
        int y1 = lower_bound(y_coords.begin(), y_coords.end(), events[i].y1) - y_coords.begin();
        int y2 = lower_bound(y_coords.begin(), y_coords.end(), events[i].y2) - y_coords.begin();
        segTree.update(y1, y2, events[i].type, 0, y_coords.size() - 1, 1);

        // 计算面积增量
        int dx = events[i + 1].x - x;
        area += 1LL * dx * segTree.len[1];  // 累加当前 x 范围的面积
    }

    cout << area << endl;

    return 0;
}

解法思路:扫描线 + 线段树

核心思想:

  1. 转换为扫描线问题

    • 把每个矩形拆成两条竖直边(左边界 + 右边界)。
    • 每条边记录 x 坐标、y 区间以及是左边界(加入)还是右边界(删除)
  2. x 坐标排序

    • 依次处理 x 变化的位置,维护 y 方向的覆盖情况。
  3. 使用线段树维护 y 方向的覆盖长度

    • 统计当前 x 坐标下 y 方向被覆盖的总长度。
    • x 变化时,用 dx * covered_length 计算新增的面积。

代码解析

  1. 读取输入并创建事件

    • 每个矩形 (x1, y1, x2, y2) 被拆成两个事件:
      • 左边界(x1, y1, y2, +1)
      • 右边界(x2, y1, y2, -1)
  2. 离散化 y

    • y 轴可能范围很大,使用排序+去重y 压缩成 [0, m-1] 范围。
  3. 排序事件

    • 按照 x 轴排序,保证扫描顺序是从左到右。
  4. 扫描线遍历

    • 遍历 x 方向的边界事件,更新 y 方向的覆盖长度。
    • 计算当前 x 范围的面积增量 dx * covered_length
  5. 线段树维护 y 方向覆盖情况

    • update(y1, y2, type) 更新 cnt 计数。
    • len[1] 存储 y 方向被覆盖的总长度。

复杂度分析

  • 事件排序 O ( N log ⁡ N ) O(N \log N) O(NlogN)
  • 线段树更新 O ( N log ⁡ N ) O(N \log N) O(NlogN)
  • 总时间复杂度 O ( N log ⁡ N ) O(N \log N) O(NlogN)

✅ 适用场景

  • 计算多个矩形的面积并
  • 计算建筑投影面积
  • 计算不规则区间合并

该方法适用于 N ≤ 10^5 的情况,效率极高!


线段树的构建过程中,我们通常将一个区间 [l, r] 拆分,直到无法继续拆分为止。
在很多常见的线段树应用(如单点更新、区间查询)中,叶子节点往往是 l == r,但是在扫描线+线段树这种应用场景下,我们的离散化 y 坐标并不连续,因此叶子节点的定义有所不同l + 1 == r 才是叶子节点,而不是 l == r


为什么 l + 1 == r 才是叶子节点?

离散化的线段树是基于区间而不是单个点。

  • 常规线段树(连续区间)

    • 例如求区间最小值、区间和,通常是 l == r 作为叶子节点。
    • 但在这里,我们是计算“区间长度”,所以叶子节点应该是最小单位的区间 [y_coords[l], y_coords[r]],而不是单个点。
  • 离散化线段树(区间合并)

    • 叶子节点应该是最小的 y 轴区间,而 l == r 只表示单个坐标,不是一个“区间”。
    • l + 1 == r,意味着 y_coords[l]y_coords[r] 之间已经没有更多的离散坐标,可以认为它们形成了最小单位的“区间”,无法再继续细分,因此它是叶子节点。

详细举例

1️⃣ 假设 y_coords = {2, 5, 10, 15}

离散化后的 y_coords 索引:

y 离散索引
2 0
5 1
10 2
15 3

表示的区间:

区间 [0,1] 表示 y = [2, 5]
区间 [1,2] 表示 y = [5, 10]
区间 [2,3] 表示 y = [10, 15]

2️⃣ 线段树的构造

构建线段树时,我们递归拆分区间 [0,3]

          [0,3]
         /     \
      [0,2]   [2,3]
     /     \
  [0,1]   [1,2]

在这棵树中:

  1. [0,1] 表示 [2,5],是叶子节点(因为 0+1 == 1
  2. [1,2] 表示 [5,10],是叶子节点(因为 1+1 == 2
  3. [2,3] 表示 [10,15],是叶子节点(因为 2+1 == 3

3️⃣ 为什么 l + 1 == r 作为叶子节点,而 l == r 不行?

如果 l == r 作为叶子节点,那就会出现 l = 0, r = 0 这种情况,这样的区间没有物理意义,因为:

  • y_coords[0] = 2 只是一个点,并不能表示一个范围。
  • y_coords[0]y_coords[1] ([2,5]) 形成了一个“区间”,才有意义。

所以,叶子节点的最小单位应该是 [y_coords[l], y_coords[r]],而不是单个点


✅ 结论

方式 叶子节点定义 适用情况 是否适用于扫描线+线段树
l == r 单个点 适用于连续值(如 RMQ、区间和) ❌ 不适用于离散区间
l + 1 == r 一个离散区间 适用于离散化的线段树(扫描线) ✅ 适用

代码示例

if (l + 1 == r) { // 叶子节点
    len[idx] = 0;  // 初始状态下没有被覆盖
} else { // 非叶子节点,合并左右子树
    len[idx] = len[idx * 2] + len[idx * 2 + 1];
}

总结

  • 线段树的叶子节点是最小单位的“区间”而不是单点,所以 l + 1 == r 作为叶子节点。
  • 常规线段树 l == r 适用于单点查询,但扫描线 + 线段树需要处理区间,所以 l + 1 == r 作为叶子节点
  • 这样,我们才能用 y_coords[r] - y_coords[l] 计算被覆盖的区间长度,否则 l == r 没有意义。

这样就能理解为什么 l + 1 == r 是叶子节点,而 l == r 不行了!

扫描线相关练习题目

  • 218. 天际线问题
  • P1904 天际线

你可能感兴趣的:(算法,数据结构,C++,线段树,扫描线)