線段樹之掃描線(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】感謝觀看!