最短路算法:Dijkstra&Bellman-Ford&SPFA
最短路算法:Dijkstra&Bellman-Ford&SPFA
书接上回,继续图论中的经典算法,今天是最短路的经典算法:Dijkstra,Bellman--Ford,以及Bellman--Ford的优化版本SPFA。
Dijkstra例题
题目描述
如图,求最短路径。

输入格式
顶点数n 边数m,n<=10,m<=100
m条边的顶点和权值
终点
输出格式
顶点0到终点的最短路径
输入样例
6 9
0 2 5
0 3 30
1 0 2
1 4 8
2 1 15
2 5 7
4 3 4
5 3 10
5 4 18
4输出样例
28我们需要找到0到某个点的最短路径。等等,最短?和上次的最小生成树很像啊。不过并不完全一样。虽然同样都是找最小的权值,但是最小生成树要求的是连通所有节点,路径并非是最短的,而最短路问题要求找到权值最短的路径。
思路
Dijkstra的思想和最小生成树的Prim比较相似。同样把注意力放在节点上:我们从起点出发,选择起点可去的那条最短的路,以此到达了下一个点,那么不难想到,这条路径就是从起点到下一个点的最短路径(事实上应该有一个严格的证明,不过这里问题不在这)。
**那么以此类推,每次都选择当前能走的最短的路,以此到达的点就是起点到这个点的最短路。**这简直和Prim一模一样对吧!不过在这里是一种递推关系,我们通过一步步求解最短的找到目标的最短路。
实现
还是一样,写一个结构体,构造一个邻接表来构建图。思考一下,怎么样每次都找到可走的最短的路。直接枚举太慢了,显然不符合我们的风度。同样采用Prim中的优先队列优化方法,把能走的路存入优先队列,这样每次都能找到最短的。再用一个数组dijk来记录最短路,$dijk[i]表示到达点的最短路。设当前选择的路的长度为表示到达i点的最短路。设当前选择的路的长度为l,当前将从,当前将从i到到j,那么可以得到递推式,那么可以得到递推式dijk[j] = dijk[i] + l$。
代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
struct r{
int f;
int t;
int l;
bool operator<(const r& oth) const{
return l>oth.l;
}
};
int dijk[15];
bool ud[15] = {false};
vector<r> rs[15];
priority_queue<r> q;
int main(){
int n, m, to;
cin >> n >> m;
for(int i=1;i<=m;++i){
int a, b, l;
cin >> a >> b >> l;
rs[a].push_back({a,b,l});
}
cin >> to;
q.push({0, 0, 0});
dijk[0] = 0;
while(!q.empty()){
r cr = q.top();
q.pop();
if(ud[cr.t]) continue;
dijk[cr.t] = cr.l;
ud[cr.t] = true;
if(!rs[cr.t].empty()){
for(r tr:rs[cr.t]){
if(!ud[tr.t]) {
tr.l += dijk[tr.f];//最短路径的值
q.push(tr);
}
}
}
}
cout << dijk[to];
return 0;
}Bellman-ford例题
题目描述
平面上有$n$个点$(n≤100)$,每个点的坐标均在$-10000~10000$之间。其中的一些点之间有连线。
若有连线,则表示可从一个点到达另一个点,即两点间有通路,通路的距离为两点间的直线距离。现在的任务是找出从一点到另一点之间的最短路径。
已知两点坐标分别为 $(x1,y1) (x2,y2)$, 则 两点间的距离为 $sqrt( (x2-x1)* (x2-x1) + (y2-y1)*(y2-y1) )$
输入格式
共$n+m+3$行,其中:
第一行为整数$n$。
第2行到第$n+1$行(共$n$行) ,每行两个整数$x$和$y$,描述了一个点的坐标。
第$n+2$行为一个整数$m$,表示图中连线的个数。
此后的$m$ 行,每行描述一条连线,由两个整数$i$和$j$组成, 表示第$i$个点和第$j$个点之间有连线。
最后一行:两个整数$s$和$t$,分别表示源点和目标点。
输出格式
一行,一个实数(保留两位小数),表示从$s$到$t$的最短路径长度。
输入样例
5
0 0
2 0
2 2
0 2
3 1
5
1 2
1 3
1 4
2 5
3 5
1 5输出样例
3.41事实上,Dijkstra会出点问题:如果存在负权的边,那么Dijkstra就会失效,因为它的贪心策略只注重当下,没有考虑到后面可能的负边。
思路
而Bellman--Ford算法可以解决这个问题。Bellford--Ford通过松弛来一步步找到到达终点的最短路。
用一个$dis$数组来记录到$i$的最短路,初始化为最大值。将起点的$dis$初始化为0。不断地遍历已知的路来更新最短路。当遍历了$n-1$遍时,就找到了所有的最短路。
怎么解释呢,比如第一次遍历时更新了从起点到相邻点的$dis$,第二次更新了相邻点的相邻点,也可能包括起点,以此类推…最终所有点的$dis$都得到了更新,也就得到了最短路。这实在是一种有趣的动态规划的方法。
实现
用结构体存储边,按思路遍历$n-1$次边即可。不过这显然浪费了性能,因为一开始后面的点根本去不了,而到了后面前面的点或许已经找到了最短路,有时所有点都找到了最优解但仍然在遍历。
有一个简单的优化方案,我们可以用一个变量来记录每次遍历是否有点的情况发生了改变。如果有一次遍历没有任何$dis$改变,那么此时已经找到了最优解,结束遍历。
代码
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#include <iomanip>
using namespace std;
struct p{
double x;
double y;
};
struct l{
int f;
int t;
double len;
};
vector<l> ls;
p ps[105];
double dis[105];
int main(){
int n;
cin >> n;
for(int i=1;i<=n;++i){
double a, b;
cin >> a >> b;
ps[i].x=a;
ps[i].y=b;
}
int m;
cin >> m;
for(int i=1;i<=m;++i){
int x, y;
cin >> x >> y;
double len = sqrt((ps[x].x-ps[y].x)*(ps[x].x-ps[y].x)+(ps[x].y-ps[y].y)*(ps[x].y-ps[y].y));
ls.push_back({x, y, len});
}
int s, t;
cin >> s >> t;
fill(dis, dis+1+n, 9999999);
dis[s] = 0;
for(int k=1;k<n;++k){
bool flag = false;
for(l pl:ls){
if(dis[pl.t]>dis[pl.f]+pl.len){
dis[pl.t] = dis[pl.f]+pl.len;
flag = true;
}
}
if(!flag) break;
}
cout << fixed << setprecision(2) << dis[t];
return 0;
}显然这样只解决了一部分问题,“一开始后面的点根本去不了,而到了后面前面的点或许已经找到了最短路”的问题还没有得到解决,对此还有一个优化方案,那就是只松弛能被松弛的两个点。这就是Bellman-Ford的进化版:SPFA。
SPFA
用邻接表存储图,用一个队列来存储当前还没有得到答案并能松弛的点。一开始把起点推入队列,然后不断地取出队列里的点,遍历该点的边并更新dis。对于更新了的点,还可以继续通过它找到答案,所以接下来把它推入队列。就这样循环往复,最后便高效地得到了答案。
代码
#include <iostream>
#include <queue>
#include <algorithm>
#include <iomanip>
#include <cmath>
using namespace std;
struct p{
int x;
int y;
};
struct l{
int t;
double len;
};
vector<l> ls[105];
p ps[105];
double dis[105];
queue<int> q;
bool inq[105] {false};
int main(){
int n;
cin >> n;
for(int i=1;i<=n;++i) cin >> ps[i].x >> ps[i].y;
int m;
cin >> m;
for(int i=1;i<=m;++i){
int x, y;
cin >> x >> y;
double len = sqrt((ps[x].x-ps[y].x)*(ps[x].x-ps[y].x)+(ps[x].y-ps[y].y)*(ps[x].y-ps[y].y));
ls[x].push_back({y, len});
}
fill(dis, dis+1+n, 9999999);
int s, t;
cin >> s >> t;
dis[s] = 0;
q.push(s);
inq[s] = true;
while(!q.empty()){
int f = q.front();
q.pop();
inq[f] = false;
if(!ls[f].empty()){
for(l cl:ls[f]){
if(dis[cl.t]>dis[f]+cl.len){
dis[cl.t] = dis[f]+cl.len;
if(!inq[cl.t]) q.push(cl.t);
}
}
}
}
cout << fixed << setprecision(2) << dis[t];
return 0;
}