单源最短路问题
0xFF 引入
在OI中经常会有一些问题要求图上两点的最短路径 这时就可以用最短路算法
注意
在最短路问题中我们默认没有负环(即没有权值和为负数的环)
0x01 约定
- 在单源最短路中我们用 \(s\) 表示起点 用 \(t\) 表示终点
- 在单源最短路中我们用 \(l_i\) 表示从 \(s\) 倒 \(i\) 的最短路径
- \(l_s=0\)
- 用 \((u,v)\) 表示从 \(u\) 到 \(v\) 的长度
0x02 松弛操作
可以发现当 \(l_v>l_u+(u,v)\) 时我们可以用 \(l_u+(u,v)\) 来替换 \(l_v\)
0x03 Bellman-Ford
可以轻易的想到这种做法
- 将 \(l_s\) 设置成 \(0\)
- 枚举每一条边 如果 \(l_v>l_u+(u,v)\) 就用 \(l_u+(u,v)\) 来替换 \(l_v\)
- 如果没有进行松弛就结束 否则继续执行 \(2\) 操作
code:
//LOJ
//单源最短路
//https://loj.ac/p/119
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
struct e{
int u,v;
ll w;
};
vector<e> G;
int n,m,s,t;
ll Bellman_Ford(int s,int t){
vector<ll> l(n+10,INT_MAX);
l[s]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<G.size();j++){
int u=G[j].u,v=G[j].v,w=G[j].w;
l[v]=min(l[v],l[u]+w);
}
}
return l[t];
}
int main(){
cin>>n>>m;
cin>>s>>t;
while(m--){
int u,v;
ll w;
cin>>u>>v>>w;
G.push_back({u,v,w});
G.push_back({v,u,w});
}
cout<<Bellman_Ford(s,t);
return 0;
}
可以发现复杂度时间为 \(\mathcal{O}(VE)\)
0x04 SPFA
可以发现在 Bellman-Ford 中有许多次是多余的
又可以发现只有上一次被松弛的结点所连接的边才有可能引起下一次的松弛操作
所以我们可以应个队列记录被松弛的结点 然后松弛它所连接的点
这样你就发明了已经死了的 SPFA (不我的 SPFA 555
code:
//LOJ
//单源最短路
//https://loj.ac/p/119
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
struct e{
int u,v;
ll w;
};
vector<e> G[2510];
int n,m,s,t;
ll SPFA(int s,int t){
vector<ll> l(n+10,INT_MAX);l[s]=0;
vector<bool> f(n+10,1);f[s]=0;
queue<int> q;
q.push(s);
while(q.size()){
int u=q.front();q.pop();
f[u]=1;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v,w=G[u][i].w;
if(l[v]>l[u]+w){
l[v]=l[u]+w;
if(f[v]){
q.push(v);
f[v]=0;
}
}
}
}
return l[t];
}
int main(){
cin>>n>>m;
cin>>s>>t;
while(m--){
int u,v;
ll w;
cin>>u>>v>>w;
G[u].push_back({u,v,w});
G[v].push_back({v,u,w});
}
cout<<SPFA(s,t);
return 0;
}
为什么说它死了呢
首先在随机数据下它的复杂度时间为 \(\mathcal{O}(KE)\) (\(K \le V\))
但如果数据会让每个都松弛如链 树 这样复杂度时间为 \(\mathcal{O}(VE)\) 那就和 Bellman-Ford 一样了
0x05 Dijkstra
英文小课堂:
和我读 /ˈdikstrɑ/ 会了吗(逃
Dijkstra 的思路和其他的不太一样 有点像贪心
他把所有的点分成了两类 一类是最短路确定了的 一类是最短路不确定的
首先所有的点都是第二类
接下来找到第二类中 \(l\) 最小的 把它标记为第一类
然后松弛它连接的点
code:
//LOJ
//单源最短路
//https://loj.ac/p/119
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
struct e{
int u,v;
ll w;
};
vector<e> G[2510];
int n,m,s,t;
ll Dijkstra(int s,int t){
vector<ll> l(n+10,INT_MAX);l[s]=0;
vector<bool> f(n+10,1);
for(int i=1;i<=n;i++){
int u=-1;
for(int j=1;j<=n;j++)if(f[j]&&(u==-1||l[j]<l[u]))u=j;
if(u==-1)break;
f[u]=0;
for(int j=0;j<G[u].size();j++){
int v=G[u][j].v,w=G[u][j].w;
l[v]=min(l[v],l[u]+w);
}
}
return l[t];
}
int main(){
cin>>n>>m;
cin>>s>>t;
while(m--){
int u,v;
ll w;
cin>>u>>v>>w;
G[u].push_back({u,v,w});
G[v].push_back({v,u,w});
}
cout<<Dijkstra(s,t);
return 0;
}
可以发现如果有负权时他的贪心策略是错误的
其次它的时间复杂度为\(\mathcal{O}(V^2)\)
0x06 堆优化Dijkstra
可以发现找到第二类中 \(l\) 最小的点这步的复杂度是\(\mathcal{O}(V)\)
所以我们可以用一种可以自动找到最值的ds
铛铛铛 它就是优先队列(如果你闲得慌可以用手写堆
我们可以每次松弛后就把这个点丢进优先队列里
然后每次取出堆顶
code:
//LG
//【模板】单源最短路径(标准版)
//https://www.luogu.com.cn/problem/P4779
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
struct e{
int u,v;
int w;
};
vector<e> G[100010];
int n,m,s;
void Dijkstra(int s){
vector<int> l(n+10,INT_MAX);l[s]=0;
vector<bool> f(n+10,1);
priority_queue<pair<int,int> > q;
q.push(make_pair(0,s));
while(q.size()){
int u=q.top().second;q.pop();
if(f[u]){
f[u]=0;
for(int j=0;j<G[u].size();j++){
int v=G[u][j].v,w=G[u][j].w;
if(l[v]>l[u]+w){
l[v]=l[u]+w;
q.push(make_pair(-l[v],v));
}
}
}
}
for(int i=1;i<=n;i++)cout<<l[i]<<" ";
}
int main(){
cin>>n>>m;
cin>>s;
while(m--){
int u,v,w;
cin>>u>>v>>w;
G[u].push_back({u,v,w});
}
Dijkstra(s);
return 0;
}
因为堆的复杂度为 \(\mathcal{O}(\log n)\)
所以堆优化Dijkstra的复杂度为 \(\mathcal{O}( (V+E) \log E)\)
inf 总结
其实每个算法都有自己的用处要根据题目选择
| 算法 | 时间复杂度 | 适合的图 | 能否在负权图中使用 |
|---|---|---|---|
| Bellman-Ford | \(\mathcal{O}(VE)\) | 稀疏图 | 能 |
| SPFA | \(\mathcal{O}(KE)\) (\(K \le V\)) | 稀疏图 | 能 |
| Dijkstra | \(\mathcal{O}(V^2)\) | 完全图 | 不能 |
| 堆优化Dijkstra | \(\mathcal{O}( (V+E) \log V)\) | 稀疏图 | 不能 |
浙公网安备 33010602011771号