扫描线(线段树)

简介: 笔记

扫描线


线段树的一大应用是扫描线。


扫描线一个很经典的例题:在坐标轴上有若干个矩形,问他们覆盖的面积总和。


扫描线求重叠矩形面积

19.png


思路

使用一条垂直于X轴的直线,从左到右来扫描这个图形,明显,只有在碰到矩形的左边界或者右边界的时候,这个线段所扫描到的情况才会改变。

所以把所有矩形的入边,出边按 X 值排序。然后根据 X 值从小到大去处理。

用线段树来维护扫描到的情况,即每条线段映射到 Y 轴上进行区间维护。


算法流程

一条垂直于 x 轴的直线,从左边向右平移

此时 y 轴上有一棵线段树,它记录的是 y 轴上每个点的覆盖次数

每当遇到某个矩形的某一条边时,就计算面积 —— (该边 x 坐标 – 上一边 x 坐标)* (当前整个 y 轴上被覆盖的点数)

当遇到某个矩形的左边,将这个矩形的左边所对应的y轴区间的覆盖次数 + 1。当遇到某个矩形的右边时,就相应的 − 1。

继续向右移动……

(注意,3操作要在4操作之前!)


性质

如何计算面积?


两条扫描线的间距很容易得到,重点是线段树部分的实现。


线段树操作:


区间修改([L, R] + k)

区间查询(整个 y 轴上被覆盖次数大于 0 的点数)

在此可以发现这道题的两个性质:


对于询问,每次都查询的是根节点的信息,所以不需要query。


所有修改,都是成对出现,且对于每个区间一定是先执行 + 操作,在执行− 操作,即 cnt >= 0恒成立。


也就是说,相应的区间前后操作的都是线段树上完全相同的节点。(这很关键)

修改不需要pushdown的两种情况讨论:

cnt>0:

40.png



cnt=0:

本在pushdown中,add为0就不需要下传懒标记信息,等同于没pushdown。

又因为query函数没有,所以本题可以不用pushdown。


首先将所有矩形的入边,出边都存起来,然后根据 X 值排序。用结构体,来存这些信息,然后排序。

struct Line {
    double x, y1, y2;
    double d; // 表示+1 or -1
    bool operator<(const Line &A) const {
        return x < A.x;
    }
} line[N << 1];
  • 线段树节点
struct node {
    int l, r; 
    int cnt; // 重复包含次数
    double len; // 当前节点维护的区间内部,有效的线段长度
} tr[N << 3];
  • pushup
void pushup(int u) {
    // 当前区间被覆盖,该段长度就为右端点 + 1后在ys中的值 - 左端点在ys中的值
    if(tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
    // 该段不被完全覆盖,那么清空该段的len值,而由可能存在其子区间段对len的贡献来更新
    else if(tr[u].l != tr[u].r) tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
    // 叶子节点,len=0
    else tr[u].len = 0;
}

经典例题


247. 亚特兰蒂斯

传送门

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
struct Line {
    double x, y1, y2;
    double d;
    bool operator<(const Line &A) const {
        return x < A.x;
    }
} line[N << 1];
struct node {
    int l, r; 
    int cnt; // 重复包含次数
    double len; // 当前节点维护的区间内部,有效的线段长度
} tr[N << 3];
vector<double> ys;
int n, t;
int find(double x) {
    return lower_bound(ys.begin(), ys.end(), x) - ys.begin();
}
void pushup(int u) {
    // 当前区间被覆盖,该段长度就为右端点 + 1后在ys中的值 - 左端点在ys中的值
    if(tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
    // 该段不被完全覆盖,那么清空该段的len值,而由可能存在其子区间段对len的贡献来更新
    else if(tr[u].l != tr[u].r) tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
    // 叶子节点,len=0
    else tr[u].len = 0;
}
void build(int u, int l, int r) {
    tr[u] = {l, r, 0, 0};
    if(l == r) { return ; }
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
// 线段树中 l点到r点出现次数 +k
void modify(int u, int l, int r, int k) {
    if(tr[u].l >= l && tr[u].r <= r) {
        tr[u].cnt += k;
        pushup(u);
        return ;
    }
    int mid = tr[u].l + tr[u].r >> 1;
    if(l <= mid) modify(u << 1, l, r, k);
    if(r > mid) modify(u << 1 | 1, l, r, k);
    pushup(u);
}
int main()
{
    while(cin >> n, n) {
        ys.clear();
        double x1, y1, x2, y2; 
        int j = 0;
        for (int i = 0; i < n; i++) {
            cin >> x1 >> y1 >> x2 >> y2;
            line[j++] = {x1, y1, y2, 1};
            line[j++] = {x2, y1, y2, -1};
            ys.push_back(y1), ys.push_back(y2);
        }
        sort(line, line + j);
        sort(ys.begin(), ys.end());
        ys.erase(unique(ys.begin(), ys.end()), ys.end());
        build(1, 0, ys.size() - 2);
        double res = 0;
        for (int i = 0; i < j; i++) {
            if(i) res += tr[1].len * (line[i].x - line[i - 1].x);
            modify(1, find(line[i].y1), find(line[i].y2) - 1, line[i].d);
        }
        printf("Test case #%d\n", ++t);
        printf("Total explored area: %.2lf\n\n", res);
    }
    return 0;
}
相关文章
|
2月前
线段树(连载中)
线段树(连载中)
20 0
|
8月前
|
C++
线段树的区间修改
线段树的区间修改
26 0
|
2月前
|
算法 测试技术 C++
|
2月前
线段树最大子段
线段树最大子段
|
2月前
|
人工智能
线段树最大连续子段板子😂单调栈
线段树最大连续子段板子😂单调栈
|
2月前
|
C++
2589. 完成所有任务的最少时间(区间取点——贪心or线段树)
2589. 完成所有任务的最少时间(区间取点——贪心or线段树)
|
2月前
|
算法 测试技术 C#
【单调栈】【区间合并】LeetCode85:最大矩形
【单调栈】【区间合并】LeetCode85:最大矩形
|
2月前
|
索引 NoSQL 容器
树状数组与线段树
树状数组与线段树
|
11月前
初识线段树
初识线段树
32 0
|
存储 算法 Java
线段树SegmentTree
线段树SegmentTree