扫描线及其应用

前言

本文例题链接

定义

在一个笛卡尔坐标系内,用一根无限长线在此坐标系内扫描,这根线就叫做扫描线,通俗易懂。

通常情况下,在坐标系内确定一条线段需要两个端点。但在特殊情况下,如该直线平行于 \(y\) 轴,只需要三个信息来确定:端点的纵坐标,任意一点的横坐标。

即是:

struct Scan_Line {
	int CoordX, CoordY_Up, CoordY_Down; 
	Scan_Line() {}
	Scan_Line(int X, int YD, int YU, int A) {
		CoordX = X;//任意一点的横坐标
		CoordY_Down = YD;//下端点的纵坐标
		CoordY_Up = YU;//上端点的纵坐标
	}
	#define X(x) Line[x].CoordX
	#define YD(x) Line[x].CoordY_Down
	#define YU(x) Line[x].CoordY_Up
};

扫描线比较特殊,一般情况下取用平行或垂直于 \(y\) 轴的直线。

应用

给一道例题来理解。

题目大意

在笛卡尔坐标系内,有 \(n\) 个矩形,求这 \(n\) 个矩形共同覆盖的面积。

输入的第一行一个正整数 \(n\)

接下来 \(n\) 行每行四个非负整数 \(x_1, y_1, x_2, y_2\) ,表示一个矩形的左下角坐标为 \((x_1, y_1)\) ,右上角坐标为 \((x_2, y_2)\)

输出答案即可。

思路

First

题目的意思用图像来表示:
扫描线及其应用
那么这 \(5\) 个矩形的面积并就为:
扫描线及其应用
这些图形都是很不规则的,但是把他们细分成以下几个部分。
扫描线及其应用
这样划分后,答案就为每一个部分的低和高来求了。注意,高可能是断断续续的,如上图的蓝色部分,虽然是两个矩阵,但是底相同。

那么就可以使用扫描线从左到右扫描,计算每个部分的面积,每个部分的面积就为底乘高

Second

大思路有了,现在来探讨怎么实现。

不难发现,扫描线扫描到了矩形的边的时候,长度发生改变。在具体一点,当扫描线扫描到矩形的左边的边时,长度可能增加,扫描到右边的边时,长度可能缩短。

进一步按照扫描线的横坐标进行排序,则扫描线的宽就出来了。按照这样的扫描顺序,就应该求出高为多少。

这又涉及到区间覆盖的问题,扫描线究竟覆盖了多少长度?前面说过,扫描线其实并不是连续的,有可能是断断续续的,而线段树这种数据结构就成了首选。

线段树主要维护的是这个区间内的线段覆盖的情况,是一颗权值线段树,由于数据较大,可以先使用类似离散化的思想来优化空间。

一共有 \(2n\) 条线段,因为有 \(n\) 个矩形,而每个矩形都有两条边。每扫描到一条边的时候,这条线段对应的区间就改变。设 \(C\) 为该区间内有多少线段覆盖,\(Len\) 为线段的被覆盖总长度,不难推出线段树的子节点更新父节点的式子:

void Push_Up(int pos) {
	if(C(pos))//该区间被全覆盖
		LEN(pos) = R(pos) - L(pos);
	else
		LEN(pos) = LEN(LC(pos)) + LEN(RC(pos));
}

初始化建树:

void Make_Tree(int pos, int l, int r) {
	Tree[pos].Init_Tree(y[l], y[r]); 
	if(r - l <= 1)
		return;
	int mid = (l + r) >> 1;
	Make_Tree(LC(pos), l, mid);
	Make_Tree(RC(pos), mid, r);
}

若有区间被全覆盖,则 \(Len\) 就等于左右的距离,若没有全覆盖,就为两个儿子的距离总和。可能会觉得多此一举,但是若该区间已经被全覆盖了,就不用递归到子节点了,可以省去很多时间,将 \(O(n^2)\) 的算法降到 \(O(nlog(n))\) ,其实 \(C\) 就相当于延迟标记,故而时间复杂度就可以类比线段树的 “区间修改 区间查询” 问题。修改代码如下:

void Update_Tree(int pos, int l, int r, int k) {
//若该边为左边,k=1;若该边为右边,k=-1。因为C为该区间覆盖边的边数
	if(l <= L(pos) && R(pos) <= r) {
		C(pos) += k;
		Push_Up(pos);
		return;
	}
	if(l < R(LC(pos)))//与左儿子有交叉
		Update_Tree(LC(pos), l, r, k);
	if(r > L(RC(pos)))//与右儿子有交叉
		Update_Tree(RC(pos), l, r, k);
	Push_Up(pos);
}

