线段树之扫描线(HDU-1542)

线段树之扫描线(HDU-1542)

扫描线也算是线段树的经典应用了,本人也是因为扫描线而接触到线段树的,当时做题的时候发现扫描线都是处理一条条的线段,然后搭配上线段树,让我以为线段树的线段就是这么来的(其实我现在也不知道线段树为什么叫做线段树)。

一、常见应用题型

个人认为扫描线的做法主要是用在处理矩形并或者立体交的情况下,矩形并可以求的是多个矩形任意重叠后整个图形的面积 or 重叠了两次(多次)的区域面积 or 整个图形的周长 ……,立体交常见的是求多个长方体任意重叠后整个立体的体积 ……

如果使用普通其他的做法效率低下,使用线段树的话性能就十分优秀了!

二、基本思路

水平有限,这里只讨论平面矩阵交的情况:

在这里插入图片描述

如图所示的两个矩阵重叠在一起,假设我们现在需要求这个图形的总面积,不要想着求出三个矩阵的面积,然后两个大的减去重叠的矩阵面积,这种做法在两个矩阵的时候尚且可以接受,但是一旦数量变大,这样就极其麻烦。

换个想法,这个图形的面积可以由这三个部分组成:

在这里插入图片描述

这样子看的话,我们只需要从下面往上将三个矩形的面积相加就可以了!

那么如何让计算机替我们完成这样一件事呢,那就是线段树+扫描线啦!

在这里插入图片描述

我们需要保存这①-④四条线段的左右端点以及高度,即 left、right 以及 h 三个参数。

从下往上扫描,遇到线段①,记录线段的长度 len = x3 - x1,

继续,遇到线段②,面积 S += len * (y2-y1),记录线段的长度 len = x4 - x1,

遇到线段③,面积 S += len * (y3-y2),长度 len = x4 - x2,

遇到线段④,面积 S += len * (y4-y3),长度 len = 0,

结束,面积 S 就是矩形并的面积。

下面从实现的角度开始分析:

首先我们需要定义线段的结构保存每条线,

struct Edge {
    double left, right;
    double h;
    int flag;
    bool operator < (Edge other) const {
        return h < other.h;
    }
}edges[2*maxn];

为了正确计算当前总共线段的长度 len,因此需要将线段分成入边以及出边,用flag来区分。

而重载 < 比较字符,是为了后续的 sort 排序使用。

接下来我们还需要将线段涉及到的 x 坐标进行离散化并且记录,如果不清楚离散化的话,可以参考我博客中线段树专题的 “线段树之离散化”。

然后我们对所有的线段以及离散过后的 x 坐标数组进行 sort 排序并 unique 去重。

这样前期工作就做完了,我们就可以对开始建树了,先是结点的结构:

struct Node {
    int left, int right;
    int cnt;
    double len;
}tree[4*maxn]

其中 cnt 用来保存区间 [left, right] 这条线段被覆盖的次数,在扫描遇到入边的时候,则 cnt++,遇到出边则 cnt–,因为总是先扫描到入边再扫描到出边,因此可以保证 cnt 总是 >= 0 的;

len 用来保存区间 [left, right] 这条线段中需要被记录的线段长度,故当前总共线段的长度 = tree[0].len,很方便,当扫描到第 i 条 (i > 0) 线段的时候,面积 S += len * (edges[i]-edges[i-1]);

三、例题

Atlantis HDU - 1542

这道题目常常被用来当做矩阵并的模板。

题目大意:给定 n 个任意重叠的矩形地图(但地图的横边竖边分别平行于 x 轴与 y 轴),求重叠后整体的面积。

input:多组输入,第一行为 n,若 n = 0 则不处理结束程序,接下来 n 行每行都包含矩形地图的左上角坐标与右下角坐标。

output:按照 sample output 这样的形式输出整体的总面积,每组输入后要接一行空行。

sample input:

2
10 10 20 20
15 15 25 25.5
0

sample output:

Test case #1
Total explored area: 180.00 

线段树扫描线的裸题,正是要求矩形并的面积,注意数据是 double 类型的,这样就更加表明了对数据进行离散化的必要性,还要注意每组输出之后都要接一行空行。

参考代码:

// 线段树 扫描线 模板
#include<stdio.h>
#include<string>
#include<algorithm>
#include<iostream>
#define LL long long
using namespace std;

const int maxn =210;
LL N;
double x[4*maxn];

struct Edge {
	double left, right;
	double h;
	int flag;  // 判断是入边还是出边
	bool operator < (Edge other) const {
		return h < other.h;
	}
} edges[4*maxn];

struct Node {
	LL left, right;
	LL cnt;
	double len;
} tree[4*maxn];

// 二分查找 val 离散化后的值 
LL findPos(LL l, LL r, double val) {
	LL mid;
	while(l <= r) {
		mid = (l+r)/2;
		if(x[mid] > val) r = mid-1;
		else if(x[mid] < val) l = mid+1;
		else break;
	}
	return mid;
}

void build(LL k, LL l, LL r) {
	tree[k].left = l;
	tree[k].right = r;
	tree[k].len = 0;
	tree[k].cnt = 0;
	if(l == r) return ;
	LL mid = (l+r)/2;
	build(2*k, l, mid);
	build(2*k+1, mid+1, r);
}

void pushUp(LL k) {
	if(tree[k].cnt)//非0,整段覆盖
		tree[k].len = x[tree[k].right+1]-x[tree[k].left];
	else if(tree[k].left == tree[k].right)//叶子
		tree[k].len = 0;
	else//部分覆盖
		tree[k].len = tree[2*k].len + tree[2*k+1].len;
}

void update(LL k, LL l, LL r, LL val) {
	if(l <= tree[k].left && tree[k].right <= r) { //全部包含
		tree[k].cnt += val;
		pushUp(k);
		return ;
	}
	LL mid = (tree[k].left + tree[k].right)/2;
	if(l <= mid)
		update(2*k, l, r, val);
	if(r > mid)
		update(2*k+1, l, r, val);
	pushUp(k);//计算该区间被覆盖的总长度
}

int main() {
	LL K = 0;
	LL l, r;
	double x1, x2, y1, y2;
	while(~scanf("%d", &N), N) {
		LL cnt = 0;
		for(LL i = 1; i <= N; i++) {
			scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
			x[++cnt] = x1;
			edges[cnt].left = x1;
			edges[cnt].right = x2;
			edges[cnt].h = y1;
			edges[cnt].flag = 1;//下边
			x[++cnt] = x2;
			edges[cnt].left = x1;
			edges[cnt].right = x2;
			edges[cnt].h = y2;
			edges[cnt].flag = -1;//上边
		}
		sort(x+1, x+cnt+1);//排序
		sort(edges+1, edges+cnt+1);
		// 这里没有去重操作 可以加上 unique
		build(1, 1, cnt);
		double ans = 0;
		for(LL i = 1; i <= cnt; i++) { //拿出每条横线并且更新
			l = findPos(1, cnt, edges[i].left);
			r = findPos(1, cnt, edges[i].right)-1;
			update(1, l, r, edges[i].flag);
			ans += tree[1].len*(edges[i+1].h-edges[i].h);//求面积
		}
		printf("Test case #%d\nTotal explored area: %.2f\n\n", ++K, ans);
	}
	return 0;
}

【END】感谢观看!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值