掃描線及其應用

Emissaryofnight發表於2020-12-15

前言

本文例題連結

定義

在一個笛卡爾座標系內,用一根無限長線在此座標系內掃描,這根線就叫做掃描線,通俗易懂。

通常情況下,在座標系內確定一條線段需要兩個端點。但在特殊情況下,如該直線平行於 \(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;
}

相關文章