Part -999 感謝列表
(排名不分先后)
- 計算幾何「OI-Wiki」
- 數論小白都能看懂的平面凸包詳解 「ShineEternal的博客」
- 幾何畫圖「GeoGebra」 離線版
- 感謝@rui_er 指出了一個錯誤
Part 1 前言
首先說明一下,本人是剛學 \(\mathsf{OI}\) 的萌新,本學習筆記如有錯誤,並非有意,但仍然歡迎在討論去狂 \(\sf D\) 她。
關於圖片:本文所有圖片均為作者純手畫。
祝讀者有良好的閱讀體驗~
Part 2 何為計算幾何
學二維凸包,我們首先需要了解的就是計算幾何。
計算幾何,就是利用計算機建立數學模型解決幾何問題。
要用電腦解幾何題?數學好的同學們笑了。
我們並不是用計算機算數學卷子上的幾何題去了,而是解決一些更加復雜的幾何相關問題。
為了解決復雜且抽象的問題,我們一定要選擇合適的研究方法。對於計算機來說,給它看幾何圖形……
Part 3 二維凸包
Part 3.1 凸多邊形
凸多邊形是指所有內角大小都在 \([0, \pi]\) 范圍內的 簡單多邊形 。
Part 3.2 凸包
「
在平面上能包含所有給定點的最小凸多邊形叫做凸包。
其定義為:對於給定集合 \(X\) ,所有包含 \(X\) 的凸集的交集 \(S\) 被稱為 \(X\) 的 凸包 。
\(\qquad\qquad\) —— OI-Wiki
」
其實我們可以把凸包看成一個拿橡皮筋圍成的一個圖形。
假設有一個布滿小凸起的板子:
我們要把這些凸起都圍起來,怎么圍呢?
顯然,最簡單方便的方法是這樣:
但是,我們知道,橡皮筋是有彈力的,所以橡皮筋會往里縮,直到這樣:
最外圈的凸起撐起了橡皮筋。
此時橡皮筋圍成的多邊形的頂點就是最外圈凸起所在的位置。
由此,我們就定義橡皮筋圍成的圖形為一個平面凸包。
那么,換一種定義,就為:
平面凸包是指覆蓋平面上 \(n\) 個點的最小的凸多邊形。
當然,我們發現在程序中卻無法模擬橡皮筋收縮的過程,於是有了下文的誕生。
Part 3.3 二維凸包的求法
在這里我們只講兩種主要的也是最常用的二維凸包的求法。
Part 3.3.1 Graham 算法
Graham 算法的本質:
Graham 掃描算法維護一個凸殼,通過不斷在凸殼中加入新的點和去除影響凸性的點,最后形成凸包。
凸殼:凸包的一部分。
此算法主要分為兩部分:
- 排序
- 掃描
Part 3.3.1.1 排序
我們先選擇一個 \(y\) 最小的點(如 \(y\) 相同選 \(x\) 最小),記為 \(p_1\)。
剩下的點,按照極角的大小逆時針排序,記為 \(p_2,p_3,\dots, p_m\)。
Part 3.3.1.2 掃描
(下列所說的左右等是指以上一條連線為鉛垂線,新的連線偏移的方向)
剛開始,我們的點集是這樣的:
\(p_1\) 為起始點。
隨后,\(p_2\) 准備入棧,由於棧元素很少,所以可以入棧。
再看 \(p_3\),因為 \(p_3\) 向左,符合凸包條件,入棧。
隨后 \(p_4\) 也一切正常,依然向左,入棧。
\(p_5\) 依然向左,入棧。
到 \(p_6\) 時,我們發現了點問題,就是不再是向左了,而是向右了,所以我們此時要將 \(p_5\) 出棧,\(p_6\) 入棧。
入棧后,我們發現,相對於 \(p_4\),\(p_6\) 依然是向右的,所以我們還要把 \(p_4\) 出棧,\(p_6\) 入棧。
接下來 \(p_7\) 沒有問題。
\(p_8\) 時,我們發現,也是向右的,所以將 \(p_7\) 出棧,\(p_8\) 入棧。
接下來 \(p_9\) 正常,入棧。
最后,我們再把最后一個點和第一個點連起來。
此時,我們的 Graham 算法的全過程就結束了。
時間復雜度為 \(O(n \log n)\)。
Part 3.3.2 Andrew 算法
Graham 算法的一種進階。
假設我們有這些點:
首先把所有點以橫坐標為第一關鍵字,縱坐標為第二關鍵字排序。
相對於 Graham 算法來說,Andrew 算法排序更簡單,按 \(x, y\) 坐標排序,時間復雜度也更低(一般的坐標系中排序方法)。
首先將 \(p_1\) 入棧。
然后也將 \(p_2\) 入棧,\(p_2\) 可能在,也可能不在,等着之后判斷。
隨后,發現 \(p_3\) 偏右,所以我們將 \(p_2\) 出棧。
發現 \(p_4\) 依然偏右,\(p_3\) 出棧,\(p_4\) 入棧。
\(p_5\) 向右,\(p_4\) 出棧,\(p_5\) 入棧。
\(p_6\) 向左,入棧。
\(p_7\) 向右,\(p_6\) 出棧,\(p_7\) 入棧。
\(p_8\) 向右,\(p_7\) 出棧,繼續檢查發現相對於 \(p_5\) \(p_8\) 仍然向右,\(p_5\) 出棧,\(p_8\) 入棧。
此時,我們發現,凸包空了一半。
所以我們需要再從排序末尾的點(也就是 \(p_8\))出發,按照一模一樣的方式再算一遍就行了。
當然如果我們走過的點就不許要再走了(除了 \(p_1\)).
從 \(p_8\) 到 \(p_7\),向左,\(p_7\) 入棧。
\(p_6\) 向右,\(p_7\) 出棧,\(p_6\) 入棧。
\(p_5\) 向左,入棧。
\(p_4\) 向左,入棧。
\(p_3\) 向右,\(p_4\) 出棧,對於 \(p_5\) \(p_3\) 依然向右,\(p_5\) 出棧,\(p_3\) 入棧。
\(p_2\) 向右,\(p_3\) 出棧,\(p_2\) 入棧。
最后將 \(p_2\) 和 \(p_1\) 連起來。
至此,我們的 Andrew 算法就完成了!
時間復雜度:\(O(n \log n)\)
Part 3.4 實戰演練
Part 3.4.1 P2742 [USACO5.1]圈奶牛Fencing the Cows /【模板】二維凸包
先拿模板題練練手。
題目簡述:求一個二維凸包的周長。
拿 Graham 算法做即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#define line cout << endl
using namespace std;
const int NR = 1e5 + 5;
int n;
double ans;
struct point {
double x, y;
};
point p[NR], ps[NR];
double dis (point pa, point pb) { //求兩點間距離
return sqrt ((pb.x - pa.x) * (pb.x - pa.x) + (pb.y - pa.y) * (pb.y - pa.y));
}
double cp (point pa1, point pa2, point pb1, point pb2) { //求叉積
return (pa2.x - pa1.x) * (pb2.y - pb1.y) - (pb2.x - pb1.x) * (pa2.y - pa1.y);
}
bool cmp (point px, point py) { //排序
double num = cp (p[1], px, p[1], py);
if (num > 0) return true;
if (num == 0 && dis (p[0], px) < dis (p[0], py)) return true;
return false;
}
int main () {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i].x >> p[i].y;
if(i != 1 && p[i].y < p[1].y) { //去重
swap (p[i].y, p[1].y);
swap (p[i].x, p[1].x);
}
}
sort (p + 2, p + n + 1, cmp);
ps[1] = p[1]; //最低點是肯定在凸包里的
int h = 1;
for (int i = 2; i <= n; i++) {
while (h > 1 && cp (ps[h - 1], ps[h], ps[h], p[i]) <= 0) { //判斷是向左還是向右,如果向右就出棧
h--;
}
h++;
ps[h] = p[i];
}
ps[h + 1] = p[1]; //最后一個點跟第一個點相連
for (int i = 1; i <= h; i++) {
ans += dis (ps[i], ps[i + 1]); //累加
}
printf ("%.2lf\n", ans);
return 0;
}
Part 3.4.2 UVA11626 Convex Hull
這題好像拿 Graham 會 TLE?拿 Andrew罷,也是道模板題。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#define line cout << endl
using namespace std;
const int NR = 1e5 + 5;
const double eps = 1e-7;
int n;
struct point {
double x, y;
point () {}
point (double a, double b) : x (a), y (b) {}
bool operator < (const point &b) const {
if (x < b.x) return 1;
if (x > b.x) return 0;
return y < b.y;
}
point operator - (const point &b) {
return point (x - b.x, y - b.y);
}
};
point p[NR], sp[NR];
int cmp (double x) {
if (fabs (x) < eps) return 0;
return x > 0 ? 1 : -1;
}
double dis (point a, point b) {
return sqrt ((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
double cp (point a, point b) {
return a.x * b.y - a.y * b.x;
}
int Andrew () {
sort (p + 1, p + 1 + n);
int len = 0;
for (int i = 1; i <= n; i++) {
while (len > 1 && cmp (cp (sp[len] - sp[len - 1], p[i] - sp[len - 1])) < 0)
len--;
sp[++len] = p[i];
}
int k = len;
for (int i = n - 1; i >= 1; i--) {
while (len > k && cmp (cp (sp[len] - sp[len - 1], p[i] - sp[len - 1])) < 0)
len--;
sp[++len] = p[i];
}
return len;
}
int main () {
int t;
cin >> t;
while (t--) {
cin >> n;
char c;
for (int i = 1; i <= n; i++)
cin >> p[i].x >> p[i].y >> c;
int t = Andrew();
cout << t - 1 << endl;
for (int i = 1; i < t; i++)
printf ("%.0lf %.0lf\n", sp[i].x, sp[i].y);
}
return 0;
}
The End...