維諾圖(Voronoi Diagram),簡單來講,是一種平面區域的劃分方式。假設平面上有 n 個點:P1 ~ Pn,那麼對應維諾圖則劃分紅 n 個區域:S1 ~ Sn,而且 Si 內全部點到 Pi 的距離小於等於到其餘任意點的距離。維諾圖還常常和德洛內三角(Delaunay 三角網)扯上關係,德洛內三角是一系列相連不重疊的三角形集合,特色有兩個:一、任意三角形的外接圓不包含面內其餘三角形頂點,二、相鄰兩個三角形構成的凸四邊形,交換對角線,六個內角的最小角不會增大。以下圖,實線構成德洛內三角,虛線構成維諾圖,德洛內三角每個三角形的頂點即是維諾圖的初始點集。html
理論上,兩種圖形能夠互相轉化。一、生成維諾圖後,鏈接有公共邊的初始點集,即可構成德洛內三角。二、生成德洛內三角後,針對每個三角形邊,生成垂直平分線,並將垂直平分線的端點設置爲三角形邊所在外接圓的圓心便可(內部三角形邊的垂直平分線爲線段,邊界三角形邊的垂直平分線爲射線)。git
下面先推薦幾篇我看過寫得比較好的學習資料,能夠用於參考。(吐槽:這幾篇是我從海量博客中篩選出來的,網上能搜出一堆相關博客,可真正有內容的卻沒有幾篇)github
一、http://www.cnblogs.com/zhiyishou/p/4430017.html 講解如何生成Delaunay三角網的博客。這篇文章是講解地比較細,比較全,比較容易理解的,不過一樣有不少坑,閱讀時不要遺漏了博客評論區,那裏指出了不少坑點。最坑的一點,即便你排除萬難,寫出了和做者如出一轍的代碼,仍然有不少BUG,好比點數少的狀況下有可能沒有任何生成,好比最終生成的德洛內三角不是凸包等。固然,文章確實好,值得一看,用來理解德洛內三角是頗有幫助的。算法
二、https://en.wikipedia.org/wiki/Fortune%27s_algorithm 講解如何生成維諾圖的維基百科,有個gif 圖能夠幫助理解,中間那段英文的算法描述寫得很好,讀下來大體就有了生成維諾圖的思路(英文很差的能夠百度翻譯一下)。下面還附了個僞代碼,不過這僞代碼我是徹底看不懂了,百度翻譯也無論用了。文章末尾還附加了幾個算法源碼,不過不推薦閱讀,代碼可讀性太差了,反正我是啃不下來,後面會推薦一個寫得比較清楚的源碼。數據結構
三、http://www.javashuo.com/article/p-gzbexjrb-bd.html 講解如何生成維諾圖的博客,內容比較少,不過比較清晰,附加的僞代碼也比較容易理解,建議有了大體思路後根據這篇博客來完善代碼。學習
四、https://www.cs.hmc.edu/~mbrubeck/voronoi.html 提供源碼的博客,其餘不少博客都只是簡單介紹方法,而像codeproject 或Wikipedia 上的源碼可讀性太差(不是我吐槽,是真的太差,徹底看不懂),而這篇博客提供的代碼很適合用來學習,定義清晰明瞭,雖然代碼效率達不到logn,但這僅僅是存放海岸線的數據結構差別,其他內容與平面掃描法一致,能夠參照該源碼去解讀推薦的第三篇博客。不過該源碼也有些BUG,有一些特殊狀況會返回不正確的維諾圖。spa
下面就介紹經過平面掃描法來生成維諾圖,首先介紹平面掃描法的幾個基礎定義:翻譯
一、掃描線:掃描線將從 y = 0 一直掃描到 y = maxY,掃描完成後,維諾圖也將生成完畢。(上圖的黑色線)指針
二、海岸線:由多段拋物線組成,拋物線的焦點是初始點集,準線爲掃描線。(上圖的藍色線)code
三、站點事件:掃描線掃描到了某個初始點。
四、圓事件:掃描線掃描到了某個圓(三個站點共圓)的最低點。
算法思路:
咱們須要維護一條掃描線和一條海岸線,這兩條線都隨着程序的運行,經過整個平面。掃描線是一條直線,咱們能夠假定它是水平的,在平面上從上到下地移動。在算法運行期間,掃描線上方的初始點已經被歸入Voronoi圖,而掃描線下方的點暫未考慮。海灘線不是一條直線,而是一條複雜的多段曲線,位於掃描線的上方,它將已經肯定的區域和未肯定的區域分割開,即無論後續還有多少個點,海岸線上方的維諾圖都已是肯定的了。對於掃描線上方的初始點,咱們能夠定義距離該點和掃描線等距的點的曲線(即以該點爲焦點,以掃描線爲準線的拋物線),海岸線就是這些拋物線並集的邊界。隨着掃描線的推動,海岸線中相鄰拋物線交點(相交的點)將勾勒出維諾圖的邊。海岸線也隨着掃描線的推動而推動,始終保持其上的點到焦點的距離和到掃描線的距離相等。
該算法使用了排序二叉樹來維護海岸線,使用優先隊列來維護可能引發海岸線變化的事件。這些事件包括新增拋物線到海岸線(掃描過一個初始點,稱之爲站點事件)和從海岸線中刪除某一條拋物線(這條拋物線縮小成一個點時,即海岸線中相鄰的三個拋物線焦點生成的圓與掃描線相切,稱之爲圓事件)。每個事件能夠用發生該事件時的掃描線 y 座標來肯定優先級。而後,咱們要作的就是反覆從優先隊列中取出事件,進行處理,可能會影響到海岸線結構,可能會新增圓事件。
因此,該算法的重點即是如何處理站點事件和圓事件。首先看站點事件,當掃描線遇到 P4 時,過 P4 作掃描線的垂線,垂線和海岸線相交點到 P4 和 P2 距離相等,當掃描線越過 P4 時,將生成一條以 P4 爲焦點,掃描線爲準線的拋物線,該拋物線和 P2 對應的海岸線相交於兩點,這兩點會隨着掃描線的移動而分離,事實上,這兩點將勾勒出同一條維諾圖邊(能夠肯定該邊上點到 P4 和 P2 距離相等)。P4 拋物線與 P2 拋物線交於兩點,會將 P2 拋物線分割成兩段拋物線,命名爲S一、S2,假設 P4 下方沒有新的站點,隨着掃描線繼續移動,P4 對應拋物線將越變越寬,可能會將原先 S一、S2 擠兌沒,即 S1 可能由一段拋物線縮小成一個點,而 P1 拋物線和 S1 的交點,與 P4 拋物線和 S1 的交點重合,這便產生了一個圓事件,即縮小成的那一點到 P一、P二、P4 距離相等。固然程序不可能作到一點一點的移動掃描線,因此生成圓事件,是在遇到 P4 的一瞬間決定的,即遇到 P4 時,判斷 P一、P二、P4 是否共圓。接着咱們來看圓事件,當發生了圓事件後,就說明有某一段拋物線縮小成一個點,因此咱們就須要將該段拋物線刪除,並生成已經肯定下來的維諾圖邊。
維諾圖的目標是找出全部站點對應區域的邊,而邊是隨着掃描線移動,由相鄰拋物線交點勾勒出來的,因此咱們把邊記在弧上,一條弧左右各有一個交點,因此咱們每條弧記兩條邊S0、S1。當發生站點事件時,先找到其正上方的弧,而後這條弧中間部分將被新的拋物線取代,即由一段弧變成兩個交點Inter一、Inter2 和 三段弧Arc一、Arc二、Arc3,其中Arc2 是新增的弧,Arc一、Arc3 是原弧分裂出來的,因此Arc1 的S0 繼承自原弧的S0,Arc3 的S1 繼承自原弧的S1,Arc1 的S1 與 Arc2 的S0 將指向根據左交點建立的一條新邊,Arc2 的S1 和Arc3 的S0 將指向根據右交點建立的一條新邊。當發生圓事件時,弧Arc 消失,因此 Arc 的S0、S1 將完成構造,即S0、S1 的終點設置在圓事件的圓心,而 Arc 消失後,其前置弧和後置弧相交在一塊兒,因此前置弧的S1 和後置弧的S0 將指向根據圓心建立的一條新邊。當全部事件處理完畢後,咱們須要假設一條掃描線,使剩餘的全部相鄰弧交點位於維諾圖邊界外,計算此時這些交點的位置,完成剩餘的維諾圖邊。
數據結構:
一、咱們須要按 y 順序遍歷站點和圓事件,因此引入優先隊列這個數據結構(Y 越小越靠前,Y 相同則 X 越小越靠前)。
二、咱們須要快速獲取某點正上方的拋物線,因此引入排序二叉樹,排序二叉樹每個葉子結點表明一段拋物線,每個內部結點表明相鄰拋物線的交點,排序依據就是交點的 x 座標,從而能夠用logn 的時間,快速獲取某點正上方的拋物線。(排序二叉樹可能會退化成鏈表,真正使用的時候能夠考慮是否能夠替換成平衡二叉樹)
三、咱們須要快速檢查相鄰的三段拋物線對應焦點是否共圓,因此引入雙向鏈表,用於管理排序二叉樹的葉子結點,即每一個葉子結點記錄上一片葉子和下一片葉子指針。
源碼連接:僞代碼能夠看上面的第三篇博客,或者直接閱讀下面的源代碼,註釋應該是比較全了。該源碼僅用於交流學習,內部有挺多細節沒有考慮最優的算法。
https://github.com/hchlqlz/VoronoiDiagram
歡迎你們指出源碼 BUG。