【筆記篇】最良心的計算幾何學習筆記(四)

border="0" width="330" height="86" src="//music.163.com/outchain/player?type=2&id=512358011&auto=1&height=66">

本文的github地址在這裏~

Emmmm 今天的主題是:

凸包(Convex Hull)

But what is “凸包”?
You can find it here.
WTF???

沒事, 我知道wiki你萌看不下去.(因爲我也看不下去←_←
但是我們不是有baidu嘛..
說的通俗但不嚴謹一些, 就是能包含平面上所有給定點的最小的凸多邊形.
一個凸包的栗子

利用之前學過的知識, 我們已經能解決不少計算幾何方面的問題, 甚至都能計算任意多邊形的面積了. 但是如何高效地求凸包還是不那麼顯然的, 這就需要學習一些dalao們的算法了.

目前來看求凸包的常見方法有: 暴力(O(n3) ) Graham掃描法(O(nlogn) ) Jarvis步進法(O(nH) ) 快包算法(O(nlogn) ) 等

但是

暴力

複雜度顯然不怎麼科學, 而且做法也特別顯然.
所以我們就不說了.

Graham掃描法

wiki上講得非常好.. 還有圖.. 不過生肉啃起來有點累OvO
試圖google翻譯 但是機翻翻出來的真的不是人話…
但是還是要認真地學一學的(還可以順便提高英語閱讀水平不是→_→)

算法流程

我們一步一步地剖析這個算法.
首先先給出給定的點

step1

找到y 座標最小的點P , 如果y 相同則找x 更小的.
這個不需要排序啦~一般讀入的時候處理一下就好了, 這一步的複雜度是O(n) .

step2

然後我們將所有點Pi 按照PPix 軸的夾角θ 排序..
當然這個排序比較的時候並不需要真的算出夾角(三角函數是很昂貴的…)
我們知道cos<a⃗ ,b⃗ >=a⃗ b⃗ |a⃗ ||b⃗ |
而且夾角的範圍是[0,π) , 這個區間內cos 是單調遞減的, 這樣我們取x 軸正方向的單位向量爲b⃗  再算即可..
或者考慮斜率似乎也可以.. 或者用叉積..

排序算法只要是O(nlogn) (或以內)的就可以了. (但是實數的基數排序不靠譜吧)
不過作爲C++選手直接sort就okay了~

特別注意: 共線的時候要按照距離排序…

step3

建一個棧.
我們可以很容易的看出和證出P0 , P1Pn1 是凸包上的點.
於是將P0P1 入棧.

然後按順序枚舉其他的點, 判斷一下線段的拐向是順時針還是逆時針.
如果是逆時針的話, 這個圖形就還是凸的, 將其入棧.

而如果是順時針的話, 這個圖形就不再是凸的了, 我們需要退棧.

比如4-5這條邊就在3-4的順時針方向, 如果連上就變凹了, 所以4這個點是在凸包的點, 退棧.
退到3後發現3-5就在2-3的逆時針了, 5進棧.
假如3-5仍然在2-3的順時針方向, 則3也要退棧, 依此類推, 直到有逆時針方向或棧中只有P 爲止.(其實因爲排過序了所以退到最後也會剩下PP1 這條邊的…)
時間複雜度O(n) , 雖然看上去好像最壞會一直退到最前面, 複雜度像是O(n2) 的, 但實際上每條邊最多被考慮兩次(入棧的時候一次, 退棧的時候一次…)

按這個方法一路推到Pn1 (前面說過肯定在凸包上), 就得到了凸包.

其實wiki上的內個動圖就畫的非常好, 我覺得大家都應該去看一下.

最後驚奇地發現複雜度主要取決於sort的複雜度…
總時間複雜度:O(nlogn)

凸包的題目(包括但不限於模板題)是很多的, 這裏用了luogu的一道例題.. [祕技:傳送~]

實現代碼

#include <cmath> 
#include <cstdio>
#include <algorithm>
const int N=50101;
const double eps=1e-9;
int dcmp(const double &a){
    if(fabs(a)<eps)return 0;
    return a<0?-1:1;
}
inline double max(const double &a,const double &b){return dcmp(a-b)>0?a:b;}
struct point{
    double x,y;
    point(const double &X=0,const double &Y=0):x(X),y(Y){}
}p[N],stk[N];int tp,mi;
point operator -(const point &a,const point &b){
    return point(a.x-b.x,a.y-b.y);
}
double operator ^(const point &a,const point &b){
    return a.x*b.x+a.y*b.y;
}
double operator *(const point &a,const point &b){
    return a.x*b.y-a.y*b.x;
}
double len(const point &a){
    return sqrt(a^a);
}
//bool cmpa(const point &a,const point &b){
//  point X=point(1,0),A=a-p[0],B=b-p[0];
//  double coa=(A^X)/len(A),cob=(B^X)/len(B);
//  共線的時候選遠的那個.保證前面的在凸包上.
//  if(!dcmp(coa-cob)) return dcmp(len(A)-len(B))>0; 
//  return dcmp(coa-cob)>0;
//} //按夾角排序(點積版)
bool cmpa(const point &a,const point &b){
    point A=a-p[0],B=b-p[0];
    if(!dcmp(A*B)) return dcmp(len(A)-len(B))>0;
    return dcmp(A*B)>0;
} //叉積版 
void grahamScan(point* p,int n){
    std::sort(p+1,p+n,cmpa);
    stk[++tp]=p[0]; stk[++tp]=p[1];
    for(int i=2;i<n;++i){
        while(dcmp((p[i]-stk[tp])*(stk[tp]-stk[tp-1]))>=0&&tp>2) --tp;
        //由於排過序共線的時候一定不優, 所以不逆時針而且能退棧(棧裏大於兩個點)就退棧
        stk[++tp]=p[i]; //進棧
    }
}
int main(){
    int n; scanf("%d",&n); double my=1e9,ans=0;
    for(int i=0;i<n;++i){
        scanf("%lf%lf",&p[i].x,&p[i].y);
        if(dcmp(p[i].y-my)<0||
        (!dcmp(p[i].y-my)&&dcmp(p[i].x-p[mi].x)<0))
            my=p[i].y,mi=i; 
    } std::swap(p[0],p[mi]);
    grahamScan(p,n);
    for(int i=1;i<tp;++i)
        ans+=len(stk[i]-stk[i+1]);
    printf("%.2lf",ans+len(stk[tp]-p[0]));
}

注意事項

  1. 按夾角排序的時候建議使用叉積.. 因爲好寫、不使用除法精度好等原因.

  2. 記得加上最後一條邊(還記得多邊形的首尾相接吧).

  3. P 點就不要參與排序了吧.

其實還是比較好寫的..(畢竟只是個板子)

Jarvis步進法

其實wiki上面這個條目叫Gift wrapping algorithm(禮品包裝算法)來着..

這個算法是輸出敏感的, 複雜度O(nH) , H 是凸包上點的個數.
也就是說極限情況下(所有點都在凸包上)是O(n2) 的, 但很多情況下H 是小於logn 的, 那麼反而會比Graham掃描要快.(比如人口比較集中分佈在城市, 郊區的人口較少之類的). 但是算法競賽中還是要慎用…

算法流程

給定的點還是上面那些, 這裏就不畫了.

step1

找到一個一定在凸包上的點P , 然後令H=P .
這裏我們找最左邊的點(這次y 應該是無所謂的)
方法同上, 時間複雜度O(n) .

step2

枚舉每一個點H , 找出所有的HH 中最逆時針方向的一個(利用叉積即可)

可以肯定的是, 這個H 也在凸包上.
H=H
時間複雜度O(n)

step3

重複step2, 直到H 重新等於P 爲止.

這樣我們就找到了所有在凸包上的點, 也就是說找到了這個凸包.
每次我們會找到一個凸包上的點, 然後進行一次step2, 所以step2總共會進行H 次, 所以時間複雜度O(H)O(n)=O(nH)
還是比較簡單的. wiki上說”在二維中, 禮品包裝算法類似於在一組點上纏繞線(或包裝紙)的過程”.
而且這種算法似乎是可以擴展到更高維度的. 等以後再學吧..

實現代碼

還是那道題吧.. 本來以爲可能會被卡於是開了個O2但似乎並不會卡…

// luogu-judger-enable-o2
#include <cmath> 
#include <cstdio>
const int N=10101;
const double eps=1e-9;
int dcmp(const double &a){
    if(fabs(a)<eps)return 0;
    return a<0?-1:1;
}
struct point{
    double x,y;
    point(const double &X=0,const double &Y=0):x(X),y(Y){}
}p[N];
point operator -(const point &a,const point &b){
    return point(a.x-b.x,a.y-b.y);
}
double operator *(const point &a,const point &b){
    return a.x*b.y-a.y*b.x;
}
double len(const point &a){
    return sqrt(a.x*a.x+a.y*a.y);
}
int po[N],tot,mi;
void jarvisMarch(point *p,int n){
    int h=mi;
    do{
        int h2=-1;
        for(int i=0;i<n;++i)
            if(h!=i&&(h2<0||dcmp((p[i]-p[h])*(p[h2]-p[h]))>0
                ||(!dcmp((p[i]-p[h])*(p[h2]-p[h]))
                &&dcmp(len(p[h2]-p[h])-len(p[i]-p[h]))<0)))
                h2=i;
        po[++tot]=h=h2;
    }while(h!=mi);
}
int main(){
    int n;scanf("%d",&n); double ans=0,mx=1e9;
    for(int i=0;i<n;++i){
        scanf("%lf%lf",&p[i].x,&p[i].y);
        if(p[i].x<mx) mx=p[i].x,mi=i;
    }
    jarvisMarch(p,n);
    for(int i=1;i<tot;++i)
        ans+=len(p[po[i]]-p[po[i+1]]);
    printf("%.2lf",ans+len(p[po[tot]]-p[po[1]]));
}

注意事項

  1. 就是枚舉的時候不要枚舉自己.
  2. 共線的時候要選最遠的.(看到裏面內個if語句寫了多麼一坨了麼←_←

快包算法

wiki傳送門
跟快排十分相似, 平均複雜度O(nlogn) , 最好的情況能達到O(n) , 最壞的情況會變成O(n2) . 但還是一種比較常見的做法.
思路也和快排比較相似, 用的是分治.

算法流程

step1

找到最左邊和最右邊的點A,B , 它們一定在凸包上.
然後把他們連接起來.

step2

這條線上面找到這條線最遠的點C , 這個點一定在凸包上.
然後遞歸處理AC,BC 之間的凸殼.

step3

找從BA 的上凸殼, 也就是下凸殼..

這個複雜度的證明方式我是真的不會了..

快包可以比較方便地只找上/下凸殼.

實現代碼

這個方法看上去很簡單但是寫起來很鬼畜啊= =爲了方便理解和寫, 我們放一下僞代碼.

quickHull(點集S,有向線段P[A->B]){
    選取距離P最遠的點C
    有向線段M[A->C] N[C->B]
    點集SL{X|X∈S且在M左側}
    點集SR{X|X∈S且在N左側}
    quickHull(SL,M)
    C是凸包上的點
    //這裏務必採用中序的方式,保證凸包上的點是按順序輸出的
    quickHull(SR,N)
}

根據僞代碼我們可以整合出代碼來

#include <cmath> 
#include <cstdio>
#include <vector>
using namespace std;
const int N=10101;
const double eps=1e-9;
int dcmp(const double &a){
    if(fabs(a)<eps)return 0;
    return a<0?-1:1;
}
struct point{
    double x,y;
    point(const double &X=0,const double &Y=0):x(X),y(Y){}
}hull[N];
typedef vector<point> pset;
typedef pset::iterator pit;
int mi,tot;
inline bool cmp(const point &a,const point &b){
    if(!dcmp(a.x-b.x)) return dcmp(a.y-b.y)<0;
    return dcmp(a.x-b.x)<0;
}
point operator -(const point &a,const point &b){
    return point(a.x-b.x,a.y-b.y);
}
double operator *(const point &a,const point &b){
    return a.x*b.y-a.y*b.x;
}
double operator ^(const point &a,const point &b){
    return a.x*b.x+a.y*b.y;
}
double len(const point &a){
    return sqrt(a.x*a.x+a.y*a.y);
}
double ptDisSeg(const point &p,const point &q0,const point &q1){
    if(!dcmp((p-q0)^(q1-q0))) return len(p-q0); 
    if(!dcmp((p-q1)^(q0-q1))) return len(p-q1);
    return fabs((p-q0)*(q1-q0)/len(q1-q0)); 
}
void quickHull(pset s,const point &a,const point &b){
    pset sl,sr; double dis=-1e9; point c;
    for(pit i=s.begin();i!=s.end();++i){
        double d=ptDisSeg(*i,a,b);
        if(d>dis) dis=d,c=*i;
    }
    for(pit i=s.begin();i!=s.end();++i){
        if(dcmp((*i-a)*(c-a))<0)
            sl.push_back(*i);
        if(dcmp((*i-c)*(b-c))<0)
            sr.push_back(*i);
    }
    if(sl.size())quickHull(sl,a,c);
    hull[++tot]=c;
    if(sr.size())quickHull(sr,c,b);
}
int main(){
    int n; scanf("%d",&n); pset p;
    double my=1e9,ans=0;
    for(int i=0;i<n;++i){
        double x,y; scanf("%lf%lf",&x,&y);
        if(x<my) mi=i,my=x; p.push_back(point(x,y));
    } swap(p[0],p[mi]); hull[++tot]=p[0]; p.erase(p.begin());
    quickHull(p,hull[1],hull[1]);
    for(int i=1;i<tot;++i)
        ans+=len(hull[i]-hull[i+1]);
    printf("%.2lf",ans+len(hull[tot]-hull[1]));
}

總結

其他的還有不少高端大氣上檔次的算法, 比如似乎有O(nlogH) 的Chan’s Algorithm什麼的. 有時間再研究吧= = 這幾種方法應該夠用了.

來對注意事項做一波彙總

  1. 以上的算法都沒有涉及到1個點 2個點的情況 如果可能出現的話要記得特判..
  2. 還有一種可能需要進行特判的就是構不成凸多邊形(就是都在一條直線上的情況)…
  3. 實數比較的時候記得用cmp… 主要是判斷相等的時候= =
  4. 凸包中最後一條邊記得考慮.
  5. 有些時候要考慮正負號和絕對值的問題. 比如面積. 比如距離.

代碼已經上傳到github

好的, 完結撒花~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章