凸包算法入門


前言:

首先,什么是凸包? 
假設平面上有p0~p12共13個點,過某些點作一個多邊形,使這個多邊形能把所有點都“包”起來。當這個多邊形是凸多邊形的時候,我們就叫它“凸包”。如下圖: 
這里寫圖片描述

然后,什么是凸包問題? 
我們把這些點放在二維坐標系里面,那么每個點都能用 (x,y) 來表示。 
現給出點的數目13,和各個點的坐標。求構成凸包的點?

 

 

 

解一:窮舉法(蠻力法)

時間復雜度:O(n³)。 
思路:兩點確定一條直線,如果剩余的其它點都在這條直線的同一側,則這兩個點是凸包上的點,否則就不是。 
步驟:

  1. 將點集里面的所有點兩兩配對,組成 n(n-1)/2 條直線。
  2. 對於每條直線,再檢查剩余的 (n-2) 個點是否在直線的同一側。

如何判斷一個點 p3 是在直線 p1p2 的左邊還是右邊呢?(坐標:p1(x1,y1),p2(x2,y2),p3(x3,y3))

這里寫圖片描述 
當上式結果為正時,p3在直線 p1p2 的左側;當結果為負時,p3在直線 p1p2 的右邊。

 

 

 

解二:分治法

時間復雜度:O(n㏒n)。 
思路:應用分治法思想,把一個大問題分成幾個結構相同的子問題,把子問題再分成幾個更小的子問題……。然后我們就能用遞歸的方法,分別求這些子問題的解。最后把每個子問題的解“組裝”成原來大問題的解。 
步驟:

  1. 把所有的點都放在二維坐標系里面。那么橫坐標最小和最大的兩個點 P1 和 Pn 一定是凸包上的點(為什么呢?用反證法很容易證明,這里不詳講)。直線 P1Pn 把點集分成了兩部分,即 X 軸上面和下面兩部分,分別叫做上包和下包。
  2. 對上包:求距離直線 P1Pn 最遠的點,即下圖中的點 Pmax 。
  3. 作直線 P1Pmax 、PnPmax,把直線 P1Pmax 左側的點當成是上包,把直線 PnPmax 右側的點也當成是上包。
  4. 重復步驟 2、3。
  5. 對下包也作類似操作。

這里寫圖片描述

 


然而怎么求距離某直線最遠的點呢?我們還是用到解一中的公式: 
這里寫圖片描述 
設有一個點 P3 和直線 P1P2 。(坐標:p1(x1,y1),p2(x2,y2),p3(x3,y3)) 
對上式的結果取絕對值,絕對值越大,則距離直線越遠。

注意:在步驟一,如果橫坐標最小的點不止一個,那么這幾個點都是凸包上的點,此時上包和下包的划分就有點不同了,需要注意。

 

 

 

解三:Jarvis步進法

時間復雜度:O(nH)。(其中 n 是點的總個數,H 是凸包上的點的個數) 
思路:

  • 縱坐標最小的那個點一定是凸包上的點,例如圖上的 P0。
  • 從 P0 開始,按逆時針的方向,逐個找凸包上的點,每前進一步找到一個點,所以叫作步進法。
  • 怎么找下一個點呢?利用夾角。假設現在已經找到 {P0,P1,P2} 了,要找下一個點:剩下的點分別和 P2 組成向量,設這個向量與向量P1P2的夾角為 β 。當 β 最小時就是所要求的下一個點了,此處為 P3 。

這里寫圖片描述

注意:

  1. 找第二個點 P1 時,因為已經找到的只有 P0 一個點,所以向量只能和水平線作夾角 α,當 α 最小時求得第二個點。
  2. 共線情況:如果直線 P2P3 上還有一個點 P4,即三個點共線,此時由向量P2P3 和向量P2P4 產生的兩個 β 是相同的。我們應該把 P3、P4 都當做凸包上的點,並且把距離 P2 最遠的那個點(即圖中的P4)作為最后搜索到的點,繼續找它的下一個連接點。

 

 

 

解四:Graham掃描法

