一、Bellman-Ford算法描述
Bellman-Ford算法是一种用于计算图中单源最短路径的算法,也就是某一点(称为源点)到其他所有点的最短路径。可以处理边权值为负数的情况,且可以判断图中是否存在负权回路,即路长度为负数的回路,如果有负权回路则图无最短路径。
算法实现可以概括为每次过程利用图中的边,对源点进行松弛,就是看一看能不能通过这条边,使源点到达这条边终点的路径变短,如果可以变短,则松弛成功。每次该过程要尝试将图中所有边用于松弛,直到本次过程无一条边松弛成功,则得到最短路径结果。
二、举例分析
百度搜索带权有向图,随便抠了个,给边编上号,用Bellman-Ford算法分析一下。设源点为1,计算1到其他点的最短路径
- 初始化最短路径结果为[0, 999 ,999, 999 ,999],999表示无穷远,点1到点1距离为0
- 边1、边2、边5可松弛成功,最短路径变为[0,10,999,30,100]
- 先利用边3使点1到点3距离缩短为60(边3连接点2和点3,此时点1到点2距离为10),在利用边4使点1到点5距离缩短为70;然后边7又可将点1到点3距离缩短为50。最短路径变为[0,10,50,30,100]
- 只有边4可松弛成功,点1到点5最短路径缩短为60(点1到点3+点3到点5=50+10)。最短路径变为[0,10,50,30,60]
- 所有边均无法松弛,已得到结果
总结下来,就是每次观察图中的边能不能使目前已知的最短路径更短,当一轮观察后最短路径无变化就得到结果了。我说句不负责任的话,由数学知识可得,e个点的图最多尝试e-1轮即可得到结果。算法写成代码时的循环条件就是i<=e-1,但e-1是最多的尝试次数,很有可能会在中途得到结果。
三、队列优化实现
因此可以在每次尝试前判断上一次尝试后存储最短路径结果的数组是否发生变化,没有变化则已经得到结果可以直接break跳出循环,但这样每次尝试前都需要判断是否需要开始本轮尝试,浪费了一些时间。
很明显,用于松弛成功的边的起点都是上一次尝试中最短路径发生了变化的点,也就是每次尝试中可以仅对上一次尝试后最短路径发生了变化的点的相邻边进行松弛操作。可以用队列来存储这些点,用于以后的尝试中。
#include <iostream>
#include <stdio.h>
#include <queue>
using namespace std;
int main() {
int p1[21] = { 0 }, p2[21] = { 0 }, length[21] = { 0 };//点、点、之间的权值
int e, v;//点、边数
cout << "输入点和边数:";
cin >> e >> v;
cout << "输入点与点之间的权值" << endl;
int first[21] = { 0 }, other[21] = { 0 };//邻接表存储图
for (int i = 1; i <= v; i++) {//为边编号1~m
cin >> p1[i] >> p2[i] >> length[i];
other[i] = first[p1[i]];
first[p1[i]] = i;
}
int result[21] = { 0 };//存储最短路径结果数组
int inf = 9999;//假设9999为无穷远
for (int i = 1; i <= e; i++) {
result[i] = inf;
}
result[1] = 0;//计算点1到其他点的最短路径
int book[21] = { 0 };//判断某个数是否在队列中,0不在,1在
book[1] = 1;
queue<int> queue;
queue.push(1);
while (!queue.empty()) {//队列不为空时说明上一次松弛仍有最短路径发生了变化
int line = first[queue.front()];//取来队首的点的邻边用于松弛,该点在上一次循环中最短路径发生变化
while (line != 0) {//0时说明该点没有下一个邻边了
if (result[p2[line]] > result[p1[line]] + length[line]) {
result[p2[line]] = result[p1[line]] + length[line];
if (book[p2[line]] == 0) {//边的终点不在队列中
queue.push(p2[line]);//入队
book[p2[line]] = 1;
}
}
line = other[line];//该点下一个邻边
}
book[line] = 0;//每个点可能多次进入队列,需要归0
queue.pop();
}
cout << "----------------" << endl;
for (int i = 1; i <= e; i++) {//输出结果
cout << result[i] << " ";
}
bool flag = false;
for (int i = 1; i <= e; i++) {//判断是否存在负权回路
if (result[p2[i]] > result[p1[i]] + length[i]) {//若仍可松弛则存在负权回路
flag = true;
}
}
cout << endl;
if (!flag)
cout << "不存在负权回路" << endl;
else
cout << "存在负权回路,无最短路径" << endl;
return 0;
}
图的邻接表存储就不写了,也许哪天就想单独总结下图的几种存储方式了呢。下图为例子的结果