本文的github地址在這裏~
Emmmm 今天的主題是:
凸包(Convex Hull)
But what is “凸包”?
You can find it here.
WTF???
沒事, 我知道wiki你萌看不下去.(因爲我也看不下去←_←
但是我們不是有baidu嘛..
說的通俗但不嚴謹一些, 就是能包含平面上所有給定點的最小的凸多邊形.
利用之前學過的知識, 我們已經能解決不少計算幾何方面的問題, 甚至都能計算任意多邊形的面積了. 但是如何高效地求凸包還是不那麼顯然的, 這就需要學習一些dalao們的算法了.
目前來看求凸包的常見方法有: 暴力(
但是
暴力
複雜度顯然不怎麼科學, 而且做法也特別顯然.
所以我們就不說了.
Graham掃描法
wiki上講得非常好.. 還有圖.. 不過生肉啃起來有點累OvO
試圖google翻譯 但是機翻翻出來的真的不是人話…
但是還是要認真地學一學的(還可以順便提高英語閱讀水平不是→_→)
算法流程
我們一步一步地剖析這個算法.
首先先給出給定的點
step1
找到
這個不需要排序啦~一般讀入的時候處理一下就好了, 這一步的複雜度是
step2
然後我們將所有點
當然這個排序比較的時候並不需要真的算出夾角(三角函數是很昂貴的…)
我們知道
而且夾角的範圍是
或者考慮斜率似乎也可以.. 或者用叉積..
排序算法只要是
不過作爲C++選手直接sort就okay了~
特別注意: 共線的時候要按照距離排序…
step3
建一個棧.
我們可以很容易的看出和證出
於是將
然後按順序枚舉其他的點, 判斷一下線段的拐向是順時針還是逆時針.
如果是逆時針的話, 這個圖形就還是凸的, 將其入棧.
而如果是順時針的話, 這個圖形就不再是凸的了, 我們需要退棧.
比如4-5這條邊就在3-4的順時針方向, 如果連上就變凹了, 所以4這個點是在凸包內的點, 退棧.
退到3後發現3-5就在2-3的逆時針了, 5進棧.
假如3-5仍然在2-3的順時針方向, 則3也要退棧, 依此類推, 直到有逆時針方向或棧中只有
時間複雜度
按這個方法一路推到
其實wiki上的內個動圖就畫的非常好, 我覺得大家都應該去看一下.
最後驚奇地發現複雜度主要取決於sort的複雜度…
總時間複雜度:
凸包的題目(包括但不限於模板題)是很多的, 這裏用了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]));
}
注意事項
按夾角排序的時候建議使用叉積.. 因爲好寫、不使用除法精度好等原因.
記得加上最後一條邊(還記得多邊形的首尾相接吧).
P 點就不要參與排序了吧.
其實還是比較好寫的..(畢竟只是個板子)
Jarvis步進法
其實wiki上面這個條目叫Gift wrapping algorithm(禮品包裝算法)來着..
這個算法是輸出敏感的, 複雜度
也就是說極限情況下(所有點都在凸包上)是
算法流程
給定的點還是上面那些, 這裏就不畫了.
step1
找到一個一定在凸包上的點
這裏我們找最左邊的點(這次
方法同上, 時間複雜度
step2
枚舉每一個點
可以肯定的是, 這個
令
時間複雜度
step3
重複step2, 直到
這樣我們就找到了所有在凸包上的點, 也就是說找到了這個凸包.
每次我們會找到一個凸包上的點, 然後進行一次step2, 所以step2總共會進行
還是比較簡單的. 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]]));
}
注意事項
- 就是枚舉的時候不要枚舉自己.
- 共線的時候要選最遠的.(看到裏面內個if語句寫了多麼一坨了麼←_←
快包算法
wiki傳送門
跟快排十分相似, 平均複雜度
思路也和快排比較相似, 用的是分治.
算法流程
step1
找到最左邊和最右邊的點
然後把他們連接起來.
step2
這條線上面找到這條線最遠的點
然後遞歸處理
step3
找從
這個複雜度的證明方式我是真的不會了..
快包可以比較方便地只找上/下凸殼.
實現代碼
這個方法看上去很簡單但是寫起來很鬼畜啊= =爲了方便理解和寫, 我們放一下僞代碼.
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]));
}
總結
其他的還有不少高端大氣上檔次的算法, 比如似乎有
來對注意事項做一波彙總
- 以上的算法都沒有涉及到1個點 2個點的情況 如果可能出現的話要記得特判..
- 還有一種可能需要進行特判的就是構不成凸多邊形(就是都在一條直線上的情況)…
- 實數比較的時候記得用cmp… 主要是判斷相等的時候= =
- 凸包中最後一條邊記得考慮.
- 有些時候要考慮正負號和絕對值的問題. 比如面積. 比如距離.
代碼已經上傳到github
好的, 完結撒花~