資料結構和演算法學習筆記九:最短路徑

movin2333 發表於 2021-07-14
演算法 資料結構

一.簡介

  在一個帶權值的圖中,從一個頂點走到另一個頂點經過的邊的權值之和最小稱為兩個頂點的最短路徑.最短路徑的找法有兩種常見的演算法,分別是迪傑斯特拉演算法和弗洛伊德演算法.本文中實現圖所用的程式碼見資料結構和演算法學習筆記六:圖的相關實現 - movin2333 - 部落格園 (cnblogs.com).

二.程式碼實現

/************************************
* 建立人:movin
* 建立時間:2021/7/6 21:39:43
* 版權所有:個人
***********************************/
using System;
using System.Collections.Generic;
using System.Text;

namespace GraphCore
{
    public class ShortestPathUtil
    {
        /// <summary>
        /// 最短路徑問題-迪傑斯特拉演算法
        /// 採用貪心策略,不可靠
        /// </summary>
        /// <param name="graph"></param>
        /// <param name="startIndex">起始頂點下標</param>
        /// <param name="paths">儲存最短路徑走法的陣列,下標的鏈式儲存</param>
        /// <returns>起始點到其他點的最短距離陣列</returns>
        public static int[] ShortestPath_Dijkstra(AdjacencyMatrixGraph graph,int startIndex,out int[] paths)
        {
            //陣列中沒有找過的頂點下標值對應臨時最短路徑,找過的頂點下標值對應最短路徑
            //最後返回的結果陣列
            int[] minDistance = new int[graph.Count];
            //儲存最短路徑的走法,如果要找到開始頂點到某個頂點的最短路徑,遍歷這個陣列即可
            paths = new int[graph.Count];
            //標識頂點是否已經在最短路徑中的標誌位
            bool[] isSearched = new bool[graph.Count];
            //初始化
            for (int i = 0; i < graph.Count; i++)
            {
                isSearched[i] = false;
                minDistance[i] = graph.adjacencyMatrix[startIndex, i];
            }
            //開始頂點到開始頂點距離為0,已經找過
            isSearched[startIndex] = true;
            paths[0] = startIndex;
            //當前正在找的頂點下標和用來找當前最短距離的臨時最小值
            int nowIndex = startIndex;
            int tempMin = int.MaxValue;
            
            //外迴圈,每次迴圈找到一個頂點
            for (int i = 1; i < graph.Count; i++)
            {
                tempMin = int.MaxValue;
                //一輪內迴圈,找到沒有找的頂點中的路徑的最小值和頂點下標
                for (int j = 0; j < graph.Count; j++)
                {
                    if(!isSearched[j] && minDistance[j] < tempMin)
                    {
                        nowIndex = j;
                        tempMin = minDistance[j];
                    }
                }
                //找到最小值後更新
                //校驗最小值頂點沒有更新,則說明已經找完了
                if (isSearched[nowIndex])
                {
                    break;
                }
                //更新找到的最小值
                isSearched[nowIndex] = true;
                paths[i] = nowIndex;
                //二輪內迴圈,更新所有的最小距離
                for(int j = 0;j < graph.Count; j++)
                {
                    //校驗頂點沒有找過且當前最小頂點到某頂點距離比當前儲存的最短距離短,則更新最短距離
                    if(!isSearched[j] && graph.adjacencyMatrix[nowIndex,j] != int.MaxValue && (tempMin + graph.adjacencyMatrix[nowIndex,j]) < minDistance[j])
                    {
                        minDistance[j] = tempMin + graph.adjacencyMatrix[nowIndex, j];
                    }
                }
            }

            return minDistance;
        }
        /// <summary>
        /// 最短路徑問題-弗洛伊德演算法
        /// 採用動態規劃策略
        /// </summary>
        /// <param name="graph"></param>
        /// <param name="paths">任意兩個頂點之間的最短路徑走法索引表</param>
        /// <returns>任意兩個頂點之間的最短路徑距離</returns>
        public static int[,] ShortestPath_Floyd(AdjacencyMatrixGraph graph,out int[,] paths)
        {
            //路徑陣列,儲存所有最短走法的路徑,其中每個位置儲存的都是下一個頂點的下標
            paths = new int[graph.Count, graph.Count];
            //距離陣列,儲存所有最短走法的距離
            int[,] distances = new int[graph.Count, graph.Count];
            //初始化兩個陣列
            for (int i = 0; i < graph.Count; i++)
            {
                for (int j = 0; j < graph.Count; j++)
                {
                    paths[i, j] = j;
                    distances[i, j] = graph.adjacencyMatrix[i, j];
                }
            }
            //三層迴圈,最外層i迴圈每迴圈一次是一次迭代
            for (int i = 0; i < graph.Count; i++)
            {
                //內部兩層迴圈是遍歷路徑陣列和距離陣列
                for (int j = 0; j < graph.Count; j++)
                {
                    for (int k = 0; k < graph.Count; k++)
                    {
                        //每次判斷經過頂點i的距離是否比直接走要短,如果更短則更新距離陣列和路徑陣列
                        if(distances[j,i] != int.MaxValue && distances[i,k] != int.MaxValue && distances[j,k] > distances[j,i] + distances[i, k])
                        {
                            //更新最短距離
                            distances[j, k] = distances[j, i] + distances[i, k];
                            //更新路徑,paths[j,k]儲存的是從j到k頂點路徑中j的下一個頂點
                            //這裡更新的意思是將從j到i的最短路徑走法中的j的下一個頂點下標賦值給從j到k的最短路徑走法中j的下一個頂點下標
                            //因為從j到k的最短路徑走法已經是先從j走到i再從i走到k,所以這樣賦值即可
                            paths[j, k] = paths[j, i];
                        }
                    }
                }
            }
            return distances;
        }
    }
}

三.總結:

  1.迪傑斯特拉演算法:這個演算法基於貪心的思想,節約了效能但是可靠性不高.這個演算法使用了兩層迴圈,時間複雜度O(n2).外層迴圈一次找到一個頂點到開始頂點的最短路徑,內層迴圈兩次,第一次找到沒有找的頂點中到開始頂點的最短距離,第二次重新整理所有沒有找的頂點到開始頂點的最短路徑.迪傑斯特拉演算法如果想要將所有兩個頂點間的最短路徑和走法都找到,就需要將每一個頂點作為起點代入演算法計算,那麼找到任意兩個頂點之間的最短路徑的時間複雜度就是O(n3).

   2.弗洛伊德演算法:這個演算法是基於動態規劃的思想,在計算的過程中有明顯的三層迴圈巢狀,顯然時間複雜度是O(n3).最外層迴圈用於作迭代,內部兩層迴圈用於遍歷記錄最短路徑走法和距離值的二維陣列,每次迭代都判斷從j到k的走法是直接從j到k距離更短還是從j到i再從i到k走更短,如果更短就更新最短路徑和走法(i,j,k是三層迴圈的臨時變數).這個演算法一次性將任意兩個頂點之間的最短路徑和走法都計算了出來,並且支援負權值.

  3.如果是計算任意兩個頂點之間的最短路徑及走法,兩種演算法的時間複雜度相同,推薦弗洛伊德演算法,因為它基於動態規劃的思想,計算結果更為可靠;但是弗洛伊德演算法只能一次性算出任意兩個頂點的最短路徑和距離,如果我們只需求某個頂點到其他頂點(或特定頂點)的最短路徑走法和距離,顯然使用迪傑斯特拉演算法更合適.