文章目录
- 1.算法原理
- 2. 算法流程
- 3.Bellman-Ford算法的实现
- 4.单源单目的地的特殊写法
- 参考资料
Bellman-Ford算法主要针对带有负值的单源最短路径问题,当有向图带有其权小于0的边的时候,不能使用迪杰斯特拉算法,但是只要不是带负权的回路,我们依然可以使用Bellman-Ford算法。 |
1.算法原理
Bellman-Ford算法的核心思想是动态规划,即我们需要定义子问题的状态 和动态规划递归式。
讨论前提
如果图中共有个顶点,则所有的最短路径最多只有条边。
如果一条路径具有条以上的边,则一定有环路(参考图的最小生成树性质)。
而由于环路的权值都不小于0,则去掉环路后的路径会更短,所以两个连通的顶点之间不存在含有环路的最短路径,且最多有n-1条边。
动态规划公式
所以,我们像迪杰斯特拉算法一样关注最短路径的长度,
令表示源点到顶点且最多含有条边的最短路径,于是就是我们的目标。
首先,对于有,
对于,有
即要求到的最短路径,我们可以先求到的最短路径。
2. 算法流程
有了动态规划公式,我们可以按照递归式子,逆序计算而得到结果。
Bellman-Ford算法伪代码
initialize d(*,0)
// 计算其余的d(*) = d(*,n-1)
for(int k=1;k <n; k++){
for(每一条边(u,v))
d(v) = min{d(v),d(u) + cost(u,v);
}
举个栗子
则我们按照递增求出来的表如下:
从上述例子中,我们得到两点启示:
- 对于某个,的值对任意都不会变化,则我们可以中止外层循环;
- 仅当在外循环的先前迭代中发生变化的时候,我们才通知其邻居节点,进行边的内层循环更新。
- 即我们表中的红色部分表示当前顶点的发生变化,我们需要计算其邻居节点的权值是否需要更新,即比较。
所以,按照所得到的启示,我们有如下新的流程
initialize d(*) = d(*,0);
//计算d(*) = d(*,n-1)
把源点放入list1;
for(int k=1;k<n;k++){
// 查看是否有其值发生变化的顶点
if(list1为空) 跳出循环,即没有这样的点;
while(list1 不空){
从list1中删除一个顶点u;
for(每一条边(u,v)){
d(v) = min{d(v),d(u)+cost(u,v)};
if(d(v)发生改变且v不在list2中) 把v加到list2;
}//for
}// while
list1 = list2;
list2清空;
}
这里之所以用两个列表,是考虑list1
表示上一次改变的顶点,list2
表示下一次改变的顶点。同时,我们可以引入一个bool数组inList2
表示顶点当前是否属于list2
,但是如果使用一个队列来存储每次更新的顶点,则不容易分清楚哪些顶点是这次已经存储过的。
3.Bellman-Ford算法的实现
图使用邻接矩阵表示:双列表实现
#include <iostream>
#include <string>
#include<vector>
#include<stack>
using namespace std;
#define MAX_VERTEX_NUM 7// 最大的顶点数目
#define INFITY 1000 // 表示权重无穷大
// 定义邻接矩阵
typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 表示两点之间的路径权重
typedef struct {
AdjMatrix matrix;
string vexs[MAX_VERTEX_NUM]; // 顶点向量
int vexnum, arcnum; // 顶点数和边的数目
}MGraph;
void show(vector<int> l) {
for (int i = 0; i<l.size(); i++) cout << l[i] << " ";
cout << endl;
}
//图的创建
void createMGraph(MGraph &G);
// 顶点的定位
int LocateVex(MGraph G, string v);
// 返回图G中u的第一个邻接点
int First(MGraph g, int u);
// 返回图G中V相对于u的下一邻接点
int Next(MGraph G, int v, int u);
// 最短路径
void BellmanFord(MGraph G, string s, int* d, int* p) {
/*
* s表示源点
* d 表示d(*,k)距离向量
* p(v) 表示v在最短路径上的直接前驱
*/
// 定义两个表,存储d值发生改变的顶点
vector<int> list1;
vector<int> list2;
// 定义顶点是否在list2中
int* inList2 = new int[G.vexnum];
// 初始化前驱数组,
// p[i] = 0 表示当前未到达该顶点或者无前驱(源点)
int i, j, k;
for (i = 0; i < G.vexnum; i++) {
p[i] = 0;
inList2[i] = 0; // 表示不在List2中
}
// 初始化d(*)
int indexSource = LocateVex(G, s); // 源点的下标
for (i = 0; i < G.vexnum; i++) {
d[i] = (i == indexSource ? 0 : INFITY);
}
// 将源点放入list1
list1.push_back(indexSource);
// 迭代n-1次
for (i = 1; i < G.vexnum; i++) {
if (!list1.size()) break; // 当没有新更新的顶点,则退出
for (j = 0; j < list1.size(); j++) {
int w = list1[j]; // 上一次发生更新的顶点
for (k = First(G, w); k != -1; k = Next(G, w, k)) { // j是其邻居
if (p[k] == 0 || (d[w] + G.matrix[w][k] < d[k])) {
p[k] = w;
d[k] = d[w] + G.matrix[w][k];
if (!inList2[k]) {
list2.push_back(k); // 加到下一次更新的顶点列表中
inList2[k] = 1;
}
}
}
}
list1 = list2; // 这次更新的顶点
list2.clear();
for (k = 0; k < G.vexnum; k++) inList2[k] = 0;
}
p[indexSource] = 0; // 表示没有前驱
delete[] inList2;
}
int main() {
MGraph G;
createMGraph(G);
int* d = new int[G.vexnum];
int* p = new int[G.vexnum];
BellmanFord(G, "V1", d, p);
for (int i = 0; i < G.vexnum; i++) cout << d[i] << " ";
cout << endl;
printf("V1-->V7的路径\n");
int index1 = LocateVex(G, "V7");
stack<int> pre;
for (; index1 != 0; index1 = p[index1]) pre.push(index1);
pre.push(index1);
while (!pre.empty()) {
int i = pre.top(); pre.pop();
cout << G.vexs[i] << "-->";
}
cout << "end" << endl;
system("pause");
return 0;
}
void createMGraph(MGraph &G) {
int i, j;
printf("输入顶点数和边的数目:\n");
cin >> G.vexnum >> G.arcnum;
// 初始化邻接矩阵
for (i = 0; i < G.vexnum; i++) {
for (j = 0; j < G.vexnum; j++) G.matrix[i][j] = INFITY;
}
printf("输入顶点信息\n");
for (i = 0; i < G.vexnum; i++) cin >> G.vexs[i];
printf("输入边的信息Vi-->Vj weight\n");
for (i = 0; i < G.arcnum; i++) {
string v1, v2;
cin >> v1 >> v2;
int l1 = LocateVex(G, v1);
int l2 = LocateVex(G, v2);
cin >> G.matrix[l1][l2];
}
}
int LocateVex(MGraph G, string u) {
int i;
for (i = 0; i < G.vexnum && G.vexs[i] != u; i++);
if (i == G.vexnum) return -1;
else return i;
}
int First(MGraph G, int u) {
int i;
for (i = 0; i < G.vexnum; i++) {
if (G.matrix[u][i] != INFITY) break;
}
if (i == G.vexnum) return -1; // 没有邻接点
else return i;
}
int Next(MGraph G, int v, int u) {
int index;
for (index = u + 1; index < G.vexnum && G.matrix[v][index] == INFITY; index++);
if (index == G.vexnum) return -1;
else return index;
}
4.单源单目的地的特殊写法
当我们要求单个源点S到单个目的地点D的最短路径的时候,也可以简化一个我们的递归表达式:
- 定义是从到的最短路径的长度;
- 则有是我们的目标值;
- 递归式如下:
递归解法:
int c(MGraph G,int s, int d, int* tail) {
if (s == d) return 0;
int i;
int sub = d; // 后继顶点
int min = INFITY;
for (i = First(G, s); i != -1; i = Next(G, s, i)) {
if (c(G,i,d,tail) + G.matrix[s][i] < min) {
min = c(G, i, d, tail) + G.matrix[s][i];
sub = i;
}
}
tail[s] = sub;
return min;
}
当然上述解法会有很多的重复计算,我们可以用一个数组cArray
存储计算过的值
int c(MGraph G,int s, int d, int* tail,int* cArray) {
// 计算过
if (cArray[s] != -1) return cArray[s];
// 递归终止条件
if (s == d) {
cArray[s] = 0;
return cArray[s];
}
int i;
int sub = d; // 后继顶点
int min = INFITY;
for (i = First(G, s); i != -1; i = Next(G, s, i)) {
if (c(G,i,d,tail,cArray) + G.matrix[s][i] < min) {
min = c(G, i, d, tail,cArray) + G.matrix[s][i];
sub = i;
}
}
tail[s] = sub;
cArray[s] = min;
return cArray[s];
}
非递归解法
类似于3,我们逆序求c(d)再求其前驱顶点的c(),直到求到c(s)为止。
这时我们可以维护一个列表list
表示当前更新的顶点,初始为目的地d
,则每次都判断list
中顶点的前驱顶点的距离是否更新,若更新,则将其也加入列表list。
为了更好的实现该算法,我们需要完成图中前驱顶点的查找和下一前驱顶点的查找。
参考资料
1.《数据结构、算法与应用 C++描述》 第19章
2.《算法导论》