時間復雜度:O(n㏒n) 
思路:Graham掃描的思想和Jarris步進法類似,也是先找到凸包上的一個點,然后從那個點開始按逆時針方向逐個找凸包上的點,但它不是利用夾角。 
這里寫圖片描述 
步驟:

  1. 把所有點放在二維坐標系中,則縱坐標最小的點一定是凸包上的點,如圖中的P0。
  2. 把所有點的坐標平移一下,使 P0 作為原點,如上圖。
  3. 計算各個點相對於 P0 的幅角 α ,按從小到大的順序對各個點排序。當 α 相同時,距離 P0 比較近的排在前面。例如上圖得到的結果為 P1,P2,P3,P4,P5,P6,P7,P8。我們由幾何知識可以知道,結果中第一個點 P1 和最后一個點 P8 一定是凸包上的點。 
    (以上是准備步驟,以下開始求凸包) 
    以上,我們已經知道了凸包上的第一個點 P0 和第二個點 P1,我們把它們放在棧里面。現在從步驟3求得的那個結果里,把 P1 后面的那個點拿出來做當前點,即 P2 。接下來開始找第三個點:
  4. 連接 棧最上面的連個元素(即P0和棧頂)的那個點,得到直線 L 。看當前點是在直線 L 的右邊還是左邊。如果在直線的右邊就執行步驟5;如果在直線上,或者在直線的左邊就執行步驟6。
  5. 如果在右邊,則棧頂的那個元素不是凸包上的點,把棧頂元素出棧。執行步驟4。
  6. 當前點是凸包上的點,把它壓入棧,執行步驟7。
  7. 檢查當前的點 P2 是不是步驟3那個結果的最后一個元素。是最后一個元素的話就結束。如果不是的話就把 P2 后面那個點做當前點,返回步驟4。

最后,棧中的元素就是凸包上的點了。 
以下為用Graham掃描法動態求解的過程: 
這里寫圖片描述

 

 

 

解五:Melkman算法

這里寫圖片描述 
說真的,這個算法我也還沒有看清。網上的資料也少的可憐,我暫且把網上的解釋截個圖在這里,往后搞懂以后再回來補上。 
或者有人看懂了的,希望不吝指教,不甚感激! 

 

 

擴展:

以上討論的只是二維的凸包,如果延生為三維、多維的凸包問題呢?如何求解? 
不過首先,二維凸包可以用來解決圍欄問題、城市規划問題、聚類分析等等。但是三維、多維的凸包可能的使用范疇有哪些?

 

分治法實現代碼:

#include<stdio.h>
#include<stdlib.h>

int g_result[240][2];

/*getResult()實現功能:以坐標P0(x1,y1)和Pn(x2,y2)為直線,找出pack里面里這條直線最遠的點Pmax
*並找出直線P0Pmax和PmaxPn的上包,進行遞歸。
*注:Pack[0][0]存放點的個數,pack[1]開始存放點的坐標。
*全局變量g_result[][]用來存放凸包上的點,即最終所要的答案。同樣g_result[0][0]存放的是已找到的點的個數。
**/
void getResult(int Pack[240][2], int x1, int y1, int x2, int y2)
{
    int i,t,x3,y3,R,Rmax,tmax;
    int ResultPack[240][2];
    ResultPack[0][0] = 0;
    if(Pack[0][0] <= 1)
        return; 
    x3 = Pack[1][0];
    y3 = Pack[1][1];
    R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
    Rmax = R;
    tmax = 1;
    for(i=2;i<=Pack[0][0];i++)
    {
        x3 = Pack[i][0];
        y3 = Pack[i][1];
        R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
        if(R >= 0)
        {
            t = ++ResultPack[0][0];
            ResultPack[t][0] = x3;
            ResultPack[t][1] = y3;
        }
        if(R > Rmax)
        {
            Rmax = R;
            tmax = i;
        }
    }
    if(Rmax <= 0)
    {
        for(i=1;i<ResultPack[0][0];i++)
        {
            x3 = ResultPack[i][0];
            y3 = ResultPack[i][1];
            R = x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3;
            if(R == 0 && !((x3==x2&&y3==y2)||(x3==x1&&y3==y1)))
            {
                t = ++g_result[0][0];
                g_result[t][0] = ResultPack[i][0];
                g_result[t][1] = ResultPack[i][1];
            }
        }
        return;
    }
    else
    {
        t = ++g_result[0][0];
        g_result[t][0] = Pack[tmax][0];
        g_result[t][1] = Pack[tmax][1];
        if(ResultPack[0][0] == 0)
            return;
    }
    getResult(ResultPack,x1,y1,Pack[tmax][0],Pack[tmax][1]);
    getResult(ResultPack,Pack[tmax][0],Pack[tmax][1],x2,y2);
}

void main()
{
    int Point[240][2];//Point存所有點。
    int i=1;
    int x1,y1,x2,y2,x3,y3;
    g_result[0][0]=0;Point[0][0]=0;//Point的第一行第一列元素存放包里面有幾個點。初始化為0。
    printf("請輸入所有點的坐標:\n");
    while(scanf("%d,%d",&Point[i][0],&Point[i][1]) != EOF)
        i++;
    Point[0][0] = i-1;
    x1 = Point[1][0];
    y1 = Point[1][1];
    x2 = x1;
    y2 = y1;
    for(i=2;i<=Point[0][0];i++)
    {
        x3 = Point[i][0];
        y3 = Point[i][1];
        if(x3 < x1)
        {
            x1 = x3;
            y1 = y3;
        }
        else if(x3 > x2)
        {
            x2 = x3;
            y2 = y3;
        }
    }
    g_result[1][0] = x1;
    g_result[1][1] = y1;
    g_result[2][0] = x2;
    g_result[2][1] = y2;
    g_result[0][0] += 2;
    getResult(Point, x1, y1, x2, y2);
    getResult(Point, x2, y2, x1, y1);

    printf("\n\n構成凸包的點有:\n");
    for(i=1;i<=g_result[0][0];i++)
        printf("(%d,%d)\n",g_result[i][0],g_result[i][1]);
    system("pause");
}

 

Graham掃描法實現代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>


using namespace std;
struct node
{
    int x,y;
};
node vex[1000];//存入的所有的點
node stackk[1000];//凸包中所有的點
int xx,yy;
bool cmp1(node a,node b)//排序找第一個點
{
    if(a.y==b.y)
        return a.x<b.x;
    else
        return a.y<b.y;
}
double cross(node a,node b,node c)//計算叉積 判斷bc向量到ac向量 是否通過左轉得到 >0左轉 <0右轉  =0共線 
{
    return (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y);
}
double dis(node a,node b)//計算距離
{
    return sqrt((a.x-b.x)*(a.x-b.x)*1.0+(a.y-b.y)*(a.y-b.y));
}
bool cmp2(node a,node b)//極角排序另一種方法,速度快
{
    if(atan2(a.y-yy,a.x-xx)!=atan2(b.y-yy,b.x-xx))
        return (atan2(a.y-yy,a.x-xx))<(atan2(b.y-yy,b.x-xx));
    return a.x<b.x;
}
bool cmp(node a,node b)//極角排序
{
    int m=cross(vex[0],a,b);
    if(m>0)
        return 1;
    else if(m==0&&dis(vex[0],a)-dis(vex[0],b)<=0)  //共線 也讀入凸包 
        return 1;
    else return 0;
     
}
int main()
{
    int t,L;
    int i;
    t=7;
        
    vex[0].x=1,vex[0].y=0; 
    vex[1].x=-1,vex[1].y=1; 
    vex[2].x=2,vex[2].y=0; 
    vex[3].x=0,vex[3].y=0; 
    vex[4].x=-1,vex[4].y=-1; 
    vex[5].x=1,vex[5].y=-1; 
    vex[6].x=-2,vex[6].y=-0;   //可以用while讀  更改 
        
   
    memset(stackk,0,sizeof(stackk));
    
    sort(vex,vex+t,cmp1);//最左下角的點 
    stackk[0]=vex[0];  //第一個凸包點 
    sort(vex+1,vex+t,cmp);//cmp2是更快的,cmp更容易理解   極角排序
            
    stackk[1]=vex[1];//將第2個點存入凸包的結構體中
    
    int top=1;//最后凸包中擁有點的個數  實際為2 
    for(i=2; i<t; i++)
    {
                
        while(cross(stackk[top-1],stackk[top],vex[i])<0){   //是否滿足叉乘大於0 不滿足出棧  控制<0或<=0可以控制重點,共線
            top--;        
        }
        stackk[++top]=vex[i];  //滿足的入棧 
                        
    }
       
    for(i=0; i<=top; i++)   
        cout<<stackk[i].x<<" "<<stackk[i].y<<endl;
}

 練習題:圈水池


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM