您的位置:首页 > 其它

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

2015-03-05 02:57 197 查看
来源: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
為凸包的頂點數目。

 
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
 
// 小於。用以找出最低最左邊的點。
bool compare(Point& a, Point& b)
{
    return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
 
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
 
void findConvexHull()
{
    /* 用最低最左邊的點當作是起點。起點可以用凸包上面任意一個點。 */
 
    int s = 0;
    for (int i=0; i<100; ++i)
        if (compare(P[i], P[s]))
            s = i;
 
    /* 包禮物,逆時針方向。 */
 
    CH[0] = P[s];               // 紀錄起點
 
    for (int m=1; true; ++m)    // m 為凸包頂點數目
    {
        /* 開始窮舉所有點,找出位於最外圍的一點 */
 
        int next = s;
        if (m == 1) next = !s;  // 找第一點時,next預設為起點以外的點,
                                // 否則cross會一直等於零。
 
        for (int i=0; i<100; ++i)
            if (cross(CH[m], P[i], P[next]) < 0)
                next = i;
 
        if (next == s) break;   // 繞一圈後回到起點了
        CH[m] = P[next];        // 紀錄方才所找到的點
    }
}

Convex Hull: Graham's Scan

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





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






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

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


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


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

 
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100+1], CH[100+1];
 
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
 
// 小於。用以找出最低最左邊的點。
bool compare_position(Point& a, Point& b)
{
    return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
 
// 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
// 若角度一樣,則順序隨便。
bool compare_angle(Point& a, Point& b)
{
    return cross(P[0], a, b) > 0;
}
 
void findConvexHull()
{
    /* 用最低最左邊的點當作是起點。起點必須是凸包的端點。 */
    
    // 將端點換到第一點。O(N)
    swap(P[0], *min_element(P, P+100, compare_position));
 
    // 其餘各點照角度排序,並以第一點當中心點。O(NlogN)
    sort(P+1, P+100, compare_angle);
    
    /* 包,逆時針方向。O(N) */
    
    P[N] = P[0];
    
    int m = 0;      // m 為凸包頂點數目
    for (int i=0; i<=100; ++i) {
        while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) < 0) m--;
        CH[m++] = P[i];
    }
    
    m--;    // 最後一個點是重複出現兩次的起點,故要減一。
}

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


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

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

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)


 
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
 
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
 
// 小於。依座標大小排序,先排 x 再排 y。
bool compare(Point& a, Point& b)
{
    return (a.x < b.x) || (a.x == b.x && a.y < b.y);
}
 
void findConvexHull()
{
    // 將所有點依照座標大小排序
    sort(P, P+100, compare);
 
    int m = 0;  // m 為凸包頂點數目
 
    // 包下半部
    for (int i=0; i<100; ++i) {
        while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
        CH[m++] = P[i];
    }
 
    // 包上半部,不用再包入方才包過的終點,但會再包一次起點
    for (int i=100-2, t=m+1; i>=0; --i) {
        while (m >= t && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
        CH[m++] = P[i];
    }
}

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) 。是相當優美的演算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: