1. 程式人生 > 其它 >筆記-Johnson全源最短路

筆記-Johnson全源最短路

前言

noip前刷模板時刷到了這個模板,然後隨便看了一下題解發現還挺簡單的就來把坑補了(雖然好像沒什麼應用,不補也沒關係)

更好的閱讀體驗


模板題

題意

給定一個包含 \(n\) 個結點和 \(m\) 條帶權邊的有向圖,求所有點對間的最短路徑長度,一條路徑的長度定義為這條路徑上所有邊的權值和。

正文

Johnson最短路就是用來在稀疏圖上解決上述問題的。

前置芝士:Floyd,Dijkstra,Bellman-Ford,SPFA

首先我們都學過一個多源最短路的演算法:Floyd。它是基於dp的一種做法,詳細的我就不說了自己隨便上baidu搜一篇看看。它的時間複雜度是穩定的 \(O(n^3)\)

,其中 \(n\) 為點數,下同(看它三重迴圈也能看出來),這裡給出程式碼實現:

for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);

由於其基於dp的本質所以對於什麼樣的圖都是可做的,但這也有一個缺點:對於任何圖複雜度都為 \(O(n^3)\)

考慮另外一個最短路演算法:SPFA(這裡就不說 Bellman_Ford 了,不過也預設SPFA的時間複雜度為 \(O(nm)\)

,其中 \(m\) 為邊數,下同)。我們知道這是一個單源最短路演算法,如果對於每個點作為起點跑一遍SPFA,時間複雜度為 \(O(n^2m)\),在完全圖上被 Floyd 吊打,稀疏圖(樹)也只是和 Floyd 打平手,還不如就寫 Floyd。但它有一個優點:可以跑負權圖。這裡給出程式碼實現:

queue<int> q;
q.push(s);
memset(dis, 0x3f, sizeof(dis));
vis[s] = true;
dis[s] = 0;
while (!q.empty()) {
    int x = q.front();
    q.pop();
    for (int i = 0; i < (int)vec[x].size(); i++) {
        #define y vec[x][i].to;
        if (dis[y] > dis[x] + vec[x][i].val) {
            dis[y] = dis[x] + vec[x][i].val;
            if (vis[y] == false) {
                vis[y] = true;
                q.push(y);
            }
        }
        #undef y
    } vis[x] = false;
}

考慮另一個演算法:Dijkstra,它是基於貪心的,如果採取堆優化時間複雜度可以到 \(O(m\log{n})\)。跑 \(n\) 次複雜度 \(O(nm\log{n})\)。在稀疏圖上完爆上述兩個做法。這裡該出程式碼實現:

priority_queue<pair<int, int> > q;
q.push(make_pair(s, 0));
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
while (!q.empty()) {
    int x = q.top();
    q.pop();
    if (vis[x] == true) continue;
    vis[x] = true;
    for (int i = 0; i <(int)vec[x].size(); i++) {
        #define y vec[x][i].to
        if (dis[y] > dis[x] + vec[x][i].val) {
            dis[y] = dis[x] + vec[x][i].val;
           	q.push(make_pair(y, -dis[y]));
        }
        #undef y
    }
}

但是這個演算法有個缺點:不能處理負權圖。為什麼?因為他是基於貪心的一個演算法,每次從堆頂取出來更新答案的 \(x\) 必須是已經確定答案的,而如果有負權的話有可能會得到更小的答案,所以就不對了。那麼我們考慮如何將這個圖變成非負權圖。

想法1

首先有一個很直觀的想法:全部加上某個值,可是這樣答案是不正確的,原因是這樣找到的最短路的值還和路徑經過的邊數有關,所以無法保證新圖最短路和原圖最短路相同。

想法2

考慮考慮最短路的性質:\(u\rightarrow v,dis[v]\le dis[u]+val(u,v) \Rightarrow val(u,v)+dis[u]-dis[v]\ge 0\)。這不就是一個非負數嗎?那麼我們考慮新建一個源點連向每個點一個長度為 \(0\) 的邊,然後跑一遍 SPFA 求出到每個點的最短路 \(dis[x]\),然後對於每條邊 \(u\rightarrow v\),將其權值變為 \(val(u,v)+dis[u]-dis[v]\),再從每個點跑 Dijkstra 即可。時間複雜度 \(O(nm+nm\log{n})\) 稀疏圖吊打 Floyd。

完整程式碼

#include <bits/stdc++.h>
using namespace std;
struct node{
	int pre, to, val;
}edge[60005];
int head[30005], tot;
int n, m;
long long dis[30005], D[30005];
int cnt[30005];
bool vis[30005];
queue<int> q;
priority_queue<pair<long long, int> > pq;
void add_edge(int u, int v, int l) {
	edge[++tot] = node{head[u], v, l};
	head[u] = tot;
}
bool SPFA(int s) {
    for (int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f3f3f3f3f;
    q.push(s);
    vis[s] = true;
    dis[s] = 0;
    cnt[s] = 1;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (int i = head[x]; i; i = edge[i].pre) {
			int y = edge[i].to; 
            if (dis[y] > dis[x] + edge[i].val) {
                dis[y] = dis[x] + edge[i].val;
                cnt[y] = cnt[x] + 1;
              	if (cnt[y] > n + 1) return false;
                if (vis[y] == false) {
                    vis[y] = true;
                    q.push(y);
                }
            }
        } vis[x] = false;
	}
	return true;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add_edge(u, v, w);
    }
    for (int i = 1; i <= n; i++) add_edge(0, i, 0);
    if (SPFA(0) == false) {
    	puts("-1");
    	return 0;
	}
    for (int i = 1; i <= n; i++) D[i] = dis[i];
    for (int i = 1; i <= n; i++) {
        for (int j = head[i]; j; j = edge[j].pre) {
            int k = edge[j].to;
            edge[j].val += dis[i] - dis[k];
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) dis[j] = 100000000000, vis[j] = false;
        dis[i] = 0;
        while (!pq.empty()) pq.pop();
        pq.push(make_pair(0, i));
        while (!pq.empty()) {
            int x = pq.top().second;
            pq.pop();
            if (vis[x] == true) continue;
            vis[x] = true;
            for (int k = head[x]; k; k = edge[k].pre) {
                int y = edge[k].to;
                if (dis[y] > dis[x] + edge[k].val) {
                    dis[y] = dis[x] + edge[k].val;
                    pq.push(make_pair(-dis[y], y));
                }
            }
        }
        long long ans = 0;
        for (int j = 1; j <= n; j++) {
        	if (dis[j] >= 1000000000)
        		ans = ans + 1ll * j * 1000000000;
			else
				ans = ans + 1ll * j * (dis[j] - D[i] + D[j]);
		}
        printf("%lld\n", ans);
    }
    return 0;
}