您好,登錄后才能下訂單哦!
這篇文章主要介紹了PHP數據結構-圖的應用:最短路徑的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
什么是最短路徑
今天我們學習的是圖的應用中另外一個經典的問題,也就是 最短路徑 的問題。這個問題和最小生成樹是不同的,最小生成樹的要求是要連通所有的結點,并且走得是權值最小的那條路線。而最短路徑則是指的從某個頂點到另一個頂點中權值最小的那條路徑。這條路徑不一定是包含在最小生成樹中的,所以它們并沒有太大的聯系。
從這張圖來看,我們從結點 1 到結點 2 的最短路徑是 2 ,這個很明顯。那么從結點 1 到結點 3 呢?可不是直接的中間那個權值為 6 的路徑,而是走 1->2->3 這條路徑,也就是權值加起來為 5 的這條路徑。
然后我們再來看結點 3 ,它到結點 1 最短路徑應該是走 3->4->1 這條路徑,也就是權值為 6 的這條路徑,而不是中間的那條直線的權值為 7 的路徑。
沒錯,這就是最短路徑的概念了。在最短路徑中,我們一般會解決單向圖的問題,但實際生活中呢?最典型的地圖相關的應用其實是都是雙向圖的。不過這并不影響我們的學習,我們可以把這個示例圖中的結點看成是城市火車站點,就算是連接結點 1 和結點 3 的火車線路,也不一定來去的時間都是相同的。比如說從長沙到北京的 Z2 次火車全部運行時間為14小時42分,而回來的 Z1 次則是14小時10分。那么我們是否可以選擇其它的火車,比如有趟火車從長沙到石家莊可能只需要8小時,然后從石家莊到北京只需要2小時,這樣我們選擇這條線路的總時間就只需要10小時了(當然,這只是例子,大家在非高鐵的情況下肯定還是更多地會選擇起始站的火車來坐)。
多源最短路徑 Floyd 算法
首先,我們先說一個多源最短路徑的算法。那么什么叫做多源呢?
其實就是這一個算法就能夠得出所有結點到所有結點之間的最短路徑。沒錯,就這一個算法,不管哪個結點到哪個結點,它們之間的最短路徑都一次性算出來了。神奇嗎?不不不,更神奇的,而且你一會就會叫出 Oh!My God! 的是它的核心代碼,只有五行!!
function Floyd($graphArr){ $n = count($graphArr); for($k = 1;$k<=$n;$k++){ // 設 k 為經過的結點 for($i = 1;$i<=$n;$i++){ for($j = 1;$j<=$n;$j++){ // 如果經過 k 結點 能使 i 到 j 的路徑變短,那么將 i 到 j 之間的更新為通過 k 中轉之后的結果 if($graphArr[$i][$j] > $graphArr[$i][$k] + $graphArr[$k][$j]){ $graphArr[$i][$j] = $graphArr[$i][$k] + $graphArr[$k][$j]; } } } } for($i = 1;$i<=$n;$i++){ for($j = 1;$j<=$n;$j++){ echo $graphArr[$i][$j], ' '; } echo PHP_EOL; } } // 請輸入結點數:4 // 請輸入邊數:8 // 請輸入邊,格式為 出 入 權:1 2 2 // 請輸入邊,格式為 出 入 權:1 3 6 // 請輸入邊,格式為 出 入 權:1 4 4 // 請輸入邊,格式為 出 入 權:2 3 3 // 請輸入邊,格式為 出 入 權:3 1 7 // 請輸入邊,格式為 出 入 權:3 4 1 // 請輸入邊,格式為 出 入 權:4 1 5 // 請輸入邊,格式為 出 入 權:4 3 12 // 0 2 5 4 // 9 0 3 4 // 6 8 0 1 // 5 7 10 0
我們可以先驗證下結果,就是注釋中最后輸出的矩陣。結點 1 到結點 2、3、4的最短距離為 2 、5 、4 。結點 3 到結點 1 、2 、4 的最短距離為 6 、8 、1 。也就是說,原來的那個圖的鄰接矩陣成了這個最短路徑的矩陣。每一行代表每個結點到其它結點的最短距離。
好吧,結果沒問題,那么代碼到底是寫得啥玩意?這個 k 是什么?別急,我們一步一步來看。
假設兩點之間的距離不是最短的,那么肯定是有另外一個點做為媒介進行跳轉,由 i 點先跳到這個點然后再跳向 j 點,這樣的一條路徑是比直接的 i 到 j 要近的,我們就定義這個點為 k 點
但是我們不知道要走哪個結點呀,而且還有可能不只是一個 k ,或許我們從 i 到 j 要經歷好多個 k ,這時候,我們就從 k 開始遍歷,也就是第一層循環
在第一層循環下,進行我們正常的 i 和 j 的遍歷循環,獲得 i 直接到 j 的長度,也就是 [i][j] 。這時,由于有最外層的 k 存在,所以我們也知道了如果 i 從 k 走再從 k 到 j 的長度,也就是 [i][k] + [k][i] 這段距離
很明顯,如果 [i][k] + [k][i] 的距離要比 [i][j] 短的話,更新 [i][j] 的值為 [i][k] + [k][i]
內部的 i 和 j 循環完成后,第 1 個結點做為媒介跳轉的遍歷也完成了,當前的矩陣中各個結點之間的權重已經是經過第 1 個結點做為媒介之后的最短路徑了
但是呢,這并不準確,說不定我們可能經過 i 、k1 、 k2 、 j 的路徑才是最短的,所以外層的 k 循環繼續遍歷并將第 2 個結點作為媒介結點
循環往復直到所有結點都做過一次中間的媒介結點之后,我們就得到了一個最短路徑的矩陣圖,也就是我們上面測試代碼中輸出的結果
我們就拿結點 4 和結點 3 來說明。我們定義 4 為 i ,結點 3 為 j 。
初始化時,[i][j] 為 12 ,這個沒什么問題,單向圖的那條帶箭頭的邊的權值就是 12 。
然后當 k 為 1 時,也就是我們經過結點 1 來看路徑有沒有變短,這時 [i][k] 是 5 ,[k][j] 是 6 ,OK,路徑變成 11 了,把 [i][j] 的值改成 11 。
同時,在 i 為 4 ,j 為 2 的情況下,他們兩個的最短路徑也在這次 k=1 的循環中被賦值為 7 。最開始 4 到 2 是沒有直接的邊的,現在在結點 1 的連接下,他們有了路徑,也就是 [4][2] = [4][1] + [1][2] = 7 。
當 k 為 2 時,[i][j] 為 11 ,這時 [i][k] 就是上面說過的 [4][2] 。也就是 7 ,而 [k][j] 則是 3 ,路徑又縮小了,[i][k] + [k][j] = 10 ,[i][j] 現在又變成了 10 。
循環繼續,但已經沒有比這條路徑更小的值了,所以最后 [4][2] 的最短路徑就是 10 。
看著暈嗎?拿出筆來在紙上或者本子上自己畫畫,每一步的 k 都去畫一下當前的最短路徑矩陣變成什么樣了。這樣畫一次之后,馬上就知道這個 Floyd 算法的核心奧秘所在了。
不得不說,前人的智慧真的很偉大吧,不過說是前人,其實 Floyd 大佬在 1962 年才發表了這個算法,但這個算法的核心思想卻是數學中的動態規劃的思想。所以說,算法和數學是沒法分家的,各位大佬哪個不是數學界的一把手呢。
單源最短路徑 Dijkstra 算法
說完了多源最短路徑,我們再講一個鼎鼎大名的單源最短路徑的算法。雖說上面的多源很牛X,但是它的時間復雜度也就是時間效率也確實是太差了,沒看錯的話三個 N 次的循環嵌套就是 O(N3)。如果數據稍微多一點的話基本就可以從 Oh!My God! 變成 Oh!FxxK! 了。而且大多數情況下,我們的需求都會是固定的求從某一點到另一點的最短路徑問題,也就是單源最短路徑問題。這時,就可以使用這種效率稍微好一點的算法來快速地解決了。
// origin 表示源點,也就是我們要看哪個結點到其它結點的最短路徑 function Dijkstra($graphArr, $origin) { $n = count($graphArr); $dis = []; // 記錄最小值 $book = []; // 記錄結點是否訪問過 // 初始化源點到每個點的權值 for ($i = 1; $i <= $n; $i++) { $dis[$i] = $graphArr[$origin][$i]; // 源點到其它點的默認權值 $book[$i] = 0; // 所有結點都沒訪問過 } $book[$origin] = 1; // 源點自身標記為已訪問 // 核心算法 for ($i = 1; $i <= $n - 1; $i++) { $min = INFINITY; // 找到離目標結點最近的結點 for ($j = 1; $j <= $n; $j++) { // 如果結點沒有被訪問過,并且當前結點的權值小于 min 值 if ($book[$j] == 0 && $dis[$j] < $min) { $min = $dis[$j]; // min 修改為當前這個節點的路徑值 $u = $j; // 變量 u 變為當前這個結點 } // 遍歷完所有結點,u 就是最近的那個頂點 } $book[$u] = 1; // 標記 u 為已訪問 for ($v = 1; $v <= $n; $v++) { // 如果 [u][v] 頂點小于無窮 if ($graphArr[$u][$v] < INFINITY) { // 如果當前 dis[v] 中的權值大于 dis[u]+g[u][v] if ($dis[$v] > $dis[$u] + $graphArr[$u][$v]) { // 將當前的 dis[v] 賦值為 dis[u]+g[u][v] $dis[$v] = $dis[$u] + $graphArr[$u][$v]; } } } // 最近的結點完成,繼續下一個最近的結點 } for ($i = 1; $i <= $n; $i++) { echo $dis[$i], PHP_EOL; } } // 請輸入結點數:4 // 請輸入邊數:8 // 請輸入邊,格式為 出 入 權:1 2 2 // 請輸入邊,格式為 出 入 權:1 3 6 // 請輸入邊,格式為 出 入 權:1 4 4 // 請輸入邊,格式為 出 入 權:2 3 3 // 請輸入邊,格式為 出 入 權:3 1 7 // 請輸入邊,格式為 出 入 權:3 4 1 // 請輸入邊,格式為 出 入 權:4 1 5 // 請輸入邊,格式為 出 入 權:4 3 12 // 測試第四個結點到其它結點的最短路徑 Dijkstra($graphArr, 4); // 5 // 7 // 10 // 0
代碼一下增加了不少吧,不過仔細看一下核心的算法部分,這次只是兩層循環的嵌套了,時間復雜度一下子就降到了 O(N2) ,這一下就比 Floyd 算法提升了很多。當然,它的場景也是有限的,那就是只能一個結點一個結點的計算。
好了,我們還是來看一下 Dijkstra 到底在干嘛吧。我們依然是使用上面那個簡單的圖,并且還是研究結點 4 到其它結點的算法執行情況。
首先,我們初始化結點 4 到其他所有結點的默認值,這時結點 4 到結點 2 是沒有直接路徑的,所以是無窮大,而到結點 1 是 5,到結點 3 是 12 。
然后將結點 4 標記為已訪問,也就是 book[4] = 1
進入核心算法,從頭開始遍歷結點,這里是標記為 i 下標,因為這里是單源的最短路徑,所以我們不需要再看自己到自己的最短路徑了,只需要 n-1 次循環就可以了
開始 j 層的循環,先判斷當前的結點是否已經被標記過,沒有被標記過的話再看它的值是否是最小的,最后循環完成后獲得一個從結點 4 出發的權值最小的路徑,并將這條路徑到達的結點下標記為 u ,標記 u 下標的這個結點為已訪問結點
進入 v 循環,判斷圖中 u 到 v 的結點是否是無窮,如果不是的話再判斷 u 到 v 的結點加上原來的 dis[u] 的權值是否小于 dis[v] 中記錄的權值,如果比這個小的話,更新 dis[v] 為 u 到 v 的結點加上原來的 dis[u] 的權值
循環重復地進行比較完成算法
對于結點 4 來說,dis 經歷了如下的變化:
首先,默認情況下 dis = [5, 9999999, 12, 0]
第一次循環后,結點1 完成查找,并在 v 的循環中發現了可以從結點1 到結點2 和結點3 而且比原來的值都要小 ,于是 dis = [5, 7, 11, 0]
第二次循環后,結點2 完成查找,這次循環發現從結點2 到結點3 的距離更短,于是 dis = [5, 7, 10, 0]
第三次循環后,結點3 完成查找,沒有發現更短的路徑,dis = [5, 7, 10, 0]
看明白了嗎?不明白的話自己試試吧,不管是斷點還是在中間輸出一下 dis 和 book ,都能夠幫助我們更好地理解這個算法的每一步是如何執行的。從代碼中就可以看出來,這個 Dijkstra 算法的時間復雜度是 O(N2) ,這可比 Floyd 快了不少了吧。
感謝你能夠認真閱讀完這篇文章,希望小編分享的“PHP數據結構-圖的應用:最短路徑的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。