P5490 【模板】扫描线 & 矩形面积并

提供一个扫描线模板代码,后面附带和人类结晶ds的扫描线答疑过程

#include<bits/stdc++.h>
#define ll long long
#define lc rt << 1
#define rc rt << 1 | 1
#define lson lc, l, mid
#define rson rc, mid + 1, r
using namespace std;
const int N = 2e5 + 10;

struct ScanLine {
    ll x, y1, y2;
    int flag; // 1表示入边,-1表示出边
} line[N << 1];  // 每个矩形产生2条边

struct Node {
    ll len;   // 当前区间被覆盖的总长度
    int cnt;  // 当前区间被完全覆盖的次数
} t[N << 3];  // 线段树开8倍空间

ll ys[N << 1]; // 静态数组存储离散化后的y坐标
int n, m, cnt_y; // cnt_y记录离散化后的y坐标数量

void pushup(int rt, int l, int r) {
    if (t[rt].cnt) {
        t[rt].len = ys[r + 1] - ys[l];
    } else if (l != r) {
        t[rt].len = t[lc].len + t[rc].len;
    } else {
        t[rt].len = 0;
    }
}

void build(int rt, int l, int r) {
    t[rt] = {0, 0};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lson);
    build(rson);
}

void update(int rt, int l, int r, int L, int R, int val) {
    if (R < l || L > r) return;
    if (L <= l && r <= R) {
        t[rt].cnt += val;
        pushup(rt, l, r);
        return;
    }
    int mid = (l + r) >> 1;
    update(lson, L, R, val);
    update(rson, L, R, val);
    pushup(rt, l, r);
}

bool cmp(const ScanLine &a, const ScanLine &b) {
    return a.x < b.x;
}

int main() {
    scanf("%d", &n);
    cnt_y = 0;
    for (int i = 1; i <= n; i++) {
        ll x1, y1, x2, y2;
        scanf("%lld%lld%lld%lld", &x1, &y1, &x2, &y2);
        line[2 * i - 1] = {x1, y1, y2, 1};  // 入边
        line[2 * i] = {x2, y1, y2, -1};     // 出边
        ys[++cnt_y] = y1;
        ys[++cnt_y] = y2;
    }

    // 离散化y坐标
    sort(ys + 1, ys + cnt_y + 1);
    cnt_y = unique(ys + 1, ys + cnt_y + 1) - ys - 1;
    m = cnt_y - 1; // 线段树管理的区间是[1, m]

    // 按照x坐标排序扫描线
    sort(line + 1, line + 2 * n + 1, cmp);

    build(1, 1, m); // 线段树区间从1开始

    ll res = 0;
    for (int i = 1; i <= 2 * n; i++) {
        if (i > 1) {
            res += t[1].len * (line[i].x - line[i - 1].x);
        }
        
        // 找到离散化后的y坐标(下标从1开始)
        int y1 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y1) - ys;
        int y2 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y2) - ys - 1;
        
        update(1, 1, m, y1, y2, line[i].flag);
    }

    printf("%lld\n", res);
    return 0;
}

 

在扫描线算法中,1 和 -1 用于标记矩形的 入边(左边界) 和 出边(右边界),这是为了高效维护当前 y轴区间被覆盖的次数。下面我会详细解释它们的意义和作用原理:


1. 为什么用 1 和 -1

  • 1(入边)
    当扫描线遇到矩形的 左边界(x=x₁) 时,表示从这个 x 坐标开始,该矩形的 y 区间 [y₁, y₂] 开始被覆盖。此时需要 +1 表示新增覆盖。

  • -1(出边)
    当扫描线遇到矩形的 右边界(x=x₂) 时,表示从这个 x 坐标开始,该矩形的 y 区间 [y₁, y₂] 不再被覆盖。此时需要 -1 表示移除覆盖。


2. 覆盖次数的维护

线段树的每个节点维护两个关键信息:

  • cnt:当前 y 区间 被完全覆盖的次数(直接由 1 和 -1 更新)。

  • len:当前 y 区间 实际被覆盖的总长度(通过 cnt 计算)。

操作逻辑

  • 遇到入边(+1)

    • cnt += 1 → 如果 cnt > 0,说明该区间被至少一个矩形覆盖,len 直接取区间长度。

  • 遇到出边(-1)

    • cnt -= 1 → 如果 cnt == 0,说明该区间不再被覆盖,len 由子区间的覆盖情况决定。


3. 具体例子

假设有一个矩形 (x₁=2, y₁=3, x₂=5, y₂=6),扫描线会生成两条边:

line[1] = {2, 3, 6, 1};  // 左边界(入边)
line[2] = {5, 3, 6, -1}; // 右边界(出边)

扫描过程

  1. 扫描到 x=2(入边)

    • 更新 y 区间 [3, 6] 的 cnt += 1

    • 此时 cnt=1 → len = 6-3 = 3(区间被覆盖)

  2. 扫描到 x=5(出边)

    • 更新 y 区间 [3, 6] 的 cnt -= 1

    • 此时 cnt=0 → len = 0(区间不再被覆盖)


4. 为什么这样设计?

  • 高效维护覆盖状态
    通过 +1 和 -1 的差分操作,可以 动态维护当前 x 位置下 y 轴各区间的覆盖次数,无需遍历所有矩形。

  • 支持重叠区域
    如果多个矩形的 y 区间重叠,cnt 会累加。例如:

    • 两个矩形覆盖同一 y 区间 → cnt=2

    • 退出一个矩形后 → cnt=1(仍被覆盖,不影响 len 计算)

  • 面积计算的正确性
    相邻扫描线之间的面积 = 当前总覆盖长度 (t[1].len) × (x₂ - x₁)1 和 -1 保证了 len 的准确更新。


5. 对比其他标记方法

标记值含义作用
1 入边(开始覆盖) 增加 y 区间的覆盖次数
-1 出边(结束覆盖) 减少 y 区间的覆盖次数
0 无效 不用于扫描线算法(会丢失覆盖信息)

总结

    • 1 和 -1 是差分思想的体现,高效维护区间覆盖状态。

    • 入边 (+1):开始覆盖某个 y 区间。

    • 出边 (-1):结束覆盖某个 y 区间。

    • 线段树通过 cnt 和 len 的配合,最终计算出 所有矩形的并集面积

 

 

cnt_y = unique(ys + 1, ys + cnt_y + 1) - ys - 1;和int y1 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y1) - ys; int y2 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y2) - ys - 1; 这些里面最后的-ys和-ys - 1的意义是?
 
 

这些表达式中的 -ys 和 -ys - 1 是 指针/迭代器算术运算 的关键部分,用于将 迭代器位置转换为数组下标。下面我会详细解释它们的意义和计算逻辑:


1. unique(ys + 1, ys + cnt_y + 1) - ys - 1

作用

计算 去重后 ys 数组的实际有效长度(即离散化后的不同 y 坐标数量)。

分步解析

  1. unique(ys + 1, ys + cnt_y + 1)

    • unique 函数将 ys[1..cnt_y] 范围内的 相邻重复元素移到末尾,并返回 指向去重后末尾的下一个位置的指针

    • 例如,假设 ys = [0, 100, 100, 150, 200, 200, 200]cnt_y=6),去重后可能变为 [0, 100, 150, 200, x, x, x]unique 返回指向第一个 x 的指针。

  2. - ys

    • 用 unique 返回的指针 减去数组首地址 ys,得到的是 去重后的元素个数(包含无效部分)

    • 例如,若 unique 返回 ys + 4,则 ys + 4 - ys = 4(表示前4个元素有效)。

  3. - 1

    • 因为 ys 的下标从 1 开始,所以需要 减去1 来得到 实际存储的有效坐标数量

    • 例如,unique 返回 ys + 4 → 4 - 1 = 3,表示 ys[1..3] 是去重后的坐标。

最终结果

  • cnt_y = unique(...) - ys - 1 的值是 去重后 ys 数组的有效长度(即离散化后的不同 y 坐标数量)。


2. lower_bound(ys + 1, ys + cnt_y + 1, val) - ys

作用

在离散化后的 ys 数组中 二分查找 val 的下标(从 1 开始)。

分步解析

  1. lower_bound(ys + 1, ys + cnt_y + 1, val)

    • 在 ys[1..cnt_y] 范围内查找 第一个大于等于 val 的元素的指针

    • 例如,ys = [0, 100, 150, 200]val=150 时返回指向 ys[3] 的指针。

  2. - ys

    • 用 lower_bound 返回的指针 减去数组首地址 ys,得到的是 val 在 ys 中的下标(1-based)

    • 例如,返回 ys + 3 → ys + 3 - ys = 3(表示 val 是 ys[3])。

用于 y1 的查找

  • y1 = lower_bound(...) - ys 直接得到 line[i].y1 在 ys 中的下标(从 1 开始)。


3. lower_bound(ys + 1, ys + cnt_y + 1, val) - ys - 1

作用

在离散化后的 ys 数组中 二分查找 val 的前一个位置的下标,用于 线段树区间右端点

为什么需要 -1

  • 线段树的每个叶子节点表示的是一个 区间 [ys[i], ys[i+1]],而不是单个点。

  • 对于矩形的 y2,我们需要找到 覆盖的区间右边界,即 ys[i] 对应的线段树区间是 i-1

  • 例如:

    • ys = [0, 100, 150, 200](下标 1..3

    • val = 150 时:

      • lower_bound 返回 ys + 2(值 150

      • - ys 得到 2

      • -1 后得到 1,对应线段树管理的区间 [ys[1], ys[2]] = [100, 150]

用于 y2 的查找

  • y2 = lower_bound(...) - ys - 1 得到 line[i].y2 对应的 线段树区间右端点下标


总结

表达式作用示例(假设 ys = [0, 100, 150, 200]
unique(...) - ys - 1 计算去重后的有效长度 unique 返回 ys+4 → 4-1=3(有效 ys[1..3]
lower_bound(...) - ys 查找值对应的下标(1-based) val=150 → 3
lower_bound(...) - ys - 1 查找线段树区间右端点 val=150 → 2(对应区间 [ys[2], ys[3]]

关键点

  1. -ys:将指针转换为数组下标(1-based)。

  2. -1

    • 在 unique 中:调整下标从 1 开始的计数。

    • 在 lower_bound 中:将点坐标转换为线段树管理的区间下标。

  3. 线段树区间:每个叶子节点管理的是 [ys[i], ys[i+1]],所以 y2 需要 -1

这样设计保证了离散化后的坐标能正确映射到线段树的区间上,从而高效计算覆盖面积。

posted @ 2025-05-22 10:50  CRt0729  阅读(43)  评论(0)    收藏  举报