既然不递归到子节点,那么程序会不会出错?注意,我们想要查找的是整个扫描线被覆盖的长度,即是根节点被覆盖的长度,不会去查询子节点,所以不用担心。

C++代码

#include <cstdio>
#include <algorithm>
using namespace std;
void Quick_Read(int &N) {
	N = 0;
	char c = getchar();
	int op = 1;
	while(c < '0' || c > '9') {
		if(c == '-')
			op = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9') {
		N = (N << 1) + (N << 3) + (c ^ 48);
		c = getchar();
	}
	N *= op;
}
const int MAXN = 2e5 + 5;
struct Segment_Tree {//权值线段树
	int Left_Section, Right_Section;
	int Cover_Section;
	long long Linear_Length;
	void Init_Tree(int l, int r) {
		Left_Section = l;
		Right_Section = r;
		Linear_Length = 0;
		Cover_Section = 0;
	}
	#define LC(x) (x << 1)
	#define RC(x) (x << 1 | 1)
	#define L(x) Tree[x].Left_Section
	#define R(x) Tree[x].Right_Section
	#define C(x) Tree[x].Cover_Section
	#define LEN(x) Tree[x].Linear_Length
};
struct Scan_Line {//扫描线
	int CoordX, CoordY_Up, CoordY_Down; 
	int AddSub;
	Scan_Line() {}
	Scan_Line(int X, int YD, int YU, int A) {
		CoordX = X;
		CoordY_Down = YD;
		CoordY_Up = YU;
		AddSub = A;
	}
	#define X(x) Line[x].CoordX
	#define YD(x) Line[x].CoordY_Down
	#define YU(x) Line[x].CoordY_Up
	#define A(x) Line[x].AddSub
};
Segment_Tree Tree[MAXN << 3];
Scan_Line Line[MAXN];
int y[MAXN];
int n, now;
bool cmp(Scan_Line x, Scan_Line y) {
	return x.CoordX < y.CoordX;
}
void Push_Up(int pos) {
	if(C(pos))
		LEN(pos) = R(pos) - L(pos);
	else
		LEN(pos) = LEN(LC(pos)) + LEN(RC(pos));
}
void Make_Tree(int pos, int l, int r) {//初始化线段树
	Tree[pos].Init_Tree(y[l], y[r]); 
	if(r - l <= 1)//r-l就结束,若l=r就结束,那么l~l这条线段就是一个点,没有修改的意义
		return;
	int mid = (l + r) >> 1;
	Make_Tree(LC(pos), l, mid);
	Make_Tree(RC(pos), mid, r);
//这里不能mid+1,因为线段l~mid加上mid+1~r不等价于l~r
}
void Update_Tree(int pos, int l, int r, int k) {
	if(l <= L(pos) && R(pos) <= r) {//全覆盖
		C(pos) += k;//覆盖的线段个数+1或-1
		Push_Up(pos);//C该边L有可能该边
		return;
	}
	if(l < R(LC(pos)))
		Update_Tree(LC(pos), l, r, k);
	if(r > L(RC(pos)))
		Update_Tree(RC(pos), l, r, k);
	Push_Up(pos);//子节点该边父节点也有可能改变
}
void Build() {
	sort(y + 1, y + 1 + (n << 1));//类似离散化的思想
	sort(Line + 1, Line + 1 + (n << 1), cmp);//对扫描线排序
	Make_Tree(1, 1, (n << 1));
}
void Scan() {
	unsigned long long res = 0;
	for(int i = 1; i <= n << 1; i++) {
		res += LEN(1) * (X(i) - X(i - 1));//“低×高”
		Update_Tree(1, YD(i), YU(i), A(i));//修改区间覆盖的长度
	}
	printf("%llu", res);
}
void Read() {
	Quick_Read(n);
	int X_1, X_2, Y_1, Y_2;
	for(int i = 1; i <= n; i++) {
		Quick_Read(X_1);
		Quick_Read(Y_1);
		Quick_Read(X_2);
		Quick_Read(Y_2);
		y[i] = Y_1;
		y[i + n] = Y_2;
		Line[i] = Scan_Line(X_1, Y_1, Y_2, 1);//左边
		Line[i + n] = Scan_Line(X_2, Y_1, Y_2, -1);//右边
	}
}
int main() {
	Read();
	Build();
	Scan();
	return 0;
}
发表评论

相关文章