1. 程式人生 > >十二、圖的演算法入門--(4)最短路問題---Dijkstra演算法實現

十二、圖的演算法入門--(4)最短路問題---Dijkstra演算法實現

先來看這樣一個問題:有n座城市,已知任意兩個座城市之間的距離,現在要分別求出城市A到其他n-1座城市的最短路徑,

也就是求所經過的距離和的最小值。

這是一個經典的單源最短路問題,即求一起點到其餘各個頂點的最短路徑問題。

首先,我們可以把該場景看成是一個帶權圖,把n個城市看成n個頂點,把兩座城市之間的距離看成是兩個頂點之間的邊權值,

這樣問題就轉化成了求頂點A到其餘n-1個頂點的最短路徑。

Dijkstra演算法是最常見的求解單源最短路問題的演算法。

先來看看 Dijkstra 演算法的具體過程:

定義帶權圖 GG 所有頂點的集合為 VV,接著我們再定義已確定最短路徑的頂點集合為 UU,初始集合 U

U 為空。接著執行以下操作:

  1. 首先我們將起點 xx 加入集合 UU,並在陣列 AA 中記錄起點 xx 到各個點的最短路徑(如果頂點到起點 xx 有直接相連的邊,則最短路徑為邊權值,否則為一個極大值)。

  2. 從陣列 AA 中選擇一個距離起點 xx 最近的、且不屬於集合 UU 的頂點 vv(如果有多個頂點 vv,任選其一即可),將頂點 vv 加入集合 UU,並更新所有與頂點 vv 相連的頂點到起點xx 的最短路徑。

  3. 重複第二步操作,直至集合 UU 等於集合 VV

演算法結束,陣列 AA 記錄了起點 xx 到其餘 n - 1n1 個點的最短路徑。

下面我們用一個小例子來模擬下 Dijkstra 演算法吧。


通過模擬,最終我們可以得到起點 aa 到各點的最短路徑分別為:

l: 5 (a -> l)l:5(a>l)

e: 8 (a -> l -> e)e:8(a>l>e)

r: 7 (a -> l -> r)r:7(a>l>r)

b: 9 (a -> l -> e -> b)b:9(a>l>e>b)

仔細分析演算法,我們可以發現,Dijkstra 演算法和前面講解的 Prim 演算法很相像,都是從一個點開始,每次確定一個點並完成更新,重複操作直至 nn 個點都確定為止。Dijkstra 演算法的時間複雜度為 O(V^2+E)

O(V2+E)VV 為頂點總個數,EE 為總邊數。如果利用堆進行優化,可以將時間複雜度優化 O(VlogV+E)O(VlogV+E),是最壞情況下最優的單源最短路演算法。

需要注意的是,Dijkstra 不適用於有邊權為負數的情況哦,否則會影響演算法的正確性。

演算法實現:計算一個帶權無向圖中從頂點0出發,到所有頂點的最短路徑長度。

比如下圖:


在這個圖中,從a出發到e的最短路a->l->e,長度是8;到b的最短路是a->l->e->b,長度為9;

程式碼如下:

#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;

const int INF = 0x3f3f3f3f;

struct Edge {
	int vertex, weight;
};

class Graph {
private:
	int n;
	vector<Edge> * edges;
    bool * visited;
public:
	int * dist;
	Graph (int input_n) {
		n = input_n;
		edges = new vector<Edge>[n];
		dist = new int[n];
        visited = new bool[n];
        memset(visited, 0, n);
		memset(dist, 0x3f, n * sizeof(int));
	}
	~Graph() {
		delete[] dist;
		delete[] edges;
        delete[] visited;
	}
    void insert(int x, int y, int weight) {
        edges[x].push_back(Edge{y, weight});
        edges[y].push_back(Edge{x, weight});
    }
    void dijkstra(int v) {
        //[1]初始化
        dist[v] = 0;
        //[2]n次迴圈,每次找出從源點出發最短路徑最小的未訪問的頂點,標記為已訪問,之後更新
        //和這個頂點相鄰的所有頂點的最短路。
        for(int i=0; i<n; i++) {
            int min_dist = INF, min_vertex;
            //[3]找出所有未訪問頂點中從源點出發最短路徑最小的,並將其儲存。
            for(int j=0; j<n; ++j) {
            //[4]如果頂點j未訪問且最短路徑比min_dist記錄的更小,那麼就用源點到頂點j的最短路徑更新
            //min_dist,然後將頂點編號儲存到min_vertex中。
                if(!visited[j] && dist[j] < min_dist) {
                    min_dist = dist[j];
                    min_vertex = j;
                }
            }
            //[5]將min_vertex點的訪問標誌置為1
            visited[min_vertex] = 1;
            //[6]鬆弛操作:對於已經找到的當前最近頂點min_vertex,如果源點到它的最短路徑長度
            //min_dist加上和min_vertex相鄰邊的邊權小於源點到該邊另一端的最短路徑,那麼就
            //更新另一端的最短路徑,也就是dist陣列中對應的值。
            for(Edge &j:edges[min_vertex]) {
                if(min_dist+j.weight < dist[j.vertex]) {
                    dist[j.vertex] = min_dist + j.weight;
                }
            }
        }
    }
};

int main() {
	int n, m;
	cin >> n >> m;
	Graph g(n);
	for (int i = 0; i < m; i++) {
		int a, b, c;
		cin >> a >> b >> c;
		g.insert(a, b, c);
	}
	g.dijkstra(0);
	for (int i = 0; i < n; i++) {
		cout << i << ": " << g.dist[i] << endl;
	}
	return 0;
}