[图论] 最短路探秘 之 全源最短路
[图论] 最短路探秘 之 全源最短路
被迫营业again
- 前置知识:
- Dijkstra算法(优先队列/堆优化)
- Bellman-Ford算法/队列优化Bellman-Ford算法(SPFA)
前言:有一个包含 n 个结点和 m 条带权边的有向图
对于单源最短路(给定一个起点s,求s到图中任意点的距离),我们熟知的算法有Dijkstra算法和Bellman-Ford算法。
那么要想求出图中所有节点对的最短路径,我们应当怎么做呢?
很容易想出思路:我们以每个点作为起点,分别做一次单源最短路,即可得到答案。对于所有边边权非负的图,我们可以使用优先队列优化的Dijkstra算法,时间复杂度为
那么有没有什么好方法能够解决这个问题呢?
一、 Floyd-Warshall 算法
1. 算法介绍
首先我们给出一种基于动态规划思想的算法——Floyd-Warshall 算法(又称Floyd算法)。该算法的创始人之一是1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W. Floyd)。该算法在负权边可以存在但负环不存在时生效,时间复杂度是
2. 算法思路与过程
此算法基于图的邻接矩阵存储方式,利用了动态规划的思想,具体的算法思路如下:
初始图存储在一个邻接矩阵中,有直接连边的节点对对应在矩阵中的值设置为该边边权,其他全部设置为
接下来枚举节点集合
对于任意节点对
① 点
②点
3. 状态转移方程
基于Floyd的算法过程,我们可以设计如下状态转移方程:
其中,
4. 核心代码示例
for(int k=0;k<n;k++)//k为中间点
for(int i=0;i<n;i++) //i为起点
for(int j=0;j<n;j++) //j为终点
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
5.疑难问题解析
细心的同学可能想提问,为什么我们**必须要先枚举
其实我们现在得到的这个状态转移方程可以说算是优化之后的版本
原本的状态转移方程其实是这样子的:
根据动态规划的思想,
那么
根据动态规划的思想,
再细说一下,由状态转移方程
又因为观察状态转移方程可以发现,
二、 Johnson 算法
1. 算法介绍
Johnson 算法能够在
2. 算法思路与过程
还记得我们一开始说的:
-
“ 那么要想求出图中所有节点对的最短路径,我们应当怎么做呢?
很容易想出思路:我们以每个点作为起点,分别做一次单源最短路,即可得到答案。对于所有边边权非负的图,我们可以使用优先队列优化的Dijkstra算法,时间复杂度为
” -
“ 但如果图中存在负权边,我们就必须使用
次Bellman-Ford算法,此算法效率低下,时间复杂度为 ”
考虑这样一个思路:
我们可不可以把原来的图,等价转化为一个图,使得所有的边的边权非负,并且使得新图中任意两点的最短路仍然是原图中的最短路?
很有趣的思想是不是,我们可以做一些小小的尝试,比如说,找到原图中的最小的边权,然后把所有的边的边权都加上这个最小值的绝对值?比如以下这个图:

对于从点
根据我们的思想,我们把这个图的每一条边都加上3,得到的新图如下:

则对于新图,从点
明显两个图的最短路是不一样的,所以我们的思路是错误的
那我们要怎样做,才能保证原图与新图的最短路径是一致的呢?
Johnson 算法给了下面这样的一种权值赋予方法:
在原图的基础上,新建一个虚拟节点,编号为
在这个图上,以节点
用
则可定义新边权如下:
以下

那么为什么可以这样做呢?
首先我们来证明新图边权
在对点
所以我们可以有
即
接着我们来证明新图最短路径仍然是原图最短路:
对于节点对
那么
若新图和原图最短路径相同的话,则
则有以下推导过程:
对于给定的
(注:
所以我们得到了解决的方法,按照上述方法可以得到 Johnson 算法的具体实现过程:
① 新建虚拟节点
② 跑一遍Bellman-Ford最短路算法/队列优化的Bellman-Ford(SPFA)算法,解决负环问题,并求出从点
③ 将原图中的所有边重新赋边权为
④ 以图中每一个点为起点,分别跑一次二叉堆/优先队列优化的Dijkstra算法,求出全源最短路
注意最后要复原,即
3. 代码示例
Johnson 算法以 Dijkstra 算法和 Bellman-Ford 算法作为子程序
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> paii;
#define N (3000+5)
int n,m,cnt[N];
bool vis[N],flag;
LL dis[N][N],h[N]; //dis[u][v]:点u到点v的最短路
vector <LL> edge[N],w[N],w2[N]; //edge:邻接表存图,w:原图边权,w2:新图边权
void add(int u,int v,int c){ //add函数:建图连边
edge[u].push_back(v);
w[u].push_back(c);
}
void create_new(int u,int v,int c){ //create_new函数:重新赋新边权
w2[u].push_back(c+h[u]-h[v]);
}
void SPFA(int be){ //SPFA:队列优化的Bellman-Ford算法
queue <int> q;
q.push(be);
memset(h,0x3f,sizeof(h));
h[be]=0,vis[be]=1;
while(!q.empty()){
int now=q.front();
q.pop();
vis[now]=0;
int len=edge[now].size();
for(int i=0;i<len;i++){
int nxt=edge[now][i];
if(h[nxt]>h[now]+w[now][i]){
h[nxt]=h[now]+w[now][i];
cnt[nxt]=cnt[now]+1;
if(cnt[nxt]>(n+1)){
flag=1;
return;
}
if(!vis[nxt]) vis[nxt]=1,q.push(nxt);
}
}
}
}
void Dijkstra(int be){ //Dijkstra算法
priority_queue<paii,vector<paii>,greater<paii> > q;
memset(vis,0,sizeof(vis));
dis[be][be]=0;
paii begin;
begin.first=0,begin.second=be;
q.push(begin);
while(!q.empty()){
paii now=q.top();
q.pop();
int u=now.second,c=now.first;
if(vis[u]) continue;
vis[u]=1;
int len=edge[u].size();
for(int i=0;i<len;i++){
int v=edge[u][i],s=w2[u][i];
if(dis[be][v]>c+s){
dis[be][v]=c+s;
if(!vis[v]){
paii nxt;
nxt.first=dis[be][v],nxt.second=v;
q.push(nxt);
}
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
memset(dis,0x3f,sizeof(dis)); //初值赋为inf
for(int i=0;i<=n;i++) dis[i][i]=dis[0][i]=0;
for(int i=1;i<=m;i++){
int u,v,c;
scanf("%d%d%d",&u,&v,&c);
add(u,v,c);
}
for(int i=1;i<=n;i++){ //0号节点向其他点连边,边权为0
add(0,i,0);
}
cnt[0]=1;
SPFA(0);
if(flag){ //图中存在负环
puts("-1");
return 0;
}
for(int i=1;i<=n;i++){
int len=edge[i].size();
for(int j=0;j<len;j++){
create_new(i,edge[i][j],w[i][j]); //建新图
}
}
for(int i=1;i<=n;i++){
Dijkstra(i);
for(int j=1;j<=n;j++){
if(dis[i][j]>=4000000000000000000) //inf
dis[i][j]=1e9;
else{
dis[i][j]=dis[i][j]-h[i]+h[j]; //最短路长度复原,最终dis[i][j]即为原图中i到j的最短路
}
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?