1. 程式人生 > >計算幾何(一):凸包問題(Convex Hull)

計算幾何(一):凸包問題(Convex Hull)

### 引言 首先介紹下什麼是凸包?如下圖: ![](https://gitee.com//riotian/blogimage/raw/master/img/20200921200555.png) 在一個二維座標系中,有若干點雜亂排列著,將最外層的點連線起來構成的凸多邊型,它能包含給定的所有的點,這個多邊形就是凸包。 > 實際上可以理解為用一個橡皮筋包含住所有給定點的形態。 > > 凸包用最小的周長圍住了給定的所有點。如果一個凹多邊形圍住了所有的點,它的周長一定不是最小,如下圖。根據三角不等式,凸多邊形在周長上一定是最優的。 > > ![](https://gitee.com//riotian/blogimage/raw/master/img/20200922190515.png) ### 凸包的求法 尋找凸包的演算法有很多種,常用的求法有 Graham 掃描法和 Andrew 演算法 #### Graham Scan 演算法求凸包 Graham Scan 演算法是一種十分簡單高效的二維凸包演算法,能夠在 $O(nlogn)$ 的時間內找到凸包。 Graham Scan 演算法的做法是先確定一個起點(一般是最左邊的點和最右邊的點),然後一個個點掃過去,如果新加入的點和之前已經找到的點所構成的 "殼" 凸性沒有變化,就繼續掃,否則就把已經找到的最後一個點刪去,再比較凸性,直到凸性不發生變化。分別掃描上下兩個 "殼",合併在一起,凸包就找到了。這麼說很抽象,我們看圖來解釋: ![](https://gitee.com//riotian/blogimage/raw/master/img/20200921200559.png) 先找 "下殼",上下其實是一樣的。首先加入兩個點 A 和 B。 ![](https://resource.ethsonliu.com/image/20180327_03.png) 然後插入第三個點 C,並計算 $\overrightarrow{AB}×\overrightarrow{BC}$ 的向量積,卻發現[向量積係數](https://www.cnblogs.com/RioTian/p/13708238.html)小於(等於)0,也就是說 $\overrightarrow{BC}$ 在 $\overrightarrow{AB}$ 的順時針方向上。 ![](https://gitee.com//riotian/blogimage/raw/master/img/20200921200608.png) 於是刪去 B 點。 ![](https://gitee.com//riotian/blogimage/raw/master/img/20200921200619.png) 按照這樣的方法依次掃描,找完 "下殼" 後,再找 "上殼"。 關於掃描的順序,有座標序和極角序兩種,本文采用前者。座標序是比較兩個點的 x 座標,小的先被掃描(掃描上凸殼的時候反過來),如果兩個點 x 座標相同,那麼就比較 y 座標,同樣的也是小的先被掃描(掃描上凸殼的時候也是反過來)。極角序使用 `atan2` 函式的返回值進行比較,讀者可以自己嘗試寫下。 下面貼下程式碼:Graham Scan 演算法 ```c++ struct Point { double x, y; Point operator-(Point & p) { Point t; t.x = x - p.x; t.y = y - p.y; return t; } double cross(Point p) // 向量叉積 { return x * p.y - p.x * y; } }; bool cmp(Point & p1, Point & p2) { if (p1.x != p2.x) return p1.x < p2.x; return p1.y < p2.y; } Point point[1005]; // 無序點 int convex[1005]; // 儲存組成凸包的點的下標 int n; // 座標系的無序點的個數 int GetConvexHull() { sort(point, point + n, cmp); int temp; int total = 0; for (int i = 0; i < n; i++) // 下凸包 { while (total >
1 && (point[convex[total - 1]] - point[convex[total - 2]]).cross(point[i] - point[convex[total - 1]]) <= 0) total--; convex[total++] = i; } temp = total; for (int i = n - 2; i >= 0; i--) // 上凸包 { while (total > temp && (point[convex[total - 1]] - point[convex[total - 2]]).cross(point[i] - point[convex[total - 1]]) <= 0) total--; convex[total++] = i; } return total - 1; // 返回組成凸包的點的個數,實際上多了一個,就是起點,所以組成凸包的點個數是 total - 1 } ``` #### Andrew 演算法求凸包 首先把所有點以橫座標為第一關鍵字,縱座標為第二關鍵字排序。 顯然排序後最小的元素和最大的元素一定在凸包上。而且因為是凸多邊形,我們如果從一個點出發逆時針走,軌跡總是“左拐”的,一旦出現右拐,就說明這一段不在凸包上。因此我們可以用一個單調棧來維護上下凸殼。 因為從左向右看,上下凸殼所旋轉的方向不同,為了讓單調棧起作用,我們首先 **升序列舉** 求出下凸殼,然後 **降序** 求出上凸殼。 求凸殼時,一旦發現即將進棧的點( $P$ )和棧頂的兩個點( $S_1,S_2$ ,其中 $S_1$ 為棧頂)行進的方向向右旋轉,即叉積小於 $0$ : $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}<0$ ,則彈出棧頂,回到上一步,繼續檢測,直到 $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}\ge 0$ 或者棧內僅剩一個元素為止。 通常情況下不需要保留位於凸包邊上的點,因此上面一段中 $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}<0$ 這個條件中的“ $<$ ”可以視情況改為 $\le$ ,同時後面一個條件應改為 $>$ 。 ##### 程式碼實現 ```cpp // stk[]是整型,存的是下標 // p[]儲存向量或點 tp = 0; //初始化棧 std::sort(p + 1, p + 1 + n); //對點進行排序 stk[++tp] = 1; //棧內新增第一個元素,且不更新used,使得1在最後封閉凸包時也對單調棧更新 for (int i = 2; i <= n; ++i) { while (tp >= 2 //下一行*被過載為叉積 && (p[stk[tp]] - p[stk[tp - 1]]) * (p[i] - p[stk[tp]]) <= 0) used[stk[tp--]] = 0; used[i] = 1; // used表示在凸殼上 stk[++tp] = i; } int tmp = tp; // tmp表示下凸殼大小 for (int i = n - 1; i >
0; --i) if (!used[i]) { // ↓求上凸殼時不影響下凸殼 while (tp > tmp && (p[stk[tp]] - p[stk[tp - 1]]) * (p[i] - p[stk[tp]]) <= 0) used[stk[tp--]] = 0; used[i] = 1; stk[++tp] = i; } for (int i = 1; i <= tp; ++i) //複製到新陣列中去 h[i] = p[stk[i]]; int ans = tp - 1; ``` 根據上面的程式碼,最後凸包上有 $ans$ 個元素(額外儲存了 $1$ 號點,因此 $h$ 陣列中有 $ans+1$ 個元素),並且按逆時針方向排序。周長就是 $$ \sum_{i=1}^{ans}\left|\overrightarrow{h_ih_{i+1}}\right| $$ ## 參考 - [Graham Scan 凸包演算法](https://segmentfault.com/a/1190000000488339) - [Andrew 凸包演算法](https://oi-wiki.org/geometry/convex-hull/)