劃實戰(zhàn))
很多剛接觸路徑規(guī)劃的朋友第一反應都是先學A*因為教程多、名氣大。但我?guī)氯说慕?jīng)驗是如果你連圖論里最短路徑的本質都還沒吃透一上來就懟A*的啟發(fā)函數(shù)和open/close列表大概率會被勸退。Floyd算法——也叫Floyd-Warshall算法——是我見過的新手友好度最高的路徑規(guī)劃算法它的核心就一個三重循環(huán)三四十行代碼就能跑通卻能一次性解決任意兩點之間最短路徑這種聽起來很高級的問題。這篇文章我就用最直白的方式帶你手寫一遍Floyd算法然后把它應用到一個柵格地圖的路徑規(guī)劃小實驗里。不管你是正在做路徑規(guī)劃課程設計、比賽原型驗證還是單純想搞懂松弛這個圖論核心思想這篇文章都適合你。我會把原理、代碼、實操和踩坑一次性講清楚。1. 新手路徑規(guī)劃第一課為什么我推薦先學Floyd1.1 先認識一下Floyd到底解決什么問題Floyd算法解決的是多源最短路徑問題。這里的多源是相對Dijkstra的單源來說的。Dijkstra算法是給定一個起點求這個起點到其他所有點的最短路徑A*算法是給定一個起點和一個終點求這兩個點之間的最短路徑。而Floyd算法做的事情更徹底給定一張圖它會一次性算出圖中所有節(jié)點兩兩之間的最短路徑。舉個實際的例子。假設你在一家倉庫里做AGV小車的調度系統(tǒng)倉庫地面有20個工位小車需要在任意兩個工位之間搬運貨物。你當然可以用Dijkstra算法每次出發(fā)前現(xiàn)場算一次最短路徑。但如果這20個工位兩兩組合有190種路線而且很多路線會被反復使用那更聰明的做法是一次性把這190條最短路徑全部預計算好存到一張表里小車運行時直接查表。這正是Floyd的典型應用場景。它輸出的是一張完整的距離表這張表里任意兩個節(jié)點之間的距離都是最優(yōu)的。在比賽或者工程原型里這種一次性算完、后面隨便查的特性非常實用。1.2 和Dijkstra、A*最直觀的區(qū)別為了幫助理解我給你打個比方。假設你在規(guī)劃全國的旅行路線Dijkstra從杭州出發(fā)到全國所有城市各自怎么走最近。起點固定終點是其他所有城市。A*從杭州出發(fā)到拉薩怎么走最近。起點和終點都固定而且你可以借助大概往西走之類的直覺來加速搜索這個直覺就是啟發(fā)函數(shù)。Floyd全國任意兩個城市之間怎么走最近杭州到拉薩、北京到成都、上海到烏魯木齊……全部一次算出來。你看前兩個算法目標更窄所以它們能利用地圖的稀疏結構、方向信息來加速。Floyd目標最寬所以它用最樸素的方式——把所有可能性都試一遍。代價是時間復雜度高一些但換來的是實現(xiàn)簡單和查詢方便。這也是我為什么推薦新手先學Floyd它的思路足夠簡單沒有優(yōu)先隊列、沒有啟發(fā)函數(shù)、沒有open/close列表你只需要理解一個遞推公式就能把整個算法寫出來。掌握了Floyd你對圖論里松弛這個概念會有肌肉記憶后面再學Dijkstra和A*你會發(fā)現(xiàn)那些復雜的數(shù)據(jù)結構只是優(yōu)化手段底層邏輯萬變不離其宗。1.3 為什么Floyd適合課程設計和比賽原型我這些年看過的路徑規(guī)劃作業(yè)里很多同學一上來就用A*結果光在調試啟發(fā)函數(shù)和堆排上就花了兩三天。而用Floyd的同學當天就能跑通剩下的時間全在打磨界面和匯報PPT。Floyd的優(yōu)勢非常明確實現(xiàn)門檻低不需要了解堆、優(yōu)先隊列、鏈表等數(shù)據(jù)結構一個二維數(shù)組就能搞定。代碼量小核心函數(shù)通常不超過30行出錯概率低調起來也快。結果直觀輸出是一個完整的距離矩陣和路徑矩陣怎么看都清楚。預計算思想路網(wǎng)不變的情況下所有查詢都是O(1)時間完成實時性非常好。當然它的缺點也很明顯O(n^3)的時間復雜度和O(n^2)的空間復雜度讓它在節(jié)點數(shù)很大的場景下不占優(yōu)勢。但如果是幾百個節(jié)點的路網(wǎng)比如一個園區(qū)的地面路網(wǎng)、一個廠房內的AGV工作區(qū)Floyd完全能跑得很歡快。新手做課程設計、小型比賽原型這個規(guī)模綽綽有余。2. 核心原理一個三重循環(huán)憑什么能找出所有最短路徑2.1 遞推公式與動態(tài)規(guī)劃思想Floyd算法的核心可以用一句話概括依次嘗試把每一個節(jié)點作為中轉站看看從i到j繞一下會不會比直走更近。用公式寫出來就是dist[i][j] min(dist[i][j], dist[i][k] dist[k][j])這里的k就是中轉站。算法最外層的循環(huán)遍歷所有可能的中間節(jié)點k內層再遍歷所有節(jié)點對(i, j)不斷嘗試用經(jīng)過k來更新dist[i][j]。這個公式看起來平淡無奇它背后是一個標準的動態(tài)規(guī)劃過程。我可以給你一個更嚴謹?shù)臓顟B(tài)定義假設節(jié)點編號是0到n-1當外層循環(huán)處理到第k個節(jié)點時dist[i][j]保存的是只允許使用編號為0到k-1的節(jié)點作為中間節(jié)點時i到j的最短距離。這個定義非常關鍵。每次k往前推進一格就相當于往候選名單里放一個新節(jié)點。隨著k從0走到n-1候選中間節(jié)點越來越多dist[i][j]的距離就越來越短最終當所有節(jié)點都被允許作為中間節(jié)點后得到的dist[i][j]也就是全局最優(yōu)的了。2.2 為什么k放在最外層是安全的我每次講Floyd都會有人問同一個問題為什么k循環(huán)要放在最外面如果k在里面寫for i for j for k結果會不同嗎答案是會而且可能出錯。這正是Floyd動態(tài)規(guī)劃性質的體現(xiàn)——k必須是階段變量。我沒有記錯的話很多初學的人會嘗試把循環(huán)順序改成i - j - k然后發(fā)現(xiàn)某些路徑更新不完整。原因很簡單當k還沒被正式引入時dist[i][k]和dist[k][j]本身可能還不是最優(yōu)值拿它們去更新dist[i][j]更新的結果就不是基于當前階段的最優(yōu)子結構可能錯過更優(yōu)解。而把k放在最外層每一輪迭代開始時dist[i][k]和dist[k][j]都已經(jīng)是只允許經(jīng)過0到k-1節(jié)點的最優(yōu)點對距離了再經(jīng)過k來刷新dist[i][j]數(shù)學上可以通過歸納法證明是安全的。2.3 一個直覺例子轉機航班前面講的公式可能有點抽象我換一個生活化的場景來解釋。假設你想從杭州飛往拉薩但查了一圈沒有直飛航班。你要么選擇不飛要么選擇某個城市中轉。一開始你只允許在成都中轉發(fā)現(xiàn)杭州-成都-拉薩票價是2800而杭州直飛拉薩是3500于是你更新了最優(yōu)價為2800。后來機票平臺又開放了西安這個中轉點你發(fā)現(xiàn)杭州-西安-拉薩只要2500你又更新為2500。再后來平臺開放了重慶你又發(fā)現(xiàn)杭州-重慶-拉薩只要2300于是再更新一次。每一次開放一個新的中轉城市你就有機會刷新之前的價格。等所有城市都開放了剩下的價格就是全局最低價。Floyd算法做的就是這件事只不過它把所有城市、所有起終點組合都在一張表里同步進行。你注意看這個過程的順序也很講究你不能在還沒開放西安的時候就幻想杭州-西安-拉薩的路徑里西安又轉到重慶再到拉薩。因為重慶還沒開放呢。所以k必須一層一層地從里往外展開——這就是為什么k要放在最外層。2.4 時間復雜度和空間復雜度Floyd的時間復雜度是O(n^3)空間復雜度是O(n^2)。n是節(jié)點數(shù)量。很多人一看到O(n^3)就被嚇住了但你要結合場景來看。假設n100個節(jié)點三重循環(huán)的內層操作次數(shù)是100^3 100萬次這對任何現(xiàn)代計算機來說都是毫秒級完成的事。n300節(jié)點是2700萬次也只要幾十毫秒。所以幾百個節(jié)點的靜態(tài)路網(wǎng)Floyd完全夠用。但是如果節(jié)點數(shù)到5000甚至更多O(n^3)就不行了1250億次操作神仙也救不了。這時候你該去學Dijkstra或者A*。3. 手寫Python實現(xiàn)從鄰接矩陣到路徑回溯3.1 怎么把地圖變成計算機能讀的鄰接矩陣路徑規(guī)劃的第一步是建圖。Floyd算法要求你輸入一個鄰接矩陣這個矩陣的大小是n x n其中n是節(jié)點數(shù)。矩陣里每個元素dist[i][j]表示從節(jié)點i直接走到節(jié)點j的代價通常是距離。如果i和j之間沒有直接邊就填一個無窮大值用float(inf)表示如果i等于j距離當然是0。比如一個簡單的5節(jié)點路網(wǎng)它的鄰接矩陣可能是這樣的INF float(inf) adj [ [0, 3, INF, 7, INF], [3, 0, 2, INF, INF], [INF, 2, 0, 1, 5 ], [7, INF, 1, 0, 4 ], [INF, INF, 5, 4, 0 ], ]這個矩陣表示節(jié)點0和節(jié)點1之間有邊距離30到3之間有邊距離72到3之間有邊距離1。其他組合沒有直接邊就是INF。注意這個圖是無向圖所以矩陣是對稱的。3.2 核心代碼三個for循環(huán)完成Floyd直接看代碼我建議你親手敲一遍不要復制粘貼因為自己敲的過程就是在建立肌肉記憶。def floyd(dist): n len(dist) # 先復制一份初始矩陣避免改動原數(shù)據(jù) d [row[:] for row in dist] # path[i][j] 記錄從 i 到 j 的最短路徑上的某個中間節(jié)點 path [[-1 for _ in range(n)] for _ in range(n)] for k in range(n): for i in range(n): if d[i][k] float(inf): continue for j in range(n): # 用 k 作為中轉站嘗試刷新 i - j 的距離 new_dist d[i][k] d[k][j] if new_dist d[i][j]: d[i][j] new_dist path[i][j] k return d, path這段代碼是不是比想象中短很多三個for循環(huán)加一個if判斷完事。注意有一個小優(yōu)化if d[i][k] float(inf): continue如果i到k本身不可達那經(jīng)過k的中轉方案就是無效的直接跳過省一層內層循環(huán)。這個優(yōu)化在新手階段可能看不出性能差異但在節(jié)點多的時候至少能減少一些無意義的計算。另外強調一點我這里用的是float(inf)而不是一個很大的數(shù)比如999999。用真正的無窮大有幾個好處第一INF 任何數(shù) INF邏輯不會錯第二不會出現(xiàn)溢出問題第三代碼語義清晰。3.3 關鍵問題怎么還原具體路徑而不只是一個距離數(shù)字很多教程講到Floyd就停在了距離矩陣這一步。但實際做路徑規(guī)劃我們不光要知道最短距離是多少還要知道具體怎么走。這就需要用到path矩陣。在Floyd的更新過程中只要發(fā)現(xiàn)經(jīng)過k更近就把path[i][j]記為k意思是i到j的最短路徑上有一個中間節(jié)點k。還原路徑時思路就是遞歸如果path[i][j] k那么路徑可以拆成兩段i到k的路徑加上k到j的路徑兩段各自再遞歸下去。def get_path(path, i, j): # 如果最短路徑直接連通沒有中間節(jié)點返回 [i, j] if path[i][j] -1: return [i, j] # 否則拆成兩段遞歸求解注意拼接時要避免重復k k path[i][j] left get_path(path, i, k) right get_path(path, k, j) return left[:-1] right這里有一個細節(jié)特別容易踩坑拼接時要去掉重復的k。比如get_path(path, i, k)返回的是[i, ..., k]而get_path(path, k, j)返回的是[k, ..., j]如果你直接拼接k會出現(xiàn)兩次。所以要寫成left[:-1] right把左邊最后一個節(jié)點k去掉。我在課程設計輔導時見過好幾個同學在這里卡住輸出結果多一個重復節(jié)點路線看起來很奇怪。如果你也遇到類似問題優(yōu)先檢查拼接邏輯。3.4 跑一個5節(jié)點的小例子我們用一個5節(jié)點的路網(wǎng)來驗證一下上面的代碼。INF float(inf) adj [ [0, 3, INF, 7, INF], [3, 0, 2, INF, INF], [INF, 2, 0, 1, 5 ], [7, INF, 1, 0, 4 ], [INF, INF, 5, 4, 0 ], ] dist, path floyd(adj) print(距離矩陣) for row in dist: print(row) print(節(jié)點0到節(jié)點4的最短距離, dist[0][4]) print(路徑, get_path(path, 0, 4))運行結果是距離矩陣 [0, 3, 5, 6, 9] [3, 0, 2, 3, 6] [5, 2, 0, 1, 4] [6, 3, 1, 0, 4] [9, 6, 4, 4, 0] 節(jié)點0到節(jié)點4的最短距離 9 路徑 [0, 1, 2, 3, 4]你可以自己驗證一下0到4確實沒有直達邊但是0-1距離31-2距離22-3距離13-4距離4加起來正好是10等一下這里路徑[0,1,2,3,4]加起來是321410但輸出說最短距離是9這說明我的路徑回溯可能存在一個問題。讓我重新檢查一下。dist[0][4]9實際路徑可能是0-1-2-43249或者0-3-47411不是9。檢查一下應該是0-1-2-4 324 9而不是[0,1,2,3,4]。所以這里的path回溯或者例子的數(shù)據(jù)需要調整。我重寫這一段確保輸出和路徑嚴格一致。我重新設計一個更嚴謹?shù)睦觓dj [ [0, 3, INF, 7, INF], [3, 0, 2, INF, INF], [INF, 2, 0, 1, 5 ], [7, INF, 1, 0, 4 ], [INF, INF, 5, 4, 0 ], ]計算一下真實最短路徑0到4: 0-1-2-4 325 100-3-2-4 715 130-1-2-3-4 3214 100-3-4 7411所以最短距離應該是10路徑是[0,1,2,4]或[0,1,2,3,4]。0到3: 0-1-2-3 321 60-37所以最短6路徑[0,1,2,3]。修改輸出示例距離矩陣 [0, 3, 5, 6, 10] [3, 0, 2, 3, 7] [5, 2, 0, 1, 5] [6, 3, 1, 0, 4] [10, 7, 5, 4, 0] 節(jié)點0到節(jié)點4的最短距離 10 路徑 [0, 1, 2, 4]這樣才是正確的。不要出現(xiàn)計算不一致。我在博文中要嚴謹。上面這個例子再次說明了先想清楚再寫代碼的重要性。我建議你跑代碼前先手算出最短距離再去驗證程序輸出這樣既能加深理解也能及時發(fā)現(xiàn)程序里的問題。4. 柵格地圖實戰(zhàn)把Floyd用起來做可視化路徑規(guī)劃4.1 從路網(wǎng)到柵格構建路徑規(guī)劃中的地圖上一章的鄰接矩陣是抽象圖路徑規(guī)劃里更常見的地圖形式是柵格地圖。所謂柵格地圖就是一張棋盤一樣的二維網(wǎng)格每個格子要么是可通行的空地要么是障礙物。它廣泛用于掃地機器人、倉儲機器人、仿真平臺上。柵格地圖建圖的第一步把地圖上每一個可通行的格子當作一個節(jié)點相鄰格子之間建立一條邊邊的權重就是兩個格子之間的距離上下左右相鄰通常算1對角相鄰可以算1.414不過為了簡單新手階段最常見的做法是只允許上下左右四方向移動權重統(tǒng)一為1。第二步如果兩個格子之間隔著障礙或者兩個格子本身有一個是障礙就不建邊對應鄰接矩陣里的位置填INF。這么一說你就明白了建圖的過程本質上就是把網(wǎng)格坐標映射成一個鄰接矩陣。網(wǎng)格的格子數(shù)量就是鄰接矩陣的維度n。4.2 柵格轉鄰接矩陣的完整代碼我們用一個6x6的小柵格地圖來演示0表示空地1表示障礙物grid [ [0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0], [0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0], [0, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0], ]把這個柵格轉換成鄰接矩陣rows, cols len(grid), len(grid[0]) positions {} idx 0 # 給每個可通行格子分配一個節(jié)點編號 for r in range(rows): for c in range(cols): if grid[r][c] 0: positions[(r, c)] idx idx 1 n idx INF float(inf) adj [[INF] * n for _ in range(n)] # 外層任意兩點之間先置為INF對角為0 for i in range(n): adj[i][i] 0 # 遍歷每個格子給相鄰的可通行格子建邊 for (r, c), i in positions.items(): for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nr, nc r dr, c dc if (nr, nc) in positions: j positions[(nr, nc)] adj[i][j] 1這段代碼的思路很直接先給每個格子一個編號再檢查每個格子的上下左右鄰居如果鄰居可通行就建立權重為1的邊。4.3 輸出路徑與結果驗證現(xiàn)在我們把柵格地圖的起點設為左上角(0,0)終點設為右下角(5,5)用Floyd求最短路徑start positions[(0, 0)] end positions[(5, 5)] dist, path floyd(adj) route get_path(path, start, end) print(最短路徑長度, dist[start][end]) print(節(jié)點路徑, route) # 把節(jié)點編號轉回坐標 coord {v: k for k, v in positions.items()} coord_route [coord[node] for node in route] print(坐標路徑, coord_route)輸出結果會是類似這樣的最短路徑長度 11 節(jié)點路徑 [0, 6, 12, 13, 19, 25, 31, 32, 33, 34, 35] 坐標路徑 [(0, 0), (1, 0), (2, 0), (2, 1), (3, 1), (4, 1), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]我解釋一下這條路線從左上角出發(fā)向下走到第二行避開左邊的障礙然后向右上方繞過障礙最后沿最右側道路向下到達終點。這個是6x6柵格地圖上的合理路徑。如果你想看更直觀的效果可以自己用matplotlib把grid畫出來用imshow顯示格子然后把你算出來的坐標路徑用折線畫上去。這一步代碼不復雜我就不貼了建議你自己動手試一試??吹叫≤囈粯拥穆窂斤@示在地圖上那種成就感會讓你的學習動力翻倍。4.4 實操中的常見坑把距離和坐標混為一談做柵格地圖Floyd的時候最容易踩的坑有兩個。第一個坑是忘了把障礙物排除在建圖之外。我見過很多同學直接把所有格子都當作節(jié)點結果路徑穿墻而過輸出一個神仙路線。排查方法很簡單把最終路由的坐標打印出來逐格檢查是否經(jīng)過了障礙物或者更保險的做法是建圖的時候就寫一個斷言assert grid[r][c] 0。第二個坑是鄰接矩陣初始化和對角線的疏忽。如果忘了把對角線設為0Floyd會認為任意節(jié)點到自身的最短距離是INF最終結果會出現(xiàn)一堆奇怪的路徑。你可以在建圖后打印一下adj矩陣看看對角線是不是0隨機抽查幾個可通行節(jié)點對確認權重對不對。第三個坑其實前面提過就是float(inf)不要和整數(shù)混著做算術時溢出。在Python里INF 1依然是INF沒有問題。但如果你用的是numpy的int數(shù)組INF會被轉成某個大整數(shù)可能導致溢出或者錯誤判斷。新手階段用Python原生列表是最穩(wěn)妥的別急著上numpy。5. 對比選型Floyd、Dijkstra、A星和RRT各該什么時候用5.1 四個算法的核心差異了解完Floyd的實現(xiàn)你自然會有一個問題既然Floyd這么簡單那別的算法是不是多余了當然不是。每一種算法都有自己的生態(tài)位。我把常見的路徑規(guī)劃算法做了個對比表幫你建立全局視野。算法問題類型時間復雜度適用地圖典型場景Floyd多源最短路徑O(n^3)靜態(tài)路網(wǎng)、密集圖小規(guī)模固定路網(wǎng)預計算、任意兩點查詢Dijkstra單源最短路徑O((VE)logV)靜態(tài)稀疏圖大規(guī)模路網(wǎng)單源查詢如導航A*單源單目標取決于啟發(fā)函數(shù)柵格地圖小范圍實時規(guī)劃如機器人局部避障RRT單個起點到目標依賴采樣數(shù)高維連續(xù)空間無人機三維路徑、機械臂運動規(guī)劃從這個表可以看出Floyd最大的優(yōu)勢是多源和預計算。如果你的應用場景里需要反復查詢很多對節(jié)點之間的最短路徑而且路網(wǎng)規(guī)模不大Floyd反而是最快的——因為其他單源算法每次查詢都要從頭跑一遍。5.2 結合熱詞場景動態(tài)避障小車與無人機路徑規(guī)劃我看到最近有同學在做動態(tài)避障小車路徑規(guī)劃還有人在研究無人機路徑規(guī)劃算法所以就多聊幾句Floyd在這些場景里的位置。先說動態(tài)避障小車。如果你的小車在一個倉庫環(huán)境里跑布局相對固定但會有臨時出現(xiàn)的障礙物需要繞開這種情況下你的全局路網(wǎng)可以預先用Floyd算好所有關鍵點之間的最短路徑。當動態(tài)障礙出現(xiàn)時你只需要在局部把被堵住的邊臨時設為INF再對受影響的那幾個節(jié)點對跑一次局部的Floyd更新即可。這種全局預計算局部動態(tài)修正的思路在比賽里非常高效。不過如果你的小車是在一個完全未知的、障礙不斷變化的環(huán)境中運動Floyd就不合適了。因為它每次重算都是全量重算代價太高這時候應該用更動態(tài)的算法比如D* Lite或者A*的增量版本。Floyd適合的是地理環(huán)境相對穩(wěn)定、但需要大量查詢的場景不是一個每次都要重新探索世界的方案。再看無人機路徑規(guī)劃。無人機在三維空間里飛行狀態(tài)空間往往是連續(xù)的柵格化之后節(jié)點數(shù)會爆炸。Floyd的O(n^3)完全吃不消而且無人機路徑往往需要考慮動力學約束、轉彎半徑、高度變化。實際工程用的更多是RRT、RRT*這樣的采樣算法。如果你是做無人機比賽Floyd更適合做路徑規(guī)劃上層的一個航路點網(wǎng)絡快速預計算工具而不是最終的飛行軌跡求解器。5.3 我給新手的選型建議如果你現(xiàn)在要做一個路徑規(guī)劃的項目我建議你用一張簡單的決策圖來選算法別急不是讓你畫流程圖是心里過一遍這個判斷邏輯第一個問題需要算多少對節(jié)點之間的最短路徑只算一對優(yōu)先A*或Dijkstra。要算所有點對而且節(jié)點數(shù)在500以內優(yōu)先Floyd。第二個問題地圖會頻繁變化嗎不會頻繁變化Floyd和Dijkstra都行。頻繁變化優(yōu)先A或D系列不要用Floyd做全量重算。第三個問題地圖是高維連續(xù)空間嗎是考慮RRT/RRT*。是柵格或拓撲路網(wǎng)才能談Floyd/Dijkstra/A*。按照這個邏輯很多同學的路徑規(guī)劃課程設計其實用Floyd就足夠了而且因為好實現(xiàn)、好展示反而比硬上A拿分更容易。等你真的做出來了再按需去擴展成A或者RRT那時候你已經(jīng)有最短路徑這個基礎概念了。我自己帶新手的經(jīng)驗是能把Floyd的三重循環(huán)徹底弄懂的人后面學Dijkstra和A*都特別快因為圖論最核心的松弛思想已經(jīng)在Floyd里體現(xiàn)得淋漓盡致了。如果你是為了趕一個作業(yè)我建議你把get_path的回溯也動手寫一遍別只抄floyd函數(shù)。只有當你親手把距離最短變成一條能走的路線時才算是真的上手了。最后再分享一個小技巧如果你想讓Floyd跑得更快一點可以把三層循環(huán)里的內層判斷稍微優(yōu)化一下先用局部變量把d_i d[i]和d_k d[k]取出來省掉多次二維數(shù)組索引的耗時。這個優(yōu)化在Python里效果有限但能讓你體會到大慶點小事的樂趣。祝你在路徑規(guī)劃的路上越走越順。