从0开始详解 Johnson 全源最短路(P5905 【模板】Johnson 全源最短路)

问题引入:(【模板】Johnson 全源最短路 - 洛谷

ps:(如果不想 情景带入 请直接转跳到Johnson算法详解)

目录

问题引入:(【模板】Johnson 全源最短路 - 洛谷)

题目描述

输出格式

说明/提示

【数据范围】

 常用最短路时间复杂度:

 Johnson 全源最短路 详解

第一个问题:如何可以把负权都转变成正权

将负权边 转 正权的可行性证明:

跑完最短路答案的回推:

就这道题分析(上代码):

局部代码解析or功能(目的):

最后感谢您的阅读!!!


题目描述

给定一个包含 n 个结点和 m 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。

注意:

  1. 边权可能为负,且图中可能存在重边和自环;

  2. 部分数据卡 n 轮 SPFA 算法。

输入格式

第 1 行:222 个整数 n,m,表示给定有向图的结点数量和有向边数量。

接下来 m 行:每行 333 个整数 u,v,w,表示有一条权值为 w 的有向边从编号为 u 的结点连向编号为 v 的结点。

输出格式

输入输出样例

输入 #1

5 7
1 2 4
1 4 10
2 3 7
4 5 3
4 2 -2
3 4 -3
5 3 4

输出 #1

128
1000000072
999999978
1000000026
1000000014

输入 #2

5 5
1 2 4
3 4 9
3 4 -3
4 5 3
5 3 -2

输出 #2

-1

说明/提示

【样例解释】

左图为样例 111 给出的有向图,最短路构成的答案矩阵为:

0 4 11 8 11 
1000000000 0 7 4 7 
1000000000 -5 0 -3 0 
1000000000 -2 5 0 3 
1000000000 -1 4 1 0 

右图为样例 222 给出的有向图,红色标注的边构成了负环,注意给出的图不一定连通。

【数据范围】

通过看题我们可以得到什么有帮助的信息:

1.SPFA已死(题明确表示SPFA将会被卡 --> T)

2.给的边权可能为

3.这是一道全源最短路问题

 常用最短路时间复杂度:

朴素dijkstra算法 --> 时间复杂是 O(n2+m), n 表示点数,m 表示边数

堆优化版dijkstra --> 时间复杂度 O(mlogn), n 表示点数,m 表示边数

Bellman-Ford算法 --> 时间复杂度 O(nm), n 表示点数,m 表示边数

spfa 算法(队列优化的Bellman-Ford算法)--> 时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数

floyd算法 --> 时间复杂度是 O(n3), n 表示点数

请再次看一下本题数据范围:

我们可能会很头疼  ):

因为无论是单跑  floyd算法 还是

跑n遍 朴素dijkstra算法(不能处理负权问题)or Bellman-Ford算法 or spfa 算法

都难逃T的命运 ):

但!但是,我们可以发现

如果我们可以跑 n遍 堆优化版dijkstra 就可以控制在时间允许的范围去解决这个多源最短路问题。

但!但是,堆优化版dijkstra 不能处理负权问题

这就抛出了一个思路:

我们是否可以将所有边权 想办法 变成正(是可以为0的,因为0并不会影响跑 堆优化版dijkstra)的,然后跑 n遍 堆优化版dijkstra

好滴!

这就引进了一种新的算法:Johnson 全源最短路

----------------------------------------------------------------------我不是分割线 (:         (:         (:

 Johnson 全源最短路 详解

解决问题 最暴力的方式 就是层层攻破(滑稽,我就喜欢搞“暴力”)

第一个问题:如何可以把负权都转变成正权

为了更好的帮助理解,我借用b站Johnson 全源最短路讲解视频

[Rocky]Johnson全源最短路-C++竞赛级入门第三十一节

[Rocky]Johnson全源最短路-C++竞赛级入门第三十一节_哔哩哔哩_bilibili

中引入的 这一概念

假设有一个"nb点"(下面简称nb),nb可以到所有点 & nb到所有点的距离都相同(为了更好的描述,将距离 设为 k)

下面是图示(便于理解)

如果加上这个nb点跑最短路会得到什么呢?

为了更舒服与方便的去写&理解,我们不防将k == 0

示意图如下:

 跑最短路(nb到其他点的最短距离)

示意图如下:

ps:绿色数值 代表nb到该点的最短距离

这时候又出现了一个问题:这!啊这?到底有什么用呢?

约定:u点到v点 记作 u --> v,权值为 w

nb点到x点的最短距离为 head(x)

我们可以先拿一个负权边看一下

3 --> 4 :

-3 + 0 - (-3) == 0;

也就是:w + head(u) - head(v)

 

要不拿一条正权边看看?

2 --> 3

7 + (-5) - 0 == 2

可以惊奇的发现:nb(值nb点) NB 啊!!!

现在已经解决将负权边 转换为 正权边的方法了!!!         (:

 

如果很纠结 这样操作为啥是可行的 ???

不用纠结下面就给你简单的证明:

将负权边 转 正权的可行性证明:

因为我们 预处理操作(得到head[某点]值的操作)是跑的最短路

u -- v    w

一定有 h(v) <= h(u) + w (原理)

那么也一定有:

w + h(u) - h(v) >= 0

只是简单的证明,应该可以帮助您理解 (:

呃呃呃... ...

似乎又抛出了一个问题:

现在全部都是正权边,但最后最短路跑出来的结果要怎么 推回去呢?

跑完最短路答案的回推:

约定:dist[j] 是初始点(可以自己设置,根据题意设置初始点) 到 j 点的最短距离

j*dist[j]+head[j]-head[i]     (每次就是简单倒推回去)

为什么要乘 j 呢?

因为中间经过了j个点,我们需要把每一个点都倒推回去哦!!!

如果有点难理解,希望您可以先耐心往下看,结合代码可以更好的帮助您理解 (:

就这道题分析(上代码):

先看一下整体代码:

#include <bits/stdc++.h>
#define buff                     \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
#define int long long
#define endl "\n"
#define PII pair<int, int>
using namespace std;
const int N = 1e4;
int n, m;
int e[N], w[N], ne[N], h[N], idx;
bool st[N];
int dist[N];
int head[N];
int cnt[N];
void add(int a, int b, int c)
{
    ne[++idx] = h[a];
    e[idx] = b;
    w[idx] = c;
    h[a] = idx;
}
bool spfa()
{
    queue<int> q;
    for (int i = 1; i <= n; i++)
    {
        q.push(i);
        st[i] = 1;
    }
    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = 0;
        for (int i = h[t]; i; i = ne[i])
        {
            int j = e[i];
            if (head[t] + w[i] < head[j])
            {
                head[j] = head[t] + w[i];
                cnt[j]++;
                if (cnt[j] >= n + 1)
                    return 1;
                if (!st[j])
                {
                    st[j] = 1;
                    q.push(j);
                }
            }
        }
    }
    return 0;
}
void dij(int s)
{
    for (int i = 1; i <= n; i++)
    {
        st[i] = 0;
        dist[i] = 1e9;
    }
    priority_queue<PII> q;
    dist[s] = 0;
    q.push(make_pair(-dist[s], s));
    while (q.size())
    {
        int u = q.top().second;
        q.pop();
        if (st[u] == 1)
            continue;
        st[u] = 1;
        for (int i = h[u]; i; i = ne[i])
        {
            int v = e[i];
            if (dist[u] + w[i] < dist[v])
            {
                dist[v] = dist[u] + w[i];
                q.push(make_pair(-dist[v], v));
            }
        }
    }
}
signed main()
{
    buff;
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    for (int i = 1; i <= n; i++)
    {
        add(0, i, 0);
    }
    if (spfa())
    {
        cout << -1 << endl;
        return 0;
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = h[i]; j; j = ne[j])
        {
            w[j] += head[i] - head[e[j]];
        }
    }
    //测试使用
    // for(int i=1;i<=n;i++)
    //     cout<<head[i]<<endl;

    for (int i = 1; i <= n; i++)
    {
        dij(i);
        int ans = 0;
        for (int j = 1; j <= n; j++)
        {
            if (dist[j] == 1e9)
                ans += j * 1e9;
            else
                ans += j * (dist[j] + head[j] - head[i]);
        }
        cout << ans << endl;
    }
}

局部代码解析or功能(目的):

bool spfa()
{
    queue<int> q;
    for (int i = 1; i <= n; i++)
    {
        q.push(i);
        st[i] = 1;
    }
    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = 0;
        for (int i = h[t]; i; i = ne[i])
        {
            int j = e[i];
            if (head[t] + w[i] < head[j])
            {
                head[j] = head[t] + w[i];
                cnt[j]++;
                if (cnt[j] >= n + 1)
                    return 1;
                if (!st[j])
                {
                    st[j] = 1;
                    q.push(j);
                }
            }
        }
    }
    return 0;
}

跑这个spfa是为了:

1.判断负环

2.得到head(x)的值        (也就是上午提到的nb点到其他点的最短距离)

for (int i = 1; i <= n; i++)
    {
        for (int j = h[i]; j; j = ne[j])
        {
            w[j] += head[i] - head[e[j]];
        }
    }

为了使负权边 变成 正权边

void dij(int s)
{
    for (int i = 1; i <= n; i++)
    {
        st[i] = 0;
        dist[i] = 1e9;
    }
    priority_queue<PII> q;
    dist[s] = 0;
    q.push(make_pair(-dist[s], s));
    while (q.size())
    {
        int u = q.top().second;
        q.pop();
        if (st[u] == 1)
            continue;
        st[u] = 1;
        for (int i = h[u]; i; i = ne[i])
        {
            int v = e[i];
            if (dist[u] + w[i] < dist[v])
            {
                dist[v] = dist[u] + w[i];
                q.push(make_pair(-dist[v], v));
            }
        }
    }
}

常规堆优化版dijkstra

for (int i = 1; i <= n; i++)
    {
        dij(i);
        int ans = 0;
        for (int j = 1; j <= n; j++)
        {
            if (dist[j] == 1e9)
                ans += j * 1e9;
            else
                ans += j * (dist[j] + head[j] - head[i]);
        }
        cout << ans << endl;
    }

这段代码的目的:

1.倒推回去得到答案

2.输出答案

最后感谢您的阅读!!!

(:        (:        (:

 生活这条狗啊,追的我连从容撒泡尿的时间都没有。——《英雄时代》

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Joanh_Lan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值