矩形覆盖面积

原题链接

题目大意: 在标准直角坐标系中,若干个矩形区域被涂上了油漆(注意:矩形间可能重叠)。矩形的表示方式为 ( x 1 , y 1 , x 2 , y 2 ) (x_1,y_1,x_2,y_2) (x1,y1,x2,y2),代表矩形的两个对角点的坐标。求被油漆覆盖的区域一共有多大面积。

关键字:线段树扫描线法
(本题是线段树非常特殊的一种应用。)

n n n个矩形,竖边有 2 n 2n 2n个。每个竖边可以用四个参数 ( x , y 1 , y 2 , s t ) (x,y_1,y_2,st) (x,y1,y2,st)来描述,其中 s t st st用来区分同一个矩形中的两条边,反映的是该边的性质(入边 or 出边)。

struct Segment
{
    int x, y1, y2;
    int st; //反映该线段的性质:+1表示入边,-1表示出边
    bool operator< (const Segment &s)const //横坐标作为排序标准
    {
        return x < s.x;
    }
}seg[N * 2];

读入 n n n个矩形实际上就是读入这 2 n 2n 2n个线段:

int n;
cin >> n;
for (int i = 0, t = 0; i < n; i ++)
{
	int x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;
    seg[t ++] = {x1, y1, y2, +1}; seg[t ++] = {x2, y1, y2, -1};
}
sort(seg, seg + 2 * n); //根据横坐标对这2n个线段进行排序

在这里插入图片描述

扫描线法即以这 2 n 2n 2n个线段为基础,生成 2 n 2n 2n个扫描线(可能存在重合),每根直线上实际用到的长度矩形的高相邻扫描线之间的距离矩形的长,从而可以求出这两扫描线之间矩形的面积。将所有面积累加起来,即得最终总面积。

S 1 = ( s e g [ 1 ] . x − s e g [ 0 ] . x ) ⋅ h 1 S 2 = ( s e g [ 2 ] . x − s e g [ 1 ] . x ) ⋅ h 2 ⋮ S i = ( s e g [ i ] . x − s e g [ i − 1 ] . x ) ⋅ h i S_1=(seg[1].x-seg[0].x)\cdot h_1\\S_2=(seg[2].x-seg[1].x)\cdot h_2\\ \vdots\\S_i=(seg[i].x-seg[i-1].x)\cdot h_i S1=(seg[1].xseg[0].x)h1S2=(seg[2].xseg[1].x)h2Si=(seg[i].xseg[i1].x)hi S 总 = S 1 + S 2 + ⋯ + S 2 n − 1 S_总=S_1+S_2+\cdots+S_{2n-1} S=S1+S2++S2n1例如:
在这里插入图片描述
转化为计算:
在这里插入图片描述
这种方法的好处是:只需要从左往右扫,一步一更新即可。每次计算需要知道的信息有:

  1. 每个新矩形的的高度。
  2. 每个新矩形的宽度。

其中每个新矩形的宽度非常容易计算,只需要将扫描线进行排序,相邻扫描线相减即可求得。

而每个新矩形高度的计算则比较麻烦,因为扫描线上实际用到的长度受先前所枚举的扫描线的影响

具体分析如下:
在这里插入图片描述
首先问一个问题:②号矩形的高如何计算?
很直观的回答就是: 10 + ( 25.5 − 20 ) = 15.5 10+(25.5-20)=15.5 10+(25.520)=15.5,先算上①号矩形高,再加上②号矩形多出来的部分。
那③号矩形的高如何计算?
显然, 15.5 − ( 15 − 10 ) = 10.5 15.5-(15-10)=10.5 15.5(1510)=10.5,先用②号矩形的高那部分减去多出来的部分。

那么为什么有时候 “多出来” 是加上一个值,有时候 “多出来” 是减掉一个值呢?
这个问题是扫描线法最核心的问题 —— “入边” 和“出边” 的问题

定义一个矩形中,左侧的高为“入边”,右侧的高为“出边”。

其实所谓的从左往右(也可以是从上往下),就是扫描的方向

当从左往右扫,遇到入边扫描线,则对入边区间进行 + 1 + 1 +1,遇到出边扫描线,就对出边区间进行 − 1 - 1 1,这就是加入区间或减去区间的规律。

这其实是一种差分的思想,不断更新每个小区间被覆盖的次数,整体上,所有覆盖次数 ≥ 1 \ge 1 1的小区间构成了当前矩形的高

具体实现方面,按横坐标的大小枚举扫描线,为保证计算时使用的是最新的实际长度,需要对整个区间进行动态维护,常用的方法是:线段树

将整个区间以1个单位长度进行划分,线段树的每个底层结点都对应一个单位长度的区间

在这里插入图片描述
具体的存储结构为:

struct Node
{
    int l, r; //该结点对应的区间的边界单位区间(注意:并非端点,而是两端的单位区间!)
    int cnt;  //该结点对应的区间被覆盖的次数(完全覆盖)
    int len;  //该结点对应的区间被覆盖的长度
}tr[N * 4];

在这里插入图片描述
从而根结点tr[1]指向的是整个区间,tr[1].len始终反映的是当前区间的有效长度。

题目所给的编号方式为①,而线段树中则是按②进行存储。因此若向线段树中加入区间[seg[i].l, seg[i].r],则更新操作为:modify(1, seg[i].l, seg[i].r - 1, st)

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 10010;
struct Segment
{
    int x, y1, y2;
    int st; //表示该线段的性质:入 or 出
    bool operator< (const Segment &t)const //根据横坐标进行排序
    {
        return x < t.x;
    }
}seg[N * 2];
struct
{
    int l, r; 
    int cnt;  
    int len;  
}tr[N * 4];

void build(int u, int l, int r)  //建立线段树,底层结点为单位长度的小区间
{
    tr[u] = {l, r}; //其余的属性(cnt、len)默认置为0

    if (l != r) //若可以再半分
    {
        int mid = (l + r) / 2;
        build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
    }
}

void pushup(int u) //更新结点信息
{
    if (tr[u].cnt > 0) tr[u].len = tr[u].r - tr[u].l + 1; //该区间被完全覆盖
    else 
    {
    	if (tr[u].l == tr[u].r) tr[u].len = 0; //该区间没有被完全覆盖,又是单位区间,因此该区间就是完全没被覆盖,即被覆盖的长度为0
    	else tr[u].len = tr[2 * u].len + tr[2 * u + 1].len; //该区间没有被完全覆盖,但子孙结点可能会被完全覆盖
    }
}
void modify(int u, int L, int R, int st) //更新线段树(加入或删除一个区间)
{
    if (L <= tr[u].l && tr[u].r <= R)  //完全包含
    {
        tr[u].cnt += st;
        pushup(u);
    }
    else
    {
        int mid = (tr[u].l + tr[u].r) / 2;
        if (L <= mid) modify(2 * u, L, R, st);
        if (R > mid) modify(2 * u + 1, L, R, st);
        pushup(u);
    }
}

int main()
{
    build(1, 0, 9999); //初始化线段树

    int n;
    cin >> n;
    for (int i = 0, t = 0; i < n; i ++)
    {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        seg[t ++] = {x1, y1, y2, +1}; seg[t ++] = {x2, y1, y2, -1};
    }
    sort(seg, seg + 2 * n); //根据横坐标进行排序

    int res = 0;
    for (int i = 0; i < 2 * n; i ++)
    {
        if (i > 0) res += tr[1].len * (seg[i].x - seg[i - 1].x); //从第2条线开始算
        modify(1, seg[i].y1, seg[i].y2 - 1, seg[i].st); //加入(或删除)一个区间(根据k而定)
    }
    cout << res;

    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值