前言
定義
在一個笛卡爾座標系內,用一根無限長線在此座標系內掃描,這根線就叫做掃描線,通俗易懂。
通常情況下,在座標系內確定一條線段需要兩個端點。但在特殊情況下,如該直線平行於 \(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;
}