最近看了一些和圖形、演算法視覺化相關的文章和程式碼,挺有意思,於是自己也學著做了些東西。
迷宮生成演算法
迷宮小時候玩過,但從來沒琢磨過迷宮是怎麼設計的,以為就是有人慢慢畫出來的。看過網上這篇文章後,才知道,原來還可以隨機生成:
Maze Generation - Visualizing Algorithms
自己找了些資料參考,試著實現了幾種之後,才慢慢領會到其中的一些原理。
演算法中討論的迷宮滿足一個條件:迷宮中任意兩點間有且只有一條路徑。
要隨機生成滿足這樣條件的迷宮,看起來很複雜啊。但是換個思路之後,就發現問題沒那麼複雜了。
“樹”其實就滿足這個條件:
- 所有的枝葉都可以通過樹枝、樹幹連通
- 由於枝幹不交叉,沒有環,所以枝葉間連通的路徑是唯一的
所以,生成隨機的迷宮的問題,就轉化為生成隨機的樹的過程。進一步,可以拆分為以下過程:
- 在迷宮網格內隨機選擇一個點作為“樹根”
- 從樹根開始,向隨機選擇的某一方向開始生長
- 直到樹的枝幹通過不斷生長、分叉充滿迷宮的所有網格
迷宮生成的不同演算法,區別主要在兩點:
- 從一個位置開始生成後一直向隨機方向延伸的最大長度:有的是延伸一個網格後立即更換生長點,有的則是直到無法繼續延伸後才更換生長點
- 更換生長點時選擇位置的方式:有的是記錄當前枝幹經過的網格,依次後退,有的乾脆是完全隨機選擇一個有可能向外生長的點
深度優先演算法
深度優先演算法,也叫遞迴回溯演算法。它會一直向隨機方向生長,直到無法生成的位置,向後回退一格,繼續生長,直到所有網格被填充。
深度優先演算法生成的迷宮,會有比較明顯的長路徑,這是因為樹在一開始生成的時候,空間比較充裕,會有一些長的枝條產生。
Prim 演算法
Prim 演算法不會一直沿著一條路徑進行探索,而是不斷嘗試隨機的生長點。所以 Prim 演算法生成的迷宮,分叉會比較多:
演算法實現
綜合以上兩種演算法,我既不希望有過長的路徑,也不希望有太多的分叉,所以我採用的思路的嘗試沿著一條路徑延伸最多一定的長度,然後再隨機選擇生長點執行相同的過程。
下圖是在 40*40 的迷宮網格,每條枝幹最多生長15個網格的效果:
這是執行了一段時間之後,迷宮大部分割槽域已經走過:
這是最後的效果:
專案地址:luobotang/maze 線上DEMO:迷宮生成演算法 - luobotang
演算法視覺化
上面例子中的迷宮生成演算法過程,迷宮網格是通過 HTML 的
實現,相鄰網格的連通效果,則是藉助 CSS 的邊框樣式。整個迷宮的所有網格由二維陣列表示,每個網格的狀態包含是否被訪問、與相鄰網格的連通情況等。
演算法的執行過程由定時器驅動,每次執行一步,從而有動畫的效果。
最短路徑演算法
與圖相關的最短路徑演算法,在生活中應該是有著廣泛的應用了吧,從一個位置到另一個位置,藉助已有的路網,計算最短的路徑。當然,還會因為路況、臨時障礙,以及使用者的個人偏好而產生不同結果。
對於“圖”上,基本要素就是:
- 節點
- 邊:根據場景的不同,還會有方向、權重屬性
Dijkstra 演算法
Dijkstra 演算法是用於計算最短路徑的比較著名的一種演算法,早在1956年就發表了。
Dijkstra 演算法如果看演算法的詳細執行過程,有點複雜,但是其基本思路在做過之後會發現,貌似很簡單。
已確定 A 到 B 的最短路徑,B 與 C 相連,且 A 到 B 的距離加上 B 到 C 的距離,小於當前 A 到 C 的距離,那麼 A 到 B 再到 C 就是 A 到 C 的最短路徑。
如上圖所示,最初從 A 來看,到 C 的最短路徑是 A -> C,距離是 4。但繼續探索到 B 後,發現 A -> B 加上 B -> C 距離只有 3,比 A -> C 的距離要小,所以 A 到 C 的最短路徑更新為:A -> B -> C。
演算法實現
基本思路上面都介紹了,細節就是每次探索節點時,都選擇當前未探索過的到源點距離最短的節點,這樣可以源點到當前點的路徑已經是最短路徑。
演算法視覺化
圖的視覺化比較複雜了,只是繪製出來其實不難,但要將節點、邊進行合理佈局就比較麻煩,是另一個話題了。
我選擇用 vis.js 提供的 Network 來繪製圖形,然後通過逐步執行演算法來更新圖形。
這是初始狀態:
執行過程中,會記錄節點是否被訪問,以及當前的最短路徑和對應的距離:
全部執行完成後,就得到了源點開始到圖中所有節點的最短路徑:
專案地址:luobotang/graph DEMO:Dijkstra 演算法 - luobotang
專案的 Github Pages 配置有點問題,只能下載到本地之後再開啟頁面了。
結語
其實上面這些實現起來並沒有特別困難,有很多現成的資料和程式碼可以利用。但是不管什麼飯,都得自己吃過、消化過才是自己的。所以,我把自己吸收的“營養”記錄下來,如果你也有興趣,不妨自己上手一試。
最後,感謝閱讀!