貪心法之單源最短路徑問題


  1、問題描述

     給定帶權有向圖G =(V,E),其中每條邊的權是非負實數。另外,還給定V中的一個頂點,稱為源。現在要計算從源到所有其他各頂點的最短路長度。這里路的長度是指路上各邊權之和。這個問題通常稱為單源最短路徑問題

    2、Dijkstra算法

     Dijkstra算法是解單源最短路徑問題的貪心算法。
    其基本思想是,設置頂點集合S並不斷地作貪心選擇來擴充這個集合。一個頂點屬於集合S當且僅當從源到該頂點的最短路徑長度已知。初始時,S中僅含有源。設u是G的某一個頂點,把從源到u且中間只經過S中頂點的路稱為從源到u的特殊路徑,並用數組dist記錄當前每個頂點所對應的最短特殊路徑長度。Dijkstra算法每次從V-S中取出具有最短特殊路長度的頂點u,將u添加到S中,同時對數組dist作必要的修改。一旦S包含了所有V中頂點,dist就記錄了從源到所有其他頂點之間的最短路徑長度。

 偽代碼如下:

    Dijkstra算法可描述如下,其中輸入帶權有向圖是G=(V,E),V={1,2,…,n},頂點v是源。c是一個二維數組,c[i][j]表示邊(i,j)的權。當(i,j)不屬於E時,c[i][j]是一個大數。dist[i]表示當前從源到頂點i的最短特殊路徑長度。在Dijkstra算法中做貪心選擇時,實際上是考慮當S添加u之后,可能出現一條到頂點的新的特殊路,如果這條新特殊路是先經過老的S到達頂點u,然后從u經過一條邊直接到達頂點i,則這種路的最短長度是dist[u]+c[u][i]。如果dist[u]+c[u][i]<dist[i],則需要更新dist[i]的值。步驟如下:

   (1) 用帶權的鄰接矩陣c來表示帶權有向圖, c[i][j]表示弧<vi,vj>上的權值。設S為已知最短路徑的終點的集合,它的初始狀態為空集。從源點v經過S到圖上其余各點vi的當前最短路徑長度的初值為:dist[i]=c[v][i], vi屬於V.
   (2) 選擇vu, 使得dist[u]=Min{dist[i] | vi屬於V-S},vj就是長度最短的最短路徑的終點。令S=S U {u}.

   (3) 修改從v到集合V-S上任一頂點vi的當前最短路徑長度:如果 dist[u]+c[u][j]< dist[j] 則修改 dist[j]= dist[u]+c[u][j]. 
   (4) 重復操作(2),(3)共n-1次.

算法具體實現如下:

import java.util.Scanner;

public class SSSP
{
    public static void main(String[] args)
    {
        Scanner input = new Scanner(System.in);
        
        System.out.print("請輸入圖的頂點和邊的個數(格式:頂點個數 邊個數):");
        int n = input.nextInt(); //頂點的個數
        int m = input.nextInt(); //邊的個數
        
        System.out.println();
        
        int[][] a = new int[n + 1][n + 1];
        //初始化鄰接矩陣
        for(int i = 0; i < a.length; i++)
        {
            for(int j = 0; j < a.length; j++)
            {
                a[i][j] = -1; //初始化沒有邊
            }
        }
        
        System.out.println("請輸入圖的路徑長度(格式:起點 終點 長度):");
        //總共m條邊
        for(int i = 0; i < m; i++)
        {
            //起點,范圍1到n
            int s = input.nextInt();
            //終點,范圍1到n
            int e = input.nextInt();
            //長度
            int l = input.nextInt();
            
            if(s >= 1 && s <= n && e >= 1 && e <= n)
            {
                //無向有權圖
                a[s][e] = l;
                a[e][s] = l;
            }
        }
        
        System.out.println();
        
        //距離數組
        int[] dist = new int[n+1];
        //前驅節點數組
        int[] prev = new int[n+1];
        
        int v =1 ;//頂點,從1開始
        dijkstra(v, a, dist, prev);
    }
    
    /**
     * 單源最短路徑算法(迪傑斯特拉算法)
     * @param v 頂點
     * @param a 鄰接矩陣表示圖
     * @param dist 從頂點v到每個點的距離
     * @param prev 前驅節點數組
     */
    public static void dijkstra(int v, int[][] a, int[] dist, int[] prev)
    {
        int n = dist.length;
        /**
         * 頂點從1開始,到n結束,一共n個結點
         */
        if(v > 0 && v <= n)
        {
            //頂點是否放入的標志
            boolean[] s = new boolean[n];
            
            //初始化
            for(int i = 1; i < n; i++)
            {
                //初始化為 v 到 i 的距離
                dist[i] = a[v][i];
                //初始化頂點未放入
                s[i] = false;
                //v到i無路,i的前驅節點置空
                if(dist[i] == -1)
                {
                    prev[i] = 0;
                }
                else
                {
                    prev[i] = v;
                }
            }
            
            //v到v的距離是0
            dist[v] = 0;
            //頂點放入
            s[v] = true;
            
            //共掃描n-2次,v到v自己不用掃
            for(int i = 1; i < n - 1; i++)
            {
                int temp = Integer.MAX_VALUE;
                //u為下一個被放入的節點
                int u = v;
                
                //這個for循環為第二步,觀測域為v的觀測域
                //遍歷所有頂點找到下一個距離最短的點
                for(int j = 1; j < n; j++)
                {
                    //j未放入,且v到j有路,且v到當前節點路徑更小
                    if(!s[j] && dist[j] != -1 && dist[j] < temp)
                    {
                        u = j;
                        //temp始終為最小的路徑長度
                        temp = dist[j];
                    }
                }
                
                //將得到的下一節點放入
                s[u] = true;
                
                //這個for循環為第三步,用u更新觀測域
                for(int k = 1; k < n; k++)
                {
                    if(!s[k] && a[u][k] != -1)
                    {
                        int newdist=dist[u] + a[u][k];
                        if(newdist < dist[k] || dist[k] == -1)
                        {
                            dist[k] = newdist;
                            prev[k] = u;
                        }
                    }
                }
            }
        }
        
        for(int i = 2; i < n; i++)
        {
            System.out.println(i + "節點的最短距離是:"
                + dist[i] + ";前驅點是:" + prev[i]);
        }

    }
}
View Code

 例,如圖中的有向圖,應用 Dijkstra算法計算從源頂點1到其它頂點間最短路徑的過程如下表所示:

 
  3、貪心選擇性質
    從V-S中選擇具有最短特殊路徑的頂點u,從而確定從源到u的最短路徑長度dist[u]。為什么從源到u沒有更短的其他路徑?如圖,如果存在一條從源到u且長度比dist[u]更短的路,設這條路初次走出S之外到達的頂點為x(x屬於V-S),然后徘徊於S內外若干次,左后離開S到達u。在這條路上分別記d(v,x),d(x,u)和d(v,u)為頂點v到頂點x,頂點x到頂點u,頂點v到頂點u的路長。則有:
      dist[x]<=dist[u]與u是當前貪心選擇矛盾!
4、最優子結構性質
     該性質描述為:如果S(i,j)={Vi....Vk..Vs...Vj}是從頂點i到j的最短路徑,k和s是這條路徑上的一個中間頂點,那么S(k,s)必定是從k到s的最短路徑。下面證明該性質的正確性。
     假設S(i,j)={Vi....Vk..Vs...Vj}是從頂點i到j的最短路徑,則有S(i,j)=S(i,k)+S(k,s)+S(s,j)。而S(k,s)不是從k到s的最短距離,那么必定存在另一條從k到s的最短路徑S'(k,s),那么S'(i,j)=S(i,k)+S'(k,s)+S(s,j)<S(i,j)。則與S(i,j)是從i到j的最短路徑相矛盾。因此該性質得證。
5、計算復雜性
     對於一個具有n個頂點和e條邊的帶權有向圖,如果用帶權鄰接矩陣表示這個圖,那么Dijkstra算法的主循環體需要O(n)時間。這個循環需要執行n-1次,所以完成循環需要O(n^2)時間。算法的其余部分所需要的時間不超過O(n^2)。
     程序運行結果為:

6.小結

 

 參考文獻:北大《算法設計與分析》公開課

                  王曉東《算法設計與分析》第二版

                  https://blog.csdn.net/liufeng_king/article/details/8726066

                  https://www.cnblogs.com/wellcherish/p/11061411.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM