最短路
参考资料
算法对比
最短路(Shortest Path)问题求图中两点间边权和最小的路径,分单源(一点到其余各点)与全源(所有点对之间)两类。下表对比常用算法,其中 代表图的点数, 代表图的边数。
| 算法名称 | 时间复杂度 | 最短路类型 | 负权图 |
|---|---|---|---|
| Floyd 算法 | 全源最短路 | 能 | |
| Bellman–Ford 算法 | 单源最短路 | 能 | |
| Dijkstra 算法 | 单源最短路 | 否 | |
| Johnson 算法 | 全源最短路 | 能 |
Floyd 算法
Floyd 基于动态规划求全源最短路。外层枚举中转点 ,用 松弛 ;枚举完所有 后 即为最短路。 必须放在最外层循环。它能处理负权边(无负环),实现简单,时间复杂度为 。
int a[N][N];
void floyd(int n)
{
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
}
}
}
}
Bellman–Ford 算法
Bellman–Ford 通过反复松弛所有边求单源最短路。最短路至多含 条边,故 轮松弛后收敛,时间复杂度为 。它能处理负权边;若第 轮仍可松弛,则图中存在负环。
SPFA(Shortest Path Faster Algorithm)是其队列优化:仅把被更新的点入队参与下一轮松弛,随机图上更快,但最坏仍为 ,易被特殊数据卡。下面的实现用 记录最短路边数,达到 即判定负环。
vector<pair<int,int>> G[N];
int dis[N],cnt[N];
bool vis[N];
bool spfa(int s,int n)
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;vis[s]=1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(auto [v,w]:G[u])
{
if(dis[v]<=dis[u]+w)continue;
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n)return 0;
if(!vis[v]){q.push(v);vis[v]=1;}
}
}
return 1;
}
Dijkstra 算法
Dijkstra 求非负权图的单源最短路,基于贪心:每次取出当前距离最小且未确定的点,用它松弛邻居,已确定的点距离不再更改。用优先队列维护候选点,时间复杂度为 。它不能处理负权边。下面用大根堆存 来模拟小根堆。
vector<pair<int,int>> G[N];
int dis[N];
bool vis[N];
void dijkstra(int s)
{
memset(dis,0x3f,sizeof dis);
priority_queue<pair<int,int>> q;
q.push({dis[s]=0,s});
while(!q.empty())
{
int u=q.top().second;
q.pop();
if(vis[u])continue;
vis[u]=1;
for(auto [v,w]:G[u])
{
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push({-dis[v],v});
}
}
}
}
Johnson 算法
Johnson 求含负权边(无负环)图的全源最短路。先建虚拟源点 向每个点连零权边,用 SPFA 求出势能 ;再将每条边 重赋权为 ,新权非负且不改变最短路结构。然后以每个点为源跑 Dijkstra,最后把距离还原为 。若虚拟源点的 SPFA 发现负环则无解。时间复杂度为 。
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=3005;
const ll inf=0x3f3f3f3f3f3f3f3f;
vector<pair<int,int>> G[N];
ll h[N],dis[N];
int cnt[N];
bool vis[N];
bool spfa(int s,int n)
{
memset(h,0x3f,sizeof h);
h[s]=0;vis[s]=1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(auto [v,w]:G[u])
{
if(h[v]<=h[u]+w)continue;
h[v]=h[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n)return 0;
if(!vis[v]){q.push(v);vis[v]=1;}
}
}
return 1;
}
void dijkstra(int s)
{
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
priority_queue<pair<ll,int>> q;
q.push({dis[s]=0,s});
while(!q.empty())
{
int u=q.top().second;
q.pop();
if(vis[u])continue;
vis[u]=1;
for(auto [v,w]:G[u])
{
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push({-dis[v],v});
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
while(m--)
{
int u,v,w;
cin>>u>>v>>w;
G[u].push_back({v,w});
}
for(int i=1;i<=n;i++)G[0].push_back({i,0});
if(!spfa(0,n+1))
{
cout<<-1<<'\n';
return 0;
}
for(int u=1;u<=n;u++)for(auto &[v,w]:G[u])w+=h[u]-h[v];
for(int i=1;i<=n;i++)
{
dijkstra(i);
ll ans=0;
for(int j=1;j<=n;j++)
{
ans+=j*(dis[j]==inf?1000000000ll:dis[j]+h[j]-h[i]);
}
cout<<ans<<'\n';
}
return 0;
}
传递闭包
传递闭包判断每对点之间是否可达。沿用 Floyd 框架,把松弛换成按位或与按位与:若 经 可达 ,则 可达 (对应代码 a[i][j]|=a[i][k]&a[k][j])。时间复杂度为 ,可用 bitset 优化到 。
#include <bits/stdc++.h>
using namespace std;
const int N=105;
bool a[N][N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cin>>a[i][j];
}
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
a[i][j]|=a[i][k]&a[k][j];
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cout<<a[i][j]<<' ';
}
cout<<'\n';
}
return 0;
}
差分约束
差分约束系统由若干形如 的不等式组成,求一组可行解。把每个约束 建成一条 权为 的边,则从虚拟源点 向所有点连零权边后跑 SPFA,得到的 就是一组可行解 。若存在负环则无解。
#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=5005;
vector<pair<int,int>> G[N];
int dis[N],cnt[N];
bool vis[N];
bool spfa(int s,int n)
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;vis[s]=1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(auto [v,w]:G[u])
{
if(dis[v]<=dis[u]+w)continue;
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n+1)return 0;
if(!vis[v]){q.push(v);vis[v]=1;}
}
}
return 1;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)G[0].push_back({i,0});
while(m--)
{
int u,v,w;
cin>>u>>v>>w;
G[v].push_back({u,w});
}
if(!spfa(0,n))
{
cout<<"NO"<<'\n';
return 0;
}
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<' ';
}
return 0;
}