扫描线(线段树)

扫描线

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

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

扫描线求重叠矩形面积

在这里插入图片描述

思路

使用一条垂直于X轴的直线,从左到右来扫描这个图形,明显,只有在碰到矩形的左边界或者右边界的时候,这个线段所扫描到的情况才会改变。
所以把所有矩形的入边,出边按 X 值排序。然后根据 X 值从小到大去处理。
用线段树来维护扫描到的情况,即每条线段映射到 Y 轴上进行区间维护。


算法流程
  1. 一条垂直于 x 轴的直线,从左边向右平移
  2. 此时 y 轴上有一棵线段树,它记录的是 y 轴上每个点的覆盖次数
  3. 每当遇到某个矩形的某一条边时,就计算面积 —— (该边 x 坐标 – 上一边 x 坐标)* (当前整个 y 轴上被覆盖的点数)
  4. 当遇到某个矩形的左边,将这个矩形的左边所对应的y轴区间的覆盖次数 + 1。当遇到某个矩形的右边时,就相应的 − 1。
  5. 继续向右移动……

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


性质

如何计算面积?

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

线段树操作:

  1. 区间修改([L, R] + k)
  2. 区间查询(整个 y 轴上被覆盖次数大于 0 的点数)

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

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

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

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

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

  • c n t > 0 {cnt > 0} cnt>0

    • 执行修改操作时,如果父节点 u {u} u c n t {cnt} cnt 1 {1} 1变为了 0 {0} 0 t r [ u ] . c n t − 1 = = 0 {tr[u].cnt - 1 == 0} tr[u].cnt1==0),则其孩子 l s , r s {ls, rs} ls,rs c n t {cnt} cnt 也要相应减 1 {1} 1,如果 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt变为了 0 {0} 0,它的 l e n {len} len就发生变化了。而 t r [ u ] . l e n = t r [ l s ] . l e n + t r [ r s ] . l e n {tr[u].len = tr[ls].len + tr[rs].len} tr[u].len=tr[ls].len+tr[rs].len,用到的 t r [ l s ] . l e n {tr[ls].len} tr[ls].len 是没更新过的 l e n {len} len

    • 换句话说,modify执行到这里时,是从上到下的顺序。可是父节点却需要用到了孩子的信息,显然会有问题。如果说先pushdown算的孩子信息,那么最坏情况当一个cnt减到0,则就要递归到最底层。

    • 所以在此, t r [ u ] . c n t {tr[u].cnt} tr[u].cnt的含义是:当前区间被覆盖的次数,跟其它节点无关。

    • 那么一个节点代表的区间被覆盖的次数不需要继承其父亲的信息。因此需要去掉pushdown,当cnt减为0, t r [ u ] . l e n = t r [ l s ] . l e n + t r [ r s ] . l e n {tr[u].len = tr[ls].len + tr[rs].len} tr[u].len=tr[ls].len+tr[rs].len就是对的。因为 t r [ u ] . c n t {tr[u].cnt} tr[u].cnt减到0跟 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt没有关系,此时 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt是最新值。

  • c n t = 0 {cnt = 0} 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;
}
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ღCauchyོꦿ࿐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值