先說部分資料來源(蒟蒻也是從他們那里學會的):
數學:凸包算法詳解——愛國吶
計算幾何之凸包(convexHull)----Graham掃描法——天澤28
話說本來在學斜率優化DP,結果因為某位坑爹博主的一句本來沒有問題的話:
是不是很像一個下凸包?
我們用當前的斜率k從下方去不斷逼近下凸包,最終會先碰到哪一個點?
我莫名其妙的去學了凸包,覺得學完之后斜率DP說不定能好做點,但是。。。。。。現在依然看不明白。
那么進入正題,先看這樣一個例題:

mhr:明顯,這個題我們可以暴力枚舉!
博主:滾!
可以發現,暴力是絕對不可取的,其實如果這個圖就擺在你面前的話,你是很容易就能看出來,柵欄長度就是最外面的一圈點,但是電腦並沒有眼睛,所以我們就要使用一些算法來計算出來那外面的一圈,也就是所謂的凸包。有一個比較學術的說法,來自某度某科:
我們有很多不同的方法求出凸包,不過這里介紹的是性價比很高的Graham算法。
graham算法的整個操作基本都在一個棧中完成。如果設所有點的集合為點集Q,那么Q中的所有點都要入棧一次,然后再把一部分不符合要求的點彈出棧,最后剩在棧中的點就是凸包上的點了。
具體實現步驟如下
- 首先我們要選取一個基點o,要求在點集Q中,基點o縱坐標必須是最小的,如果有相同最小的縱坐標,那么選取橫坐標最小的,如果還有相同的。。肯定是重點,不用管它就是了。如何快速找到?如果你不嫌麻煩,大可以sort排序之后選第一個,然而實際上,我們只需要找出那個最下面同時是最左邊的點就可以了,因為之后整個點集還會重新排序,所以這一開始的順序沒什么用。
-
然后我們把剩下的所有點以o為極點,進行極角排序,角小的放在前面,如果角度相同,那么按照到點o的距離排序。
-
設所有的點事p1,p2.....pn。將o,p1,p2三個點壓入棧,開始遍歷所有剩下的點。
-
對每一個新遍歷到的點,很明顯我們需要逆時針旋轉當前方向,如果有一個點順時針旋轉了,那么我們就把棧頂的點彈出,直到符合逆時針旋轉這個要求為止。
看上去十分簡明扼要易懂是不是???
讀者:打死這個博主,寫這些東西我能看懂啥??那個順時針逆時針我能看出來,電腦那個愚蠢的東西咋看出來??
判斷順時針還是逆時針旋轉,我們要用到一個東西——叉積。
好吧又一個新名詞。某度某科這么定義叉積:
向量積,數學中又稱外積、叉積,物理中稱矢積、叉乘,是一種在向量空間中向量的二元運算。與點積不同,它的運算結果是一個向量而不是一個標量。並且兩個向量的叉積與這兩個向量和垂直。其應用也十分廣泛,通常應用於物理學光學和計算機圖形學中。
嗯。。。這其實什么也沒說明白,這么說吧,貌似博主們和筆者都是一致認為:叉積a×b是點0,a,b和a+b組成的平行四邊形的向量面積(也就是有方向的面積)。如果計算出來的叉積是正,那么a在b右側,否則a在b左側。如果增添一個公共端點c,那么計算方法就是:(c-a)×(c-b)。P.s可能說的不是很明白,因為筆者對於插入數學公式這種操作還是略不熟練,文章最上方的dalao的博客里有比較清晰地證明。
那么給出算法導論里的證明,還是比較明白的:

然而其實也有瑕疵,就是這個正負和你計算的順序是有關系的,經常有不等號寫反結果一直爆0的現象發生。所以有句老話“盡信書則不如無書”,不要過分相信書上說的,多實踐才是真理。
其實應該手繪靜態步驟圖的,但是確實是沒那個實力,博主從小學開始美術就連B都沒得過,全是CD。。。。
那么借來一張動態的給大家看一眼吧:

。
那么基本上大體的就說完了,接下來就是代碼了。與往常不同,博主這次會加詳細的注釋誒:
p.s:以上面那道題目為例。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<queue> #define ll long long #define inf 50000000 #define re register #define MAXN 50005 using namespace std; struct node{ double x,y; }; node a[MAXN],stackk[MAXN]; double xx,yy; int n,top; inline int read() { int x=0,c=1; char ch=' '; while((ch>'9'||ch<'0')&&ch!='-')ch=getchar(); while(ch=='-') c*=-1,ch=getchar(); while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar(); return x*c; } inline double js(node a,node b)//計算距離自不必說 { return sqrt((a.x-b.x)*(a.x-b.x)*1.0+(a.y-b.y)*(a.y-b.y)*1.0); } inline bool cmp(node a,node b)//第一遍排序,來求基點。 { if(a.y==b.y) return a.x<b.x; return a.y<b.y; } inline double cross(node a,node b,node c)//計算以a為公共端點,b與c的叉積。 { return (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y); } inline bool cmp1(node a,node b)//極角排序 { double k=cross(stackk[1],a,b); if(k>0) return 1;//如果b在a時針方向返回1 else if(k==0) return js(stackk[1],a)<=js(stackk[1],b);//如果極角相等,則比較距離 else return 0; } int main() { n=read(); for(re int i=1;i<=n;i++){ scanf("%lf%lf",&a[i].x,&a[i].y); } if(n==1) {printf("0");return 0;} if(n==2) {printf("%.2lf",js(a[1],a[2]));return 0;} sort(a+1,a+n+1,cmp); stackk[++top]=a[1]; xx=stackk[top].x; yy=stackk[top].y; sort(a+2,a+n+1,cmp1); stackk[++top]=a[2]; stackk[++top]=a[3];//把p1,p2,p3壓入棧中。 for(re int i=4;i<=n;i++){ while(top>0&&cross(stackk[top-1],stackk[top],a[i])<0)//如果右旋轉了,就彈出棧頂的點 top--; stackk[++top]=a[i];//加入新點 } double ans=0; for(re int i=2;i<=top;i++)//點之間兩兩求距離。 ans+=js(stackk[i-1],stackk[i]); ans+=js(stackk[top],stackk[1]); printf("%.2lf",ans); }
其實也不是很詳細了。。。不過筆者自認為碼風清晰易懂(逃~~
