[计算几何入门]扫描线

#1.0 扫描线

#1.1 何为扫描线

扫描线,正如其名,算法的整个过程就像是用一条线在图像上扫过,常被用来解决图形面积、周长等问题。

#1.2 实际问题

题目链接

这是扫描线的经典问题。我们不妨将原本的图形放在平面直角坐标系上,如下图:

这样,我们再看这张图,不难发现原本的图形可以分为几个新的矩形:

不难发现,这些分割线总是其中原有矩形的顶或底,而图形面积则是这些分割线的长度乘上所经过的面积。

那我们是不是就可以依次访问这些线段,同时统计此时切割线的长度便可以完成对图形面积的计算。

#1.3 维护切割线长度

这一部分可以采用线段树维护,线段树上的每一个节点维护的是横坐标上的一个区间。

那么,我们可以将矩形的底边的权值设为 (1),顶的权值设为 (-1),每次对线段树上进行区间加法,如果一个节点的标记大于 (0),那么意味着这个节点所维护的区间此时被矩形覆盖。

#2.0 具体实现

上面的思路看起来很简单,但是具体实现起来仍存在许多细节需要注意。

#2.1 线段存储及离散化

我们需要将每个矩形的顶和底分别储存,同时按纵坐标自小向大地排序,同时需要记录这条线段地权值,所以我们可以声明如下的结构体:

struct SLine {
    ll l, r, h, w;
    /*l,r 分别表示线段的左端点和右端点
    h 表示高度,w 为线段的权值*/
    bool operator < (const SLine &b) const {
        return h < b.h;
    }
};
SLine line[N];

但是,需要注意到我们所建的线段树的叶节点不需要维护至点这么细,只需要维护一段即可,但即使是这样,原本的横坐标也有可能很大,但我们再线段树上维护时并不在意他们具体是多少,只知道相互之间的大小关系即可,所以我们需要将原本线段的端点横坐标离散化。

ll X[N];

sort(X + 1, X + n + 1);
int tot = unique(X + 1, X + n + 1) - X - 1;

注意这里的 (n) 是原本图形个数的 (2) 倍,因为每个矩形对应着两个横坐标(左端点和右端点)。

#2.2 线段树维护

线段树每个节点 (x) 对应的区间 p[x].l, p[x].r不变,改变区间和横边的映射关系,具体为:节点 (x) 对应 [X[p[x].l],X[p[x].r+1]] 这条横边,这样的话,线段树上叶节点维护的区间的左右端点在数值上是一样的,是一个点,但实际调用时是一个范围。

下面来看区间修改时的一个重点细节

void modify(int k, ll x, ll y, int c) {;
    if (y <= X[p[k].l] || x >= X[p[k].r + 1]) return;
    /*注意这里范围的判断是有 '=' 的,原因很简单,
    我们说过了,我们所有节点维护的是一个区间,
    而不是一个单纯的点,像 [1,1] 这样的一个
    点显然不会对答案做成贡献,也不应当出现在线段树上*/
    if (x <= X[p[k].l] && X[p[k].r + 1] <= y) {
        p[k].sum += c;
        pushup(k); return;
    }
    modify(p[k].ls, x, y, c);
    modify(p[k].rs, x, y, c);
    pushup(k);
}

#3.0 完整代码

const int N = 300010;
const int INF = 0x3fffffff;

struct SLine {
    ll l, r, h, w;
    bool operator < (const SLine &b) const {
        return h < b.h;
    }
};
SLine line[N];

struct Node {
    int l, r;
    int ls, rs;
    ll len, sum;
};
Node p[N << 1];

int cnt, n;
ll X[N];

inline void pushup(int k) {
    if (p[k].sum) p[k].len = X[p[k].r + 1] - X[p[k].l];
    else p[k].len = p[p[k].ls].len + p[p[k].rs].len;
}

void build(int k, int l, int r) {
    p[k].l = l, p[k].r = r;
    p[k].len = p[k].sum = 0;
    if (l == r) return;
    int mid = (l + r) >> 1;
    p[k].ls = ++ cnt;
    build(p[k].ls, l, mid);
    p[k].rs = ++ cnt;
    build(p[k].rs, mid + 1, r);
}

void modify(int k, ll x, ll y, int c) {;
    if (y <= X[p[k].l] || x >= X[p[k].r + 1]) return;
    if (x <= X[p[k].l] && X[p[k].r + 1] <= y) {
        p[k].sum += c;
        pushup(k); return;
    }
    modify(p[k].ls, x, y, c);
    modify(p[k].rs, x, y, c);
    pushup(k);
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++ i) {
        ll a1, a2, b1, b2;
        scanf("%lld%lld%lld%lld", &a1, &b1, &a2, &b2);
        X[2 * i - 1] = a1, X[2 * i] = a2;
        line[2 * i - 1] = (SLine) {a1, a2, b1, 1};
        line[2 * i] = (SLine) {a1, a2, b2, -1};
    }
    n <<= 1;
    sort(line + 1, line + n + 1);
    sort(X + 1, X + n + 1);
    int tot = unique(X + 1, X + n + 1) - X - 1;
    build(++ cnt, 1, tot - 1);
    ll ans = 0;
    for (int i = 1; i < n; i ++) {
        modify(1, line[i].l, line[i].r, line[i].w);
        ans += (line[i + 1].h - line[i].h) * p[1].len;
    }
    printf("%lld", ans);
    return 0;
}

参考资料

[1] 【学习笔记】扫描线 - NCC79601

[2] 扫描线 - OI Wiki

原文地址:https://www.cnblogs.com/Dfkuaid-210/p/scan_line.html