最短路径之Bellman-Ford算法——动态规划

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Africa_South/article/details/90299584

Bellman-Ford算法主要针对带有负值的单源最短路径问题,当有向图带有其权小于0的边的时候,不能使用迪杰斯特拉算法,但是只要不是带负权的回路,我们依然可以使用Bellman-Ford算法

1.算法原理

Bellman-Ford算法的核心思想是动态规划,即我们需要定义子问题的状态动态规划递归式
讨论前提
如果图中共有 n n 个顶点,则所有的最短路径最多只有 n 1 n-1 条边。
如果一条路径具有 n n 条以上的边,则一定有环路(参考图的最小生成树性质)。
而由于环路的权值都不小于0,则去掉环路后的路径会更短,所以两个连通的顶点之间不存在含有环路的最短路径,且最多有n-1条边

动态规划公式
所以,我们像迪杰斯特拉算法一样关注最短路径的长度
d ( v , k ) d(v,k) 表示源点 s s 到顶点 v v 且最多含有 k k 条边的最短路径,于是 d ( v , n 1 ) d(v,n-1) 就是我们的目标。
首先,对于 k = 0 k=0 有, d ( v , 0 ) = { 0 , v = s , v s d(v,0) = \left\{\begin{matrix} 0 & ,v=s\\ \infty & , v \neq s \end{matrix}\right.
对于 0 < k n 1 0<k≤n-1 ,有
d ( v , k ) = m i n { d ( u , k 1 ) + c o s t ( u , v ) u v } d(v,k) = min\{d(u,k-1) + cost(u,v) | u是v的前驱顶点\}
即要求 s s v v 的最短路径,我们可以先求 s s u u 的最短路径。

2. 算法流程

有了动态规划公式,我们可以按照递归式子,逆序计算 d ( , 1 ) , d ( , 2 ) , . . . , d ( , n 1 ) d(*,1),d(*,2),...,d(*,n-1) 而得到结果。

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);
}

举个栗子
有向图
则我们按照 k k 递增求出来的 d ( , k ) d(*,k) 表如下:
Bellman-Ford计算过程
从上述例子中,我们得到两点启示:

  • 对于某个 k k d ( v ) d(v) 的值对任意 k k 都不会变化,则我们可以中止外层循环;
  • 仅当 d ( u ) d(u) 在外循环的先前迭代中发生变化的时候,我们才通知其邻居节点 v v ,进行边 ( u , v ) (u,v) 的内层循环更新。
  • 即我们表中的红色部分表示当前顶点的 d ( u ) d(u) 发生变化,我们需要计算其邻居节点 v v 的权值是否需要更新,即比较 d ( v ) d ( u ) + c o s t ( u , v ) 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的最短路径的时候,也可以简化一个我们的递归表达式:

  • 定义 c ( i ) c(i) 是从 i i d d 的最短路径的长度;
  • 则有 c ( s ) c(s) 是我们的目标值;
  • 递归式如下:
    Bellman-Ford方程
    递归解法:
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.《算法导论》

猜你喜欢

转载自blog.csdn.net/Africa_South/article/details/90299584