扫描线讲解

解决矩形面积并问题:

模板题如下:

P5490【模板】扫描线

题目描述

n 个四边平行于坐标轴的矩形的面积并。

输入格式

第一行一个正整数 n

接下来 n 行每行四个非负整数 x1,y1,x2,y2,表示一个矩形的四个端点坐标为 (x1,y1),(x1,y2),(x2,y2),(x2,y1)。

输出格式

一行一个正整数,表示 n 个矩形的并集覆盖的总面积。

输入输出样例

输入 #1

2

100 100 200 200

150 150 250 255

输出 #1

18000

说明/提示

对于 20%的数据,1≤n≤1000。

对于100%的数据,1≤n≤1e5,0≤x1<x2<1e9,0≤y1<y2≤1e9。

Updated on 4.10 by Dengduck(口胡) & yummy(实现):增加了一组数据。


算法思想

上图为一张三个矩形面积并的扫描线简图:

在该平面图中,我们用一条直线自下而上扫描,每一次扫描会将整个图形划分成几个矩形,然后通过计算矩形面积并求和最终求出答案

如果现在还不明白该怎么实现,请看下例:

首先,我们要将 x 的数据进行离散化操作

离散化模板代码如下:

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &olda[i]);
        newa[i] = olda[i];
    }
    sort(olda + 1, olda + 1 + n);
    
    //int cnt = n;
    //cnt = unique(olda + 1, olda + 1 + n) - (olda + 1); // 去重,cnt是去重后的数量
    unique(olda + 1, olda + 1 + n);
    for (int i = 1; i <= n; i++) {
        newa[i] = lower_bound(olda + 1, olda + 1 + n, newa[i]) - olda;
    }
    for (int i = 1; i <= n; i++) {
        printf("%d ", newa[i]);
    }
    //printf("\ncnt = %d", cnt);

    return 0;
}

我们可以使用线段树维护一条线段,同时用另一个结构体保存每一个矩形上下两边的左右端点以及纵坐标。对于每一个矩形,我们将其下边(即入边)标记为 1上边(即出边)标记为 −1 ,用于判断该次扫描是否需要计算这一段线段长度(当标记和大于 0 时,需要计算该区间内线段长度;反之则不计算)。

扫描线结构体定义如下:

struct Line {
    int l; // 左端点 
    int r; // 右端点 
    int y; // y轴坐标 
    int flag; // 上边为-1,下边为1 
} line[4 * MAXN];

线段树结构体定义如下:

struct node {
    int l;
    int r;
    int tag; // 是否向下投影的标记
    long long sum; 
} tr[24 * MAXN];

这里线段树要维护的值有四个,l,r为左右区间,tag为是否向下投影的标记,即若tag的值大于0则向下投影,而sum是我们要求的区间和值

关于tag的处理我们使用一个函数 pushup(int k) 来操作,代码如下:

void pushup(int k) {
    if (tr[k].tag) { //tag大于0时
        tr[k].sum = (x[tr[k].r + 1] - x[tr[k].l]);
    }
    else if (tr[k].l == tr[k].r) {
        tr[k].sum = 0;
    }
    else {
        tr[k].sum = tr[k * 2].sum + tr[k * 2 + 1].sum;
    }
}

这里需要注意的是第三行:tr[k].sum = (x[tr[k].r + 1] - x[tr[k].l]);

我们线段树维护的线段的左右端点左闭右开,故右区间要加1再减左区间。

第五行,当该点是叶子结点是更新其值为0。

第七行,则是线段树的自底向上更新sum值

接着,我们要处理的是线段树标准的建树和更新操作。

建树代码如下:

void build(int k, int l, int r)
{
    tr[k].l = l;
    tr[k].r = r;
    tr[k].tag = 0;
    tr[k].sum = 0; 
    if (l == r) {
        return;
    }

    int mid = (l + r) >> 1;
    int lc = k * 2;
    int rc = k * 2 + 1;

    build(lc, l, mid);
    build(rc, mid + 1, r);

}

当然,这里由于建的是一个空树,没有任何值的传入,我们可以省略pushup自底向上更新的操作。

更新代码如下:

void update(int k, int l, int r, int v)
{
    if (tr[k].l == l && tr[k].r == r) {
        tr[k].tag += v;
        pushup(k);
        return;
    }

    int mid = (tr[k].l + tr[k].r) >> 1;        
    int lc = k * 2;
    int rc = k * 2 + 1;

    if (r <= mid) {
        update(lc, l, r, v);
    } else if (l > mid) {
        update(rc, l, r, v);
    } else {
        update(lc, l, mid, v);
        update(rc, mid + 1, r, v);
    }

    pushup(k);
}

这里与传统的更新操作的不同点在于第三行至第七行,可以发现tag值要加上v,而在main函数中v值传入的数据是line[i].flag,这个flag是记录上下边的标志,即tag值会进行加或减去1的操作

在main函数中,也存在一些坑点,先看代码:

int main()
{
    int n;
    int x1, y1, x2, y2;
    
    scanf("%d", &n);
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        cnt++;
        line[cnt].l = x1;
        line[cnt].r = x2;
        line[cnt].y = y1;
        line[cnt].flag = 1;
        x[cnt] = x1;
        cnt++;
        line[cnt].l = x1;
        line[cnt].r = x2;
        line[cnt].y = y2;
        line[cnt].flag = -1;
        x[cnt] = x2;
    }
    sort(line + 1, line + cnt + 1, cmp);
    sort(x + 1, x + cnt + 1);
    
    int num = unique(x + 1, x + cnt + 1) - (x + 1);
    
    build(1, 1, cnt - 1);
    long long sum = 0;
    for (int i = 1; i <= cnt - 1; i++) {
        int l = lower_bound(x + 1, x + num + 1, line[i].l) - x;
        int r = lower_bound(x + 1, x + num + 1, line[i].r) - x - 1;
        update(1, l, r, line[i].flag);
        sum += (tr[1].sum * (line[i + 1].y - line[i].y));
    }
    printf("%ld\n", sum);
    

    return 0;
}

代码中第十行至第二十一行都在进行一个操作,即初始化操作。将线段的左端点赋x1的值,右端点赋x2的值,扫描线的y轴坐标赋两次,一次入边的y值,一次出边的y值。flag标志边的性质。

接着,我们要对扫描线根据y值从小到大进行排序(扫描线自上而下扫描),故我们需要自定义一个排序规则:cmp,代码如下:

int cmp (Line a, Line b) {
    return a.y < b.y;
}

x1, x2, x3, ... , xn, 共组成的是(n-1)条线段(区间),故建树时结点数(cnt - 1)个。

由于第一条线处理的是它至第二条线之间的线段长,最后一条线段不用考虑,故for循环遍历边的序号为1至cnt - 1

我们前面已经提及,该线段树维护的左右端点左闭右开,故第三十二行有-1这一操作。

最后:

展示一下完整代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;

struct Line {
    int l; // 左端点 
    int r; // 右端点 
    int y; // y轴坐标 
    int flag; // 上边为-1,下边为1 
} line[4 * MAXN];

long long x[4 * MAXN];

int cmp (Line a, Line b) {
    return a.y < b.y;
}

struct node {
    int l;
    int r;
    int tag; // 是否向下投影的标记
    long long sum; 
} tr[24 * MAXN];

void build(int k, int l, int r)
{
    tr[k].l = l;
    tr[k].r = r;
    tr[k].tag = 0;
    tr[k].sum = 0; 
    if (l == r) {
        return;
    }

    int mid = (l + r) >> 1;
    int lc = k * 2;
    int rc = k * 2 + 1;

    build(lc, l, mid);
    build(rc, mid + 1, r);

}

void pushup(int k) {
    if (tr[k].tag) {
        tr[k].sum = (x[tr[k].r + 1] - x[tr[k].l]);
    }
    else if (tr[k].l == tr[k].r) {
        tr[k].sum = 0;
    }
    else {
        tr[k].sum = tr[k * 2].sum + tr[k * 2 + 1].sum;
    }
}

void update(int k, int l, int r, int v)
{
    if (tr[k].l == l && tr[k].r == r) {
        tr[k].tag += v;
        pushup(k);
        return;
    }

    int mid = (tr[k].l + tr[k].r) >> 1;        
    int lc = k * 2;
    int rc = k * 2 + 1;

    if (r <= mid) {
        update(lc, l, r, v);
    } else if (l > mid) {
        update(rc, l, r, v);
    } else {
        update(lc, l, mid, v);
        update(rc, mid + 1, r, v);
    }

    pushup(k);
}
 
int main()
{
    int n;
    int x1, y1, x2, y2;
    
    scanf("%d", &n);
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        cnt++;
        line[cnt].l = x1;
        line[cnt].r = x2;
        line[cnt].y = y1;
        line[cnt].flag = 1;
        x[cnt] = x1;
        cnt++;
        line[cnt].l = x1;
        line[cnt].r = x2;
        line[cnt].y = y2;
        line[cnt].flag = -1;
        x[cnt] = x2;
    }
    sort(line + 1, line + cnt + 1, cmp);
    sort(x + 1, x + cnt + 1);
    
    int num = unique(x + 1, x + cnt + 1) - (x + 1);
    
    build(1, 1, cnt - 1);
    long long sum = 0;
    for (int i = 1; i <= cnt - 1; i++) {
        int l = lower_bound(x + 1, x + num + 1, line[i].l) - x;
        int r = lower_bound(x + 1, x + num + 1, line[i].r) - x - 1;
        update(1, l, r, line[i].flag);
        sum += (tr[1].sum * (line[i + 1].y - line[i].y));
    }
    printf("%ld\n", sum);
    

    return 0;
}

 递交结果:

祝各位生活愉快!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值