尋找最大凸多邊形

某公司今年的技能鑒定考試是這樣的:

給出一系列平面直角坐標系中的點的坐標,寫一個程序,找出能夠圍住這些點的最大的凸多邊形,按照順時針的順序,將這個最大凸多邊形的頂點依次打印出來。如果某個點落在多邊形的邊上,該點不得打印。

想象一下在每一個點上定一個釘子,然后用一根線把這些釘子給全部圍住,線繃直了之后,就是我們要找的凸多邊形了。做出這道題的關鍵是找到算法,同時還必須有一些平面幾何的知識。下面給出兩個算法。

第一個算法描述如下:

1. 在所有點中,找出縱坐標最大的那個點(最高的點);如果有好幾個點的縱坐標都是最大的,那么就從中選出最左邊(橫坐標最小)的那個點;可以從數學上嚴格證明,這個點一定是最大凸多邊形的頂點;這里就不證明了,歡迎熱血青年來補充。
2. 記錄下這個點,記做A,從A出發按如下算法尋找下一個頂點B;
3. 連接A點和剩下的其它點,形成一系列的向量AB,從正向X軸出發順時針旋轉一個角度之后會和向量AB重合,記錄下這個夾角為α。
4. 形成最小夾角α的那個點B,就是我們要尋找的下一個頂點B。如果有好幾個點都形成相等的最小夾角,那么我們選擇線段AB長度最長的那個B點。其它長度較短的點則要從原始點集合中刪除掉。
5. 按照同樣的算法,從B點出發尋找下一頂點C,使得BC向量和正向X軸的夾角最小β,但是β必須大于上一步的α。
6. 以此類推,從C出發尋找D……,直到找到下一個頂點和A點重合,那么尋找結束。

這個算法,實際上就是模擬拿一根棍子貼著多邊形滾動了一周。它要求我們會計算兩個點的距離,會計算兩個向量的夾角。對于我大五百強的員工而言,這些應該都是小兒科了。

上面的這個算法雖然巧妙,但是卻缺少了一些數學上和程序上的美感,顯得簡單而且粗暴。算法,也要講究“性感”,下面是一個性感的解法:

從N個點里面找最大凸多邊形,比較困難;但是如果我們已經知道了這里面的N-1個點形成的最大凸多邊形的頂點,那么再加上第N個點A,根據A和原來凸多邊形的位置關系,就可以進一步計算出這N個點形成的新凸多邊形頂點了。

沒錯,我們用到遞歸,來將復雜問題簡單化。遞歸到最簡單的情況,就是三個點,這三個點要么是在一條直線上,要么一定形成一個最簡單的凸多邊形,那就是三角形。三個點能否形成三角形,是很好判斷的J。

以圖一為例:已有凸多邊形 ABCDEFGHI,現在加入一個新的點 J,顯然,加上J之后,最大凸多邊形應該變成 ABJEFGHI,CD兩頂點要刪除,并且用頂點J替換掉C和D;

圖一

以圖二為例:E點在AB的延長線上,多邊形ABCD加上E點后,新的凸多邊形應該是AECD,原來的B點應該被E替換掉;但如果ABCD 加上 F 點之后,新的凸多邊形應該是ABFCD,原來的凸多邊形上沒有點要被刪除,但是要插入新的F點。如果F點在多邊形內部,那么直接拋棄F點即可。多邊形還是不變。

圖二

問題演變為,如何判斷一個孤立點和一個凸多邊形的關系呢?知道它們的關系后,怎么來計算出新的凸多邊形呢?這里要用到向量的叉乘的概念,兩個向量a=(x1,y2)和b=(x2,y2)的叉乘結果:

|a| ^ |b| = |a| * |b| *sin<a,b> = x1*y2 - x2*y1 <a,b>夾角必須小于180°
a ^ b = - b ^ a

叉乘結果是矢量,有正負表示其方向。結果為正,表示叉乘結果指向屏幕外;結果為負,表示叉乘結果指向屏幕里,可以使用右手定則來判定結果方向:右手四指環繞方向從a向b,大拇指方向則是其結果的方向,如果a和b平行,那么其結果為0;

回到圖二,多邊形ABCD和點E,將多邊形頂點按順時針順序排列好,從E點出發分別和各頂點連接成向量EA, EB, EC ,ED,放入一個數組,然后依次計算兩個相鄰向量的叉乘,最后的ED去和EA叉乘,結果如下:[EA ^ EB , EB ^ EC, EC ^ ED ,ED ^EA], 可以發現,每一個元素的正負符號依次是[0 , + , - , -]。0表示E和AB在同一條線上,正數表示E點在BC直線的另一側(相對于多邊形在BC那側而言),負數則表示在同一側。

可以判斷,叉乘結果的數組中:

1. 如果元素全部是負數,那么這個單點肯定在已知多邊形的內部;這個單點應該直接拋棄;
2. 如果只有一個零,其它都是負數,那么這個單點一定在對應的邊上;這個單點也應該直接拋棄;
3. 如果有兩個零,那這個點一定和原來的某個頂點重合;應該拋棄這個單點;
4. 如果有一個或者多個連續的正數或者0,那么這個單點一定在這幾條連續邊的外面,例如上面的EA ^ EB , EB ^EC是0 和 正數,那么對應的連續頂點A-B-C,應該保留起始點A和結束點C,中間的點B則應該刪除,然后將單點E插入到A和C中間;從而形成新的凸多邊形頂點列表AECD;
5. 如果只有一個單獨的正數叉乘結果,那就不需要刪除任何原來的頂點,而僅需要將新的單點插入即可。
6. 不可能出現多段分開的正數或零序列,比如[-1, -1, 0, 1, 1, 1, 0, -1] 是可能的,但是如下的[-1, -1, 0, 1, -1, 0, 1, -1]叉乘結果序列是絕不可能出現的。?

知道這個關鍵點之后,剩下的就好辦了,下面來看看代碼吧,用Ruby寫成。分別實現了上面提到的兩個算法,最后有用例:


童鞋們,你們考試用的什么算法呢?

?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容