人民广场怎么走?地铁换乘算法的实现
現(xiàn)在的公共交通越來(lái)越方便,很多城市都有地鐵,日常使用的地圖App都提供了地鐵線路換乘方案的功能,只要輸入起點(diǎn)和重點(diǎn),App就能給出你換乘的方案,可是這個(gè)功能背后的算法又是怎么樣的呢。這篇文章將會(huì)告訴你。
說(shuō)到最短路徑算法不外乎就是那么幾種,廣度優(yōu)先深度優(yōu)先Dijkstra之類的,這篇博客將會(huì)講述Dijkstra算法,其他的最短路徑算法我的其他文章也自己討論過(guò),在這里不過(guò)多說(shuō)了。寫這篇文章主要是因?yàn)槲铱雌渌年P(guān)于講Dijkstra算法的博客都停留在算法階段,代碼可以用,但是實(shí)用價(jià)值不多,那么這篇文章會(huì)直接帶你來(lái)實(shí)現(xiàn)一個(gè)上海地鐵換乘規(guī)劃算法。
數(shù)據(jù)獲取
文章中的所有代碼都上傳到了GitHub,代碼中有一個(gè)MetroRequester模塊會(huì)自動(dòng)連接上海地鐵的官網(wǎng)(http://www.shmetro.com/)來(lái)下載所有的站點(diǎn)信息。request_shanghai_metro_data()函數(shù)會(huì)返回一個(gè)StationManager和一個(gè)LineManager對(duì)象。
StationManager實(shí)例是用來(lái)獲取站點(diǎn)信息的,存放著上海地鐵的344個(gè)站點(diǎn)和每個(gè)站點(diǎn)的地鐵線路,比如 人民廣場(chǎng) 站有 1,2,8 號(hào)地鐵線。
LineManager對(duì)象存放的的是地鐵線路和站點(diǎn)之間的關(guān)系,也就是說(shuō)每條線路有什么站是存在這個(gè)對(duì)象中的。這個(gè)對(duì)象提供了一個(gè)計(jì)算兩個(gè)直達(dá)站點(diǎn)最短需要的站數(shù)的函數(shù),比如莘莊到徐家匯,函數(shù)會(huì)計(jì)算出來(lái)最短路徑為7站,因?yàn)榭梢宰?號(hào)線直達(dá),但是不能計(jì)算出非直達(dá)站點(diǎn)的最短站數(shù),比如莘莊到靜安寺,這兩站需要乘坐1號(hào)線并且換乘其他線路才能到達(dá)。
對(duì)于單次的路徑,我抽象了一個(gè)Route類,他的實(shí)例儲(chǔ)存著起始站,終點(diǎn)站,乘坐的線路,和站數(shù),比如下圖對(duì)應(yīng)的實(shí)例:代表從徐家匯乘坐1號(hào)線經(jīng)過(guò)5站到達(dá)人民廣場(chǎng)。
圖的二維數(shù)組展現(xiàn)
那么基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)已經(jīng)設(shè)計(jì)好了,那么現(xiàn)在來(lái)開始講算法吧,首先我們先把問(wèn)題簡(jiǎn)化一下吧。暫時(shí)只討論,徐家匯,陜西南路,漢中路,曲阜路四站,用到的地鐵線路只有1號(hào)線(紅色)和12號(hào)線(綠色)。
處理路徑問(wèn)題的時(shí)候用到的數(shù)據(jù)結(jié)構(gòu)為圖(Graph),那么我們來(lái)畫出這個(gè)圖,四個(gè)站分別為四個(gè)節(jié)點(diǎn),邊長(zhǎng)代表兩個(gè)節(jié)點(diǎn)之間的站的個(gè)數(shù)。
下圖中左側(cè)就是表達(dá)出來(lái)的圖,可以看到徐家匯是沒(méi)有辦法直答曲阜路的(徐家匯只有1號(hào)線,9號(hào)線,11號(hào)線2;而曲阜路只有8號(hào)線和12號(hào)線),必須通過(guò)換乘地鐵才可以。
并且,邊長(zhǎng)代表到達(dá)另一個(gè)節(jié)點(diǎn)的最短距離,也就是最少需要的站的個(gè)數(shù),在這四個(gè)站中,陜西南路到達(dá)漢中路有兩個(gè)直達(dá)方案:
-
直接乘坐12號(hào)線經(jīng)過(guò)南京西路到達(dá)漢中路
-
直接乘坐1號(hào)線經(jīng)過(guò)黃陂南路,人民廣場(chǎng)新閘路到達(dá)漢中路
我直接忽略了第二個(gè)方案,因?yàn)榈诙€(gè)方案不會(huì)有人去乘坐的,因?yàn)?2號(hào)線只要坐兩站就到而1號(hào)線要做4站。
通過(guò)左邊的圖,我們可以使用一個(gè)二維數(shù)組來(lái)表示其中的關(guān)系,右邊的表可以表示,行代表起始站,列代表終點(diǎn)站。比如第一排第三列代表徐家匯到漢中路要經(jīng)歷7站,第一排第四列代表徐家匯到曲阜路并沒(méi)有直達(dá)方案,所以是一個(gè)無(wú)限符號(hào),這張表一般被成為臨街矩陣,在程序中可以抽象為一個(gè)二維數(shù)組,我將他稱為v_matrix
Dijkstra
Dijkstra是一個(gè)最短路徑算法,他的核心就是邊的松弛
舉一個(gè)例子,現(xiàn)在我要計(jì)算出來(lái)徐家匯到曲阜路的最短路徑,那么首先我們要算出徐家匯到所有地方的最短路徑。那么首先把表格的第一列拿出來(lái)代表徐家匯為起點(diǎn)。在程序中第一列可以抽象為一個(gè)1維數(shù)組,我將他命名為dis數(shù)組,代表distance。
首先找到離徐家匯最近的一個(gè)頂點(diǎn),是陜西南路,那么徐家匯到陜西南路最短距離為3就已經(jīng)確認(rèn),因?yàn)殛兾髂下肥请x徐家匯最近的一個(gè)頂點(diǎn),所以兩點(diǎn)之間不可能存在一個(gè)比這3站還近的中專線路,畢竟兩點(diǎn)之間線段最短。
然后接下來(lái)就是漢中路頂點(diǎn)。我們不妨先看看陜西南路頂點(diǎn)都有哪些邊通向別的頂點(diǎn),陜西南路可以通往漢中路和徐家匯,呢么有沒(méi)有一種方案能夠通過(guò)陜西南路來(lái)縮短徐家匯直達(dá)漢中路的7站距離呢?
通過(guò)觀察鄰接矩陣v_matrix,有的,從徐家匯到陜西南路,再?gòu)年兾髂下返綕h中路只需要走5站,要優(yōu)于徐家匯直達(dá)漢中路的7站,在代碼層面這是在比較 dis[2] 與dis[1] + v_matrix[1][2]的大小
我們發(fā)現(xiàn),?dis[2] = 7 , 大于 dis[1] + v_matrix[1][2] = 5于是我們更新dis[2]為5,于是徐家匯到漢中路的最短距離確認(rèn)為5, 這個(gè)過(guò)程稱之為邊的松弛。
同理,我們可以通過(guò)判斷 dis[2] + v_matrix[2][3] = 6 ,小與代表曲阜路的dis[3] = 無(wú)窮大。將曲阜路的路徑縮短為6.
至此所徐家匯頂點(diǎn)到其余各頂點(diǎn)的最短路徑就求出來(lái)了。
更詳細(xì)一點(diǎn)的講解可以看這篇文章:
https://www.cnblogs.com/GnibChen/p/8875247.html
代碼的實(shí)現(xiàn)
上面就是基本的算法,可是如果dis數(shù)組和v_matrix鄰接矩陣中只有一個(gè)整數(shù)代表最短距離的話,這段代碼還是沒(méi)什么用的,我想知道徐家匯到曲阜路怎么走,如果像上面那樣編程的話程序只會(huì)告訴我最短距離為6站,沒(méi)有任何用途。
所以為了實(shí)現(xiàn)能夠讓程序記住換乘的路徑,我抽象出了一開始提到的Route對(duì)象,代表一個(gè)直達(dá)的路徑,在v_matrix數(shù)組中的每一個(gè)元素都是一個(gè)Route對(duì)象實(shí)例,當(dāng)可以直達(dá)的時(shí)候,這個(gè)實(shí)例的stops為最短的站數(shù),當(dāng)不可以直達(dá)的時(shí)候stops為9999代表無(wú)限大。
并且dis數(shù)組也不是只保存了松弛過(guò)后邊的長(zhǎng)度,而是保存了一個(gè)鍵值對(duì),key為最短邊的長(zhǎng)度,value為一個(gè)數(shù)組,儲(chǔ)存著若干個(gè)Route實(shí)例,遍歷這個(gè)數(shù)組即可得到所有的換乘方案。比如下圖的結(jié)構(gòu)代表從徐家匯到上海科技館的換乘方案,要經(jīng)歷10站,先做11號(hào)線到江蘇路,再?gòu)慕K路換乘2號(hào)線到上海科技館。
其余的代碼直接在Github上看,不做多余的講解了。
優(yōu)化
上面其實(shí)就是Dijkstra的核心了,不過(guò),要是憑著上面的講解真的能夠?qū)懗鲎銐騼?yōu)秀的地鐵換乘規(guī)劃算法嗎?
答案是否定的,上面講解到通過(guò)松弛將徐家匯到漢中路的站數(shù)縮短到了5站,代價(jià)是換乘一次,本來(lái)徐家匯是可以乘坐1號(hào)線直達(dá)漢中路的,只是多了兩站而已,但是我們的算法卻偏偏選擇了換乘。在真實(shí)生活中,我是不會(huì)去為了少兩站換乘的,畢竟換乘還要等下一班地鐵還要走很多路,陜西南路1號(hào)線換乘10號(hào)線或者12號(hào)線可有你走的了。
同樣再試一下莘莊到漢中路,這兩個(gè)站是可以通過(guò)一號(hào)線直達(dá)的。跑一下程序:
可以看到算法并沒(méi)有給出一號(hào)線直達(dá)的方案,而是選擇了換乘兩次,所以這樣算出來(lái)的方案非常不切實(shí)際,歸根究底,我們沒(méi)有考慮到換乘的巨大代價(jià)。
所以我引入了一個(gè)偏執(zhí)值bias來(lái)表示換乘的代價(jià),我將他設(shè)置為3,代表?yè)Q乘一次和坐三站花費(fèi)的時(shí)間等價(jià)。
回到徐家匯到漢中路的問(wèn)題,在上文我是這么寫的:
dis[2] = 7 , 大于 dis[1] + v_matrix[1][2] = 5于是我們更新dis[2]為5
這次計(jì)算我們引入bias為3
dis[2] = 7?;?dis[1] + v_matrix[1][2] + bias = 8, 可以看到在比較的時(shí)候引入一個(gè)bias值導(dǎo)致這次計(jì)算出來(lái)直達(dá)的7站是一個(gè)最優(yōu)方案,因?yàn)閾Q乘的邊長(zhǎng)由5變成了8。
通過(guò)這個(gè)右滑,來(lái)再看看莘莊到漢中路的方案:
這一次算法給出了合理的最優(yōu)方案。
當(dāng)然這樣可能也不太完美,因?yàn)閷?duì)于頂點(diǎn)之間的邊長(zhǎng),我僅僅是使用了站點(diǎn)數(shù)來(lái)表示,如果用真實(shí)距離來(lái)表示會(huì)更加精準(zhǔn),或者用不同的站到不同的站的經(jīng)歷時(shí)間來(lái)表示長(zhǎng)短也是不錯(cuò)的選擇。
尾巴
那么換乘算法已經(jīng)有了,你有沒(méi)有想過(guò)地圖App是怎么確定你周圍最近的地鐵站的呢?沒(méi)有想法的同學(xué)可以看我?guī)啄昵皩戇^(guò)的博客:周圍的餐館有哪些?GeoHash算法
這個(gè)項(xiàng)目的Github地址:?https://github.com/Yigang0622/Metro-Transfer-Algorithm
總結(jié)
以上是生活随笔為你收集整理的人民广场怎么走?地铁换乘算法的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 年底了,如何准备 Java 初级和高级的
- 下一篇: 周围的餐馆有哪些?GeoHash算法