zoukankan      html  css  js  c++  java
  • [计算几何入门]扫描线

    #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

  • 相关阅读:
    剑指offer——用两个栈实现队列
    根据前序和中序重建二叉树、后序和中序重建二叉树
    归并排序
    排序
    快速排序(java版)
    List
    单链表的基本操作
    集合
    数组
    结构体
  • 原文地址:https://www.cnblogs.com/Dfkuaid-210/p/scan_line.html
Copyright © 2011-2022 走看看