转载:凸壳算法集及描述(繁体中文)

来源:http://acm.nudt.edu.cn/~twcourse/ConvexHull.html#a11


中文譯做「凸包」,能包住物品的最小的凸外殼,也就是能將全部東西包進去的最小凸多邊形。凸的定義是圖形內任兩點的連線不會經過圖形外部:http://mathworld.wolfram.com/Convex.html。這裡我們只討論:從二維平面上散佈的點當中找出凸包。

在所有點的外圍繞一圈可得一凸多邊形,即是凸包。

凸包所包住的區域,為各點之間做線性內插後的範圍。

UVa 109 132 218 361 681 811 819 10002 10065 10078 10135 10173 10256 11626

Convex Hull: Jarvis' March

Jarvis' March Gift Wrapping Algorithm

從一個凸包上的頂點開始,順著外圍繞一圈,順時針或逆時針都可以。

當要尋找下一個被包覆的點時,則窮舉平面上所有點,找出位於最外圍的一點來包覆即可(可利用外積運算來做判斷)。時間複雜度為 O(N*M) M 為凸包的頂點數目。

 
  1. // P為平面上的那些點。這裡設定為剛好100點。
  2. // CH為凸包上的點。這裡設定為照逆時針順序排列。
  3. struct Point {int xy;} P[100], CH[100];
  4.  
  5. // 小於。用以找出最低最左邊的點。
  6. bool compare(PointaPointb)
  7. {
  8.     return (a.y < b.y) || (a.y == b.y && a.x < b.x);
  9. }
  10.  
  11. // 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
  12. double cross(PointoPointaPointb)
  13. {
  14.     return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  15. }
  16.  
  17. void findConvexHull()
  18. {
  19.     /* 用最低最左邊的點當作是起點。起點可以用凸包上面任意一個點。 */
  20.  
  21.     int s = 0;
  22.     for (int i=0i<100; ++i)
  23.         if (compare(P[i], P[s]))
  24.             s = i;
  25.  
  26.     /* 包禮物,逆時針方向。 */
  27.  
  28.     CH[0] = P[s];               // 紀錄起點
  29.  
  30.     for (int m=1true; ++m)    // m 為凸包頂點數目
  31.     {
  32.         /* 開始窮舉所有點,找出位於最外圍的一點 */
  33.  
  34.         int next = s;
  35.         if (m == 1next = !s;  // 找第一點時,next預設為起點以外的點,
  36.                                 // 否則cross會一直等於零。
  37.  
  38.         for (int i=0i<100; ++i)
  39.             if (cross(CH[m], P[i], P[next]) < 0)
  40.                 next = i;
  41.  
  42.         if (next == sbreak;   // 繞一圈後回到起點了
  43.         CH[m] = P[next];        // 紀錄方才所找到的點
  44.     }
  45. }

Convex Hull: Graham's Scan

Graham's Scan

由前面段落可知:凸包上的頂點們有順序的沿著外圍繞行一圈。若能照此順序來包,就不必以窮舉所有點的方式來尋找最外圍的點。 Graham's Scan即是嘗試將所有點按照順序排好,再來做繞一圈的動作。

順序該如何決定呢?只要能確保凸包各頂點的前後順序是正確的,那麼便不會包錯。一個簡單的想法是依角度排序──只要將中心點設定在凸包內部或設定在凸包上面,便可以確保凸包各頂點的前後順序必定正確(讀者可自行証明此說)。

除了凸包各頂點的前後順序要正確,另外還要限制所有點依照前後順序連線起來後,不會繞成超過一個的圈圈,也不會有任何邊重疊。更精準的說法是:會形成簡單多邊形( simple polygon),不會有邊相交。如此一來,便不必理會那些不在凸包上面的點的前後順序,因為那些點會在找最外圍的點的時候被淘汰掉(讀者可自行証明此說)。

一般來說,選擇凸包上面的端點當作排序角度時的中心點是比較好的,因為最大的夾角必會小於 180 度,而可以使用外積運算來排序。(外積在大於 180度時會得負值、等於 180 度時會等於零,導致排序錯誤。)

如果凸包各頂點的前後順序是錯誤的,或者所有點依照前後順序連線後產生了很多圈圈,就會發生慘劇。有時甚至會找出凹的形狀。

其他細節在演算法書籍上面皆可找到,故不細講。時間複雜度為 O(NlogN) ,主要取決於排序的時間;若用 Counting Sort之類的排序方法便可達到 O(N) ;若已知這些點構成的簡單多邊形之後,便不需排序,就只需 O(N)

 
  1. // P為平面上的那些點。這裡設定為剛好100點。
  2. // CH為凸包上的點。這裡設定為照逆時針順序排列。
  3. struct Point {int xy;} P[100+1], CH[100+1];
  4.  
  5. // 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
  6. double cross(PointoPointaPointb)
  7. {
  8.     return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  9. }
  10.  
  11. // 小於。用以找出最低最左邊的點。
  12. bool compare_position(PointaPointb)
  13. {
  14.     return (a.y < b.y) || (a.y == b.y && a.x < b.x);
  15. }
  16.  
  17. // 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
  18. // 若角度一樣,則順序隨便。
  19. bool compare_angle(PointaPointb)
  20. {
  21.     return cross(P[0], ab) > 0;
  22. }
  23.  
  24. void findConvexHull()
  25. {
  26.     /* 用最低最左邊的點當作是起點。起點必須是凸包的端點。 */
  27.     
  28.     // 將端點換到第一點。O(N)
  29.     swap(P[0], *min_element(PP+100compare_position));
  30.  
  31.     // 其餘各點照角度排序,並以第一點當中心點。O(NlogN)
  32.     sort(P+1P+100compare_angle);
  33.     
  34.     /* 包,逆時針方向。O(N) */
  35.     
  36.     P[N] = P[0];
  37.     
  38.     int m = 0;      // m 為凸包頂點數目
  39.     for (int i=0i<=100; ++i) {
  40.         while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) < 0m--;
  41.         CH[m++] = P[i];
  42.     }
  43.     
  44.     m--;    // 最後一個點是重複出現兩次的起點,故要減一。
  45. }

若要連凸包上面共線的點都找出來,便要小心處理剛開始包、快要包好時產生共線的情形,這些點的先後順序決不能亂。

有一個解決方法是分做左右兩邊包,當排序時遇到角度相同的情況時,令距離離中心點較短的順序較高。總之相當麻煩,就不細講了。下面這段程式碼寫出一些特別要注意的地方:

 
  1. // 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
  2. // 若角度一樣,則距離較離中心點較短的順序較高。
  3. bool compare_angle(PointaPointb)
  4. {
  5.     // 加入角度相同時,距離長度的判斷。
  6.     int c = cross(P[0], ab);
  7.     return (c > 0) || (c == 0 && len2(P[0], a) < len2(P[0], b));
  8. }
  9.  
  10. void findConvexHull()
  11. {
  12.     ......
  13.     
  14.         // 這邊的判斷記得要改成小於等於零,以包含共線情形。
  15.         while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0m--;
  16.         
  17.     ......
  18. }

Convex Hull: Andrew's Monotone Chain

Andrew's Monotone Chain

排順序時改為依座標大小排序。這個方法非常優美,而且能處理共線的情形: http://www.algorithmist.com/index.php/Monotone_Chain_Convex_Hull 。我也找到了有趣的 Applet http://wind.lcs.mit.edu/~aklmiu/6.838/convexhull/index.html

時間複雜度為下述兩項總和:一、一次排序,通常為 O(NlogN) ;二、掃描 2N個點,為 O(N)

 
  1. // P為平面上的那些點。這裡設定為剛好100點。
  2. // CH為凸包上的點。這裡設定為照逆時針順序排列。
  3. struct Point {int xy;} P[100], CH[100];
  4.  
  5. // 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
  6. double cross(PointoPointaPointb)
  7. {
  8.     return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  9. }
  10.  
  11. // 小於。依座標大小排序,先排 x 再排 y。
  12. bool compare(PointaPointb)
  13. {
  14.     return (a.x < b.x) || (a.x == b.x && a.y < b.y);
  15. }
  16.  
  17. void findConvexHull()
  18. {
  19.     // 將所有點依照座標大小排序
  20.     sort(PP+100compare);
  21.  
  22.     int m = 0;  // m 為凸包頂點數目
  23.  
  24.     // 包下半部
  25.     for (int i=0i<100; ++i) {
  26.         while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0m--;
  27.         CH[m++] = P[i];
  28.     }
  29.  
  30.     // 包上半部,不用再包入方才包過的終點,但會再包一次起點
  31.     for (int i=100-2t=m+1i>=0; --i) {
  32.         while (m >= t && cross(CH[m-2], CH[m-1], P[i]) <= 0m--;
  33.         CH[m++] = P[i];
  34.     }
  35. }

Convex Hull: Quick Hull Algorithm

演算法

這是一個運用 Divide and Conquer 的演算法。

一開始將所有點以X座標位置排序。
Divide:將所有點分成左半部和右半部。
Conquer:左半部和右半部分別求凸包。
Merge:將兩個凸包合併成一個凸包。
在兩凸包頂端最凸處加一條邊,
然後在兩凸包底部最凸處加一條邊,
就變成一個凸包。

令左半部凸包最左端的點為p點,令右半部凸包最右端的點為q點。
要找上方的邊,
讓p點為基準,然後移動q點在凸包上往逆時針方向走,
讓直線pq持續往逆時針方向轉,轉到底為止。
接著讓q點為基準,然後移動q點在凸包上往逆時針方向走,
讓直線pq持續往逆時針方向轉,轉到底為止。
此時的邊pq就是上方的邊。
要找下方的邊,也可以如法炮製。

另外一種比較麻煩一點的找法是,
令左半部凸包最高的點為p點,令右半部凸包最低的點為q點。
讓p點為基準,然後移動q點在凸包上往逆時針、也往順時針方向走,
總之就是讓直線pq持續往逆時針方向轉。
接著讓q點為基準做類似的事情。
要找下方的邊,也可以如法炮製。

時間複雜度為下述兩項總和:一、一次排序的時間,通常為 O(NlogN) ;二、 Divide and Conquer向下遞迴 O(logN) 深度,合併凸包要 O(N) 時間,總共需時 O(NlogN)

Convex Hull: Melkman's Algorithm

演算法

求出一簡單多邊形的凸包。

http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html

時間複雜度為 O(N) 。是相當優美的演算法。

你可能感兴趣的:(科学计算,算法,计算几何)