凸包定義
通俗的話來解釋凸包:給定二維平面上的點集,凸包就是將最外層的點連接起來構成的凸多邊型,它能包含點集中所有的點

Graham掃描法
-
由最底的一點 \(p_1\) 開始(如果有多個這樣的點,那么選擇最左邊的),計算它跟其他各點的連線和 x 軸正向的角度,按小至大將這些點排序,稱它們的對應點為 \(p_{2},p_{3},...,p_{n}\)。這里的時間復雜度可達 \(O(n \log {n})\)
-
以下圖為例,基點為H,根據夾角由小至大排序后依次為H,K,C,D,L,F,G,E,I,B,A,J。下面進行逆時針掃描。

3. 線段<H, K>一定在凸包上,接着加入C。假設線段<K, C>也在凸包上,因為就H,K,C三點而言,它們的凸包就是由此三點所組成。但是接下來加入D時會發現,線段<K, D>才會在凸包上,所以將線段<K, C>排除,C點不在是凸包上。
4. 即當加入一點時,必須考慮到前面的線段是否會出現在凸包上。從基點開始,凸包上每條相臨的線段的旋轉方向應該一致。如果發現新加的點使得新線段與上線段的旋轉方向發生變化,則可判定上一點必然不在凸包上。實現時可用向量叉積進行判斷,設新加入的點為 \(p_{n + 1}\),上一點為 \(p_n\),再上一點為 \(p_{n - 1}\)。順時針掃描時,如果向量 \(<p_{n - 1}, p_n>\) 與 \(<p_n, p_{n + 1}>\) 的叉積為正,則將上一點刪除。
5. \(\color{red}{刪除過程需要回溯,將之前所有叉積符號相反的點都刪除,然后將新點加入凸包。}\)

Graham掃描法主要用一個\(\color{red}{棧}\)來解決凸包問題,點集 Q 中每個點都會進棧一次,不符合條件的點會被彈出,算法終止時,棧中的點就是凸包的頂點(逆時針順序在邊界上)。該算法具體步驟為

由於選擇第一個基點時, 選擇的是 y 坐標最小且最靠左的點, 所以極角取值范圍 [0, 180), 我們關心的是極角的相對大小, 而不用求角度具體大小(雖然可以通過 \(\cos(\theta)\) 在 [0, 180)遞減性質 來求實際角度大小), 所以極角排序可以通過向量叉積來做. 如果 \(p_1 \times p_2\) 向量叉積為正, 則 \(p_2\) 在 \(p_1\) 逆時針放心, 那么 \(p_2\) 與 x 正方向夾角比 \(p_1\) 大
旋轉方向
叉積
有向量 \(p1\) 和 \(p2\), 我們可以把叉積理解為由點 \((0,0)\), \(\vec p_1\), \(\vec p_2\) 和 \(\vec p_1+ \vec p_2\) 所構成的平行四邊形有向面積.

二維平面點叉乘行列式:
相對坐標原點,若 \(\vec p_1 \times \vec p_2\)值為正,\(\vec p_2\) 在 \(\vec p_1\) 逆時針方向,若值為負,\(\vec p_2\) 在 \(\vec p_1\) 順時針方向。相對公共端點 \(\vec p_0\),叉積計算為
一個簡單的確定滿足 “右手定則” 向量叉積的方向的方法是這樣的:若坐標系是滿足右手定則的,當右手的四指從 \(\vec a\) 以不超過 180 度的轉角轉向 \(\vec b\) 時,豎起的大拇指指向是 \(\vec c\) 的方向.

代碼實現

#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
#include <stack>
using namespace std;
template <typename T>
struct Point {
T x;
T y;
Point(): x(0), y(0){};
Point(T x_, T y_): x(x_), y(y_){};
};
bool isLeftTurn(Point<int> &p0, Point<int> &p1, Point<int> &p2) {
return (p1.x - p0.x) * (p2.y - p0.y) >= (p1.y - p0.y) * (p2.x - p0.x);
}
int main() {
vector<Point<int>> points;
points.emplace_back(1, 1);
points.emplace_back(8, 0);
points.emplace_back(16, 8);
points.emplace_back(2, 8);
points.emplace_back(4, 4);
points.emplace_back(0, 5);
points.emplace_back(12, 6);
points.emplace_back(8, 4);
points.emplace_back(6, 2);
// output: (8,0) (16,8) (2,8) (0,5) (1,1)
if (points.empty()) {
return 0;
}
int y_min = INT_MAX;
int x_min = INT_MAX;
int start_index = 0;
for (int i = 0; i < points.size(); i++) {
if (points[i].y < y_min) {
y_min = points[i].y;
x_min = points[i].x;
start_index = i;
} else if (points[i].y == y_min) {
x_min = points[i].x;
start_index = i;
}
}
vector<Point<int>> st;
st.emplace_back(0, 0);
points.erase(points.begin()+start_index);
for (auto &point : points) {
point.x -= x_min;
point.y -= y_min;
}
sort(points.begin(), points.end(), [](Point<int>& p1, Point<int>& p2) {
if (p1.x * p2.y == p1.y * p2.x) {
return p1.x * p1.x + p1.y * p1.y < p2.x * p2.x + p2.y * p2.y;
}
return p1.x * p2.y > p1.y * p2.x;
});
st.push_back(points[0]);
for (int i = 1; i < points.size();) {
if (isLeftTurn(st[st.size() - 2], st.back(), points[i])) {
st.push_back(points[i]);
i++;
} else {
st.pop_back();
}
}
for (auto &i : st) {
cout << i.x + x_min << '\t' << i.y + y_min << endl;
}
return 0;
}
參考: https://blog.csdn.net/u012328159/article/details/50808360