單源最短路徑複習--Dijkstra演算法和Floyd演算法

FreeeLinux發表於2017-02-09

昨天覆習了一下單源最短路徑問題,今天總結一下。

解決單源最短路徑問題,我們熟知的演算法首先就是Dijkstra演算法了。Dijkstra演算法的核心就是貪心思想。我在以前的部落格中也寫過這個演算法:圖的拓撲排序、關鍵路徑、最短路徑演算法 – C++實現,現在看以前的部落格,我的程式碼思路還是很清晰的。Dijkstra演算法可以求出某一點到其他所有點的最短路徑,本文還將介紹一種可求出所有點對的最短路徑的演算法——Floyd演算法。

Dijkstra演算法

Dijkstra演算法是典型的單源最短路徑演算法,用於計算一個節點到其他所有節點的最短路徑。主要特點是以起始點為中心向外層層擴充套件,知道擴充套件到終點為之。Dijkstra演算法的要求是圖中不存在負權邊。因為Dijkstra演算法基於貪心策略,它是短視的。如果存在某個路徑上有負權邊,可能繞了幾圈得到的結果甚至是更優的,所以Dijkstra演算法在有負權邊的圖應用上是失敗的。

Dijkstra演算法的具體解釋我就不說了,如果不明白概念可參考這篇部落格的概念解釋:最短路徑—Dijkstra演算法和Floyd演算法

本文所有測試用例所用graph就是下面這幅圖片中的graph:

下面給出我的程式碼:

#include <iostream>
#include <vector>
#include <iomanip>
#include <limits.h>

const int NUM_VERTICES = 5;  //A B C D E

void print_solution(std::vector<int>& dist, std::vector<int>& path)
{
    for(auto i : dist)
        std::cout<<std::setw(3)<<i<<' ';
    std::cout<<std::endl;
    for(auto i : path)
        std::cout<<std::setw(3)<<i<<' ';
    std::cout<<std::endl;
}

void dijkstra(std::vector<std::vector<int>>& graph, int source)
{
 std::vector<int> dist(NUM_VERTICES, 0), path(NUM_VERTICES, -1);
    for(int i=1; i<NUM_VERTICES; ++i){
        dist[i] = graph[source][i] == 0 ? INT_MAX : graph[source][i];
        path[i] = source;
    }   

    std::vector<bool> visited(NUM_VERTICES, false);
    visited[source] = true;

    for(int i=0; i<NUM_VERTICES-1; ++i){
        int min = INT_MAX;
        int min_index = -1;

        for(int j=0; j<NUM_VERTICES; ++j){
            if(!visited[j] && dist[j] < min){
                min = dist[j];
                min_index = j;
            }
        }
        visited[min_index] = true;

 for(int k=0; k<NUM_VERTICES; ++k){
            int weight = graph[min_index][k] == 0 ? INT_MAX : graph[min_index][k];
            if(!visited[k] && weight != INT_MAX
                           && dist[min_index] != INT_MAX
                           && dist[min_index]+weight < dist[k]){
                dist[k] = dist[min_index] + weight;
                path[k] = min_index;
            }
        }
    }

    print_solution(dist, path);
}

int main()
{
    std::vector<std::vector<int>>
    graph = {{0,  10, 0,  30, 100}, //A
             {0,  0,  50, 0,  0  }, //B
             {0,  0,  0,  0,  10 }, //C
             {0,  0,  20, 0,  60 }, //D
             {0,  0,  0,  0,  0  }};//E
          //  A   B   C   D   E
    dijkstra(graph, 0);  //param 0 means vertex 'A'
    return 0;
}                    

輸出結果:
這裡寫圖片描述

Dijkstra演算法的時間複雜度是O(|E|+|V|2|)=O(V2)

O(|E|+|V|^2|) = O(V^2)
(參考:Dijkstra演算法時間複雜度)。對於稠密的圖來說,|E| 就是|V|^2,所以Dijsstra演算法中遍歷dist[min_index]+weigth那個迴圈加上最外層迴圈時間複雜度為θ(|V|2)
θ(|V|^2)
,對於稠密圖,這就是最優的了,總的時間複雜度正好是θ(|E|+|V|2)=θ(|V|2+|V|2)=O(|V|2)
θ(|E|+|V|^2) = θ(|V|^2+|V|^2) = O(|V|^2)
。但是對於非稠密圖,這個 |E| 實際沒這麼大,甚至 |E|=|V|
|E| = |V|
,這個演算法就未免效率低下了。

優化法方法是使用最小堆,我們dist[min_Index]+weight小於dist[k]時,將新的dist[k]的值插入最小堆;在上面查詢最小值的操作中,每次從最小堆中取出最小值,並且檢查是否visited,如果沒visited,那就找到新的頂點了。改進後的演算法時間複雜度是O(|E|lg|V|)

O(|E|lg|V|)

Floyd演算法

Floyd演算法是解決任意兩點間最短路徑的一種演算法,可以正確處理負權圖的最短路徑問題。

上面的Dijkstra演算法求出了某個點到其他所有點的最短路徑,我們要求所有點對的最短路徑,有這樣一種思路,就是再外面再迴圈 |V| 次,那麼不就求出所由點對的最短路徑了嗎?時間複雜度為O(N3)

O(N^3)

不過,Dijkstra演算法為我們提供了一種動態規劃的思想,一個點到另外一個點的最短路徑,要麼直接到達就是最短的,要麼就是經過了一個已經最優化的點間接到達這個點就是最短的,只有這麼兩種情況。Floyd演算法就根據這個思路把所有情況同過DP表的方式計算出來,時間複雜度是一樣的,也是O(N3)

O(N^3)
,不過Floyd演算法簡潔的多。

Floyd演算法DP公式:

D0[V][W]=min{D1[V][W],D1[V][K]+D1[K][W]}
D^0[V][W]=min\{ D^-1[V][W], D^-1[V][K]+D^-1[K][W]\}

D是一個二維矩陣,是一個輔助矩陣,初始狀態和graph是一致的,通過該矩陣的變化,我們來修正path矩陣的值即可。

更多的關於Floyd演算法的解釋參見: 資料結構之最短路徑(Floyd) ,包括我下面用的列印函式,可以參見它的解釋。不過它的列印函式對於非強連通圖有一點問題,我加以修正了。

列印函式實際上意思就是,比如我要找(0, 8)的最短路徑,如果path[0][8]的值為k,說明0->8之間經過路徑k,且0->k的結果是最優的,所以目前找到了所求路徑的一部分{0, k1}。然後我們再次查詢(k, 8)的最短路徑,看它們兩之間有沒有中間更優化的路徑,比如找到,如果有,那就找到了路徑{0, k1, k2},依次下去,知道path[k][8]的結果為8,說明沒有了。總的路徑就是{0, k1, k2 … 8}。

下面給出我的程式碼:

#include <iostream>
#include <vector>
#include <iomanip>
#include <limits.h>

const int NUM_VERTICES = 5;  //A B C D E

void print_solution(std::vector<std::vector<int>>& helper, std::vector<std::vector<int>>& path)
{
    for(int i=0; i<NUM_VERTICES; ++i){
        for(int j=i+1; j<NUM_VERTICES; ++j){
            if(helper[i][j] == INT_MAX)
                continue;
            std::cout<<i<<"->";
            int k = path[i][j];
            while(k != j){
                std::cout<<k<<"->";
                k = path[k][j];
     }
            std::cout<<j<<std::endl;
        }
    }
}

void floyd(std::vector<std::vector<int>>& graph)
{
    std::vector<std::vector<int>>
    helper(NUM_VERTICES, std::vector<int>(NUM_VERTICES)),
    path(NUM_VERTICES, std::vector<int>(NUM_VERTICES));

    for(int i=0; i<NUM_VERTICES; ++i){
        for(int j=0; j<NUM_VERTICES; ++j){
            helper[i][j] = graph[i][j] == 0 ? INT_MAX : graph[i][j];
            path[i][j] = j;
        }
    }

    for(int k=0; k<NUM_VERTICES; ++k){
      for(int i=0; i<NUM_VERTICES; ++i){
            for(int j=0; j<NUM_VERTICES; ++j){
                if(helper[i][k] != INT_MAX && helper[k][j] != INT_MAX
                                           && helper[i][j] > helper[i][k] + helper[k][j]){
                    helper[i][j] = helper[i][k] + helper[k][j];
                    path[i][j] = k;
                }
            }
        }
    }

    print_solution(helper, path);
}

int main()
{
    std::vector<std::vector<int>>
    graph = {{0,  10, 0,  30, 100}, //A
             {0,  0,  50, 0,  0  }, //B
             {0,  0,  0,  0,  10 }, //C
             {0,  0,  20, 0,  60 }, //D
             {0,  0,  0,  0,  0  }};//E
          //  A   B   C   D   E
    floyd(graph);  //param 0 means vertex 'A'
    return 0;
}

輸出結果:
這裡寫圖片描述

相關文章