凸包算法知识总结
首先,什么是凸包?
假設平面上有p0~p12共13個點,過某些點作一個多邊形,使這個多邊形能把所有點都“包”起來。當這個多邊形是凸多邊形的時候,我們就叫它“凸包”。
處理何種問題:凸包可以看成在木板上釘許多釘子,用一根橡皮筋框住所有釘子所得到的多邊形,最終能求得都由哪些釘子構成該凸包。如下圖所示:
然后,什么是凸包問題?
我們把這些點放在二維坐標系里面,那么每個點都能用 (x,y) 來表示。
現給出點的數目13,和各個點的坐標。求構成凸包的點?
解一:窮舉法(蠻力法)
時間復雜度:O(n3)。
思路:兩點確定一條直線,如果剩余的其它點都在這條直線的同一側,則這兩個點是凸包上的點,否則就不是。
步驟:
將點集里面的所有點兩兩配對,組成 n(n-1)/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。
對下包也作類似操作。
然而怎么求距離某直線最遠的點呢?我們還是用到解一中的公式:
設有一個點 P3 和直線 P1P2 。(坐標:p1(x1,y1),p2(x2,y2),p3(x3,y3))
對上式的結果取絕對值,絕對值越大,則距離直線越遠。
注意:在步驟一,如果橫坐標最小的點不止一個,那么這幾個點都是凸包上的點,此時上包和下包的劃分就有點不同了,需要注意。
解三:Jarvis步進法
時間復雜度:O(nH)。(其中 n 是點的總個數,H 是凸包上的點的個數)
思路:
縱坐標最小的那個點一定是凸包上的點,例如圖上的 P0。
從 P0 開始,按逆時針的方向,逐個找凸包上的點,每前進一步找到一個點,所以叫作步進法。
怎么找下一個點呢?利用夾角。假設現在已經找到 {P0,P1,P2} 了,要找下一個點:剩下的點分別和 P2 組成向量,設這個向量與向量P1P2的夾角為 β 。當 β 最小時就是所要求的下一個點了,此處為 P3 。
注意:
找第二個點 P1 時,因為已經找到的只有 P0 一個點,所以向量只能和水平線作夾角 α,當 α 最小時求得第二個點。
共線情況:如果直線 P2P3 上還有一個點 P4,即三個點共線,此時由向量P2P3 和向量P2P4 產生的兩個 β 是相同的。我們應該把 P3、P4 都當做凸包上的點,并且把距離 P2 最遠的那個點(即圖中的P4)作為最后搜索到的點,繼續找它的下一個連接點。
解四:Graham掃描法
時間復雜度:O(n㏒n)
思路:Graham掃描的思想和Jarris步進法類似,也是先找到凸包上的一個點,然后從那個點開始按逆時針方向逐個找凸包上的點,但它不是利用夾角。
步驟:
把所有點放在二維坐標系中,則縱坐標最小的點一定是凸包上的點,如圖中的P0。
把所有點的坐標平移一下,使 P0 作為原點,如上圖。
計算各個點相對于 P0 的幅角 α ,按從小到大的順序對各個點排序。當 α 相同時,距離 P0 比較近的排在前面。例如上圖得到的結果為 P1,P2,P3,P4,P5,P6,P7,P8。我們由幾何知識可以知道,結果中第一個點 P1 和最后一個點 P8 一定是凸包上的點。
(以上是準備步驟,以下開始求凸包)
以上,我們已經知道了凸包上的第一個點 P0 和第二個點 P1,我們把它們放在棧里面。現在從步驟3求得的那個結果里,把 P1 后面的那個點拿出來做當前點,即 P2 。接下來開始找第三個點:
連接 棧最上面的連個元素(即P0和棧頂)的那個點,得到直線 L 。看當前點是在直線 L 的右邊還是左邊。如果在直線的右邊就執行步驟5;如果在直線上,或者在直線的左邊就執行步驟6。
如果在右邊,則棧頂的那個元素不是凸包上的點,把棧頂元素出棧。執行步驟4。
當前點是凸包上的點,把它壓入棧,執行步驟7。
檢查當前的點 P2 是不是步驟3那個結果的最后一個元素。是最后一個元素的話就結束。如果不是的話就把 P2 后面那個點做當前點,返回步驟4。
最后,棧中的元素就是凸包上的點了。
解五:Melkman算法
說真的,這個算法我也還沒有看清。網上的資料也少的可憐,我暫且把網上的解釋截個圖在這里,往后搞懂以后再回來補上。
性能:由一個快排(O(nlogn))和一個遍歷找點(O(n)),總體時間復雜度為O(nlogn)。
原理:
點:A(x1,y1),B(x2,y2)
向量AB=(x2-x1,y2-y1)=(x,y)
向量的叉積:a X b =
通過結果的正負判斷兩矢量之間的順逆時針關系
l 若a X b > 0,表示a在b的順時針方向上
l 若a X b < 0,表示a在b的逆時針方向上
l 若a X b == 0,表示a與b共線,但不確定方向是否相同
例如:
A(0,0)
B(2,2)
C(3,1)
D(2,-1)
AB(2,2),AC(3,1),AD(2,-1)
AC X AB = 32-12 = 4>0
AC在AB的順時針方向上,即點C在向量AB的下面。
實現步驟:
排序:按照x由小到大排序,如果x相同,按照y由小到大排序。
排序之后第一個點必為凸包上的點(證明自己意淫一下,有x最大、x最小、y最小、y最大的點都必在凸包上)。
選最近兩個剛入凸包的點,再在排序中依次選點,根據上面所提及到的原理,判斷該點在凸包那兩點的順時針還是逆時針方向。
如果在逆時針方向,將該點加入凸包,否則判定出之前進入凸包的點不合格,刪除該凸包點,重復第三步,直到該點加入凸包(也就是說每個點都曾進過凸包,只是后來有些被刪了)。
以上就是下凸包的構成步驟,上凸包參考下凸包,基本沒有什么差別,因為在判斷時是判斷是否為逆時針,別誤以為是在判段該點在向量的下方,上凸包就不可用了,對于逆時針而言都是一樣的。
這種方法求出來的點是凸包沿著逆時針方向找出來的,首位相接且第一個點重復兩次,所以除了點只有一個的情況下,記得點的個數減一。
備注:對于題目要求求凸包構成的面積時,可以參考以下圖示求法:
輸入樣例解釋:
11—散點樣例個數
5 8 —散點坐標
12 56
5 2
125 1
15 66
45 77
55 6
45 2
232 5
45 12
54 66
輸出樣例解釋:
tot=7 —構成凸包點的個數
1: 5.00 , 2.00 —沿著凸包逆時針方向,且保留兩位小數
2: 125.00 , 1.00
3: 232.00 , 5.00
4: 45.00 , 77.00
5: 15.00 , 66.00
6: 12.00 , 56.00
7: 5.00 , 8.00
//求凸包,時間復雜度nlogn #include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> using namespace std;const int MaxN=10010;int n,tot;//n為點的個數,tot為凸點的個數 struct point {double x,y; }; point p[MaxN],CHP[MaxN];//CHP為凸包最后所構成的點bool cmp(point a,point b)//水平排序,按x從大到小排,如果x相同,按y從大到小排序 {return (a.x<b.x||(a.x==b.x&&a.y<b.y)); }double xmul(point a,point b,point c)//叉積 {return (b.x-a.x)*(c.y-a.y)-(b.y-a.y)*(c.x-a.x); }void Andrew() {sort(p,p+n,cmp);tot=0;for(int i=0; i<n; ++i) //計算下半個凸包{while(tot>1&&xmul(CHP[tot-2],CHP[tot-1],p[i])<0)--tot;CHP[tot++]=p[i];}int k=tot;for(int i=n-2; i>=0; --i) //計算上半個凸包{while(tot>k&&xmul(CHP[tot-2],CHP[tot-1],p[i])<0)--tot;CHP[tot++]=p[i];}if(n>1)//對于只有一個點的包再單獨判斷--tot; }int main() {scanf("%d",&n);for(int i=0; i<n; ++i){scanf("%lf%lf",&p[i].x,&p[i].y);}Andrew();printf("tot=%d\n",tot);for(int i=0; i<tot; ++i){printf("%d: %.2lf , %.2lf\n",i+1,CHP[i].x,CHP[i].y);}return 0; }一些預備知識點:
首先在二維坐標下介紹一些定義:
點:A(x1,y1),B(x2,y2)
向量:向量AB=( x2 - x1 , y2 - y1 )= ( x , y );
向量的模 |AB| = sqrt ( xx+yy );
向量的點積: 結果為 x1x2 + y1y2。
點積的結果是一個數值。
點積的集合意義:我們以向量 a 向向量 b 做垂線,則 | a | * cos(a,b)為 a 在向量 b 上的投影,即點積是一個向量在另一個向量上的投影乘以另一個向量。且滿足交換律
應用:可以根據集合意義求兩向量的夾角,
cos(a,b) =( 向量a * 向量b ) / (| a | * | b |) = (x1x2 + y1y2) / (| a | * | b |)
向量的叉積: 結果為 x1y2-x2y1
叉積的結果也是一個向量,是垂直于向量a,b所形成的平面,如果看成三維坐標的話是在 z 軸上,上面結果是它的模。
方向判定:右手定則,(右手半握,大拇指垂直向上,四指右向量a握向b,大拇指的方向就是叉積的方向)
叉積的集合意義:1:其結果是a和b為相鄰邊形成平行四邊形的面積。
2:結果有正有負,有sin(a,b)可知和其夾角有關,夾角大于180°為負值。
3:叉積不滿足交換律
應用:
1:通過結果的正負判斷兩矢量之間的順逆時針關系
若 a x b > 0表示a在b的順時針方向上
若 a x b < 0表示a在b的逆時針方向上
若 a x b == 0表示a在b共線,但不確定方向是否相同
總結
- 上一篇: Pipe HDU - 2150(判断线段
- 下一篇: Power Network POJ -