1. 程式人生 > >迪傑斯特拉 & 堆優化

迪傑斯特拉 & 堆優化

單源最短路

該部落格的單源最短路演算法要解決的就是在一個沒有負權邊的圖上,找出所有點與源點 s 的最短路徑,這樣一個問題。這裡介紹迪傑斯特拉 O(n2) 解法與堆優化 O(mlogm) 解法,其中 n 為圖上節點數量,m 為圖上邊的數量。
不介紹也不推薦玄學複雜度的 spfa 解法。

樸素寫法思路:貪心

  1. 首先將所有點與源點的距離設為無窮大(記錄在 dis 陣列中),s 與自己的距離初始化為 0,還有一個表示“已經確定最短路徑長度”的節點集合 Set
  2. 然後不斷從不屬於 Set 集合的點中找到一個 dis 最小的點
    x
    ,那麼這個點在以後的操作中一定不會再更新(由於邊權非負,所以如果後面最短路徑將通過其他點 y 到達 x,那麼這個值最小為 disy+disyxdisy,由於 disydisx,所以可以保證在後續執行過程中 disx 不會被更新為更小的值),將這個點加入集合 Set 中,然後更新所有與 x 相鄰節點的 distmin(dist,disx+disxt)
  3. 不斷重複第二步,直到所有點的最短路徑都被找到 / 所有點都在 Set 集合中。

樸素寫法時間複雜度

從上面的演算法可以看出,大迴圈要將所有點都加入集合一次,而每次尋找這個“不在集合中且 disx 最小的點”需要一次 O(n) 的迴圈,接著更新所有與 x 鄰接的點(最多有 n 個點),這裡也需要 O(n),所以大迴圈為 O(n),小迴圈為 O(n+n),整體時間複雜度為 O(n×(n+n))=O(n2)

O(n2) 程式碼

const int maxn = 1000 + 100;
struct Node {
    int pos;
    int dis;
    Node() {}
    Node(int
p, int d) { pos = p; dis = d; } }; int n; int dis[maxn]; bool vis[maxn]; vector<Node> G[maxn]; void dij(int s) { // 距離初始化 fill(dis, dis + n + 1, INT_MAX); dis[s] = 0; int cnt = n; // cnt 表示不在集合中的節點個數 while(cnt > 0) { int Min = INT_MAX; int x; // 尋找不在集合中的,距離 s 最近的節點 for(int i = 1; i <= n; ++i) { if(!vis[i] && dis[i] < Min) { Min = dis[i]; x = i; } } // 將節點 x 加入集合 --cnt; vis[x] = true; // 更新節點 x 的鄰接節點 int len = G[x].size(); for(int i = 0; i < len; ++i) { int pos = G[x][i].pos; int d = G[x][i].dis; dis[pos] = min(dis[pos], dis[x] + d); } } }

堆優化

上面能夠優化的時間複雜度是在“找到不在集合中的離 s 最近的點”(每個點必須要進入集合才算更新結束,所以大迴圈的 O(n) 是無法減小的),我們能否通過一些排序操作,使得能夠快速地找到這個點 x 呢?
這裡就可以用到一個“小頂堆”的資料結構,它能夠在 O(logn)(其中 n 為堆中的元素數量)的時間複雜度內完成插入、刪除最小值的操作,在 O(1) 的時間複雜度內完成取堆內最小值的操作。於是我們可以將上面的查詢這一步操作放入到堆中,時間複雜度就能下降到 O(logn)
但這裡要注意一點,在我們查詢之後,是可以更新這個點的最短距離的,但是小頂堆不允許訪問、更改堆內元素,只能訪問堆頂元素,所以如果將點與當前最短距離放入堆內,將存在一些多餘的點(更新時該點還未從堆頂彈出),而這些所有資料最多有 m 個,m 為圖上邊的數量。
這裡可以標記某個點 x 已經在 Set 集合中,這樣在其他指向 x 的點更新到