Johnson全源最短路

板子题:https://www.luogu.com.cn/problem/P5905

个人认为是最难的一个最短路算法了,因为实现中用到了很多其他的最短路

数据:

负权边:支持

判断负环:不支持

复杂度:$NM\log M$

(实际上是$O(N·dijkstra)$,这里采用堆优化dijkstra)

前置知识

$bellman-ford$,$spfa$(可选),$dijkstra$,链式前向星

没错要学这个最短路其他的都得会。

基本思路

通过复杂度可以看出,这个算法与 $n$ 遍 $dijkstra$ 一样快。

那为什么不用 $n$ 遍 $dijkstra$ 呢?有负权。

那怎么解决呢?重新标权值,且保证正确性。

加一个新点0,将0与每个点之间连一条长度为0的边。

然后求0到每个点的最短路,记0到 i 的最短路为$h_i$。

更新每个边$edge\_len(u,v)$的权值为$edge\_len(u,v)+h_u-h_v$。

三角形两边之和大于第三边,所以$edge\_len(u,v)+h_u≥h_v$。

($\triangle SUV$有可能退化成线段$SV$,所以是$≥$)

所以这个图上每个边就都是非负的了,就可以跑 $n$ 边 $dijkstra$ 啦。

然后把每个源点的最短路$-h_u+h_v$还原回去,就是正确答案了。

正确性证明

设起始点为 $s$,终点为 $t$

最短路:$s\rightarrow p_0\rightarrow p_1\rightarrow p_2\rightarrow ...\rightarrow p_k\rightarrow t$

原图中的最短路长度:$w(s,p_0)+w(p_0,p_1)+w(p_1,p_2)+...+w(p_k,t)$

新图中的最短路长度:$w(s,p_0)+h_s-h_{p_0}+w(p_0,p_1)+h_{p_0}-h_{p_1}+w(p_1,p_2)+h_{p_1}-h_{p_2}+...+w(p_k,t)+h_{p_k}-h_t$

你会发现,带 $p$ 的项都消了!

则新图中最短路长度为:$w(s,p_0)+w(p_0,p_1)+w(p_1,p_2)+...+w(p_k,t)+h_s-h_t$

又因为两点间势能的差为定值,即$h_s-h_t$为定值,

所以新图中的最短路与原图中的最短路对应。

实现思路

求0到每个点的最短路,因为图中有负权,可以用$bellman-ford$或$spfa$

但这样连出来的图一定是个菊花图,选择这两种的效率没什么区别。

而且这不影响最后的时间复杂度,时间复杂度由$n$次$dijkstra$决定。

然后枚举点u,再枚举所有能到达的点v,更新u到v的权值w。

最后跑$n$遍$dijkstra$即可。

另外,对于这道题,要判断负环。

可以在求0到每个点的最短路时顺便用$bellman-ford$或者$spfa$判断负环

代码

#include <iostream>
#include <queue>
#include <utility>
#include <functional>
#include <cstring>
using namespace std;
struct Edge
{
    int v, w, nxt;
}edge[9001];
int n, m, u, v, w, head[3001], cnt;long long dis[3001], h[3001], ans;
void add(int u, int v, int w) //加边
{
    ++cnt;
    edge[cnt].v = v;
    edge[cnt].w = w;
    edge[cnt].nxt = head[u];
    head[u] = cnt;
}
void dijkstra(int s) //正常的dijkstra
{
    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > q;bool vis[3001];memset(vis, 0, sizeof vis);
    for(int i = 1;i <= n;++i) dis[i] = 1e9;
    dis[s] = 0;q.push(make_pair(0, s));
    while(!q.empty())
    {
        int u = q.top().second;q.pop();
        if(vis[u]) continue;
        vis[u] = 1;
        for(int i = head[u];i;i = edge[i].nxt)
            if(dis[edge[i].v] > dis[u] + edge[i].w)
            {
                dis[edge[i].v] = dis[u] + edge[i].w;
                q.push(make_pair(dis[edge[i].v], edge[i].v));
            }
    }
}
int main()
{
    cin >> n >> m;
    for(int i = 0;i < m;++i)
        cin >> u >> v >> w, add(u, v, w);
    for(int i = 1;i <= n;++i)
        add(0, i, 0), h[i] = 1e9; //加从0到每个点的边,权值为0
    h[0] = 0;
    for(int i = 1;i < n;++i) //开始bellman-ford
        for(int u = 0;u <= n;++u)
            for(int j = head[u];j;j = edge[j].nxt)
                h[edge[j].v] = min(h[edge[j].v], h[u] + edge[j].w);
    for(int u = 1;u <= n;++u) //判负环
        for(int i = head[u];i;i = edge[i].nxt)
            if(h[edge[i].v] > h[u] + edge[i].w)
            {
                cout << -1;
                return 0;
            }
    for(int u = 1;u <= n;++u)
        for(int i = head[u];i;i = edge[i].nxt)
            edge[i].w = edge[i].w + h[u] - h[edge[i].v]; //更新权值
    for(int u = 1;u <= n;++u)
    {
        dijkstra(u);ans = 0;
        for(int v = 1;v <= n;++v)
            if(dis[v] == 1e9) ans += v * 1e9;
            else ans += v * (dis[v] - h[u] + h[v]); //还原权值
        cout << ans << endl;
    }
    return 0;
}
posted @ 2021-07-20 16:26  5k_sync_closer  阅读(4)  评论(0编辑  收藏  举报  来源