Bellman-Ford算法—求解带负权边的最短路径

1.Dijkstra不能得到含有负权边图(这里就不是环路了)的单源最短路径

Dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i]<–dmin);但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin’),再通过一个负权边L(L<0),使得路径之和更小(dmin’+L<dmin),则dmin’+L成为最短路径,并不是dmin,这样Dijkstra就被囧掉了。

《Bellman-Ford算法—求解带负权边的最短路径》

比如上图:1—>2权值为5,1—>3权值为6,3—>2权值为-2,求1到2的最短路径时,Dijkstra就会贪心的选择权为5的1—>2,但实际上1—>3—>2才是最优的结果,这样Dijkstra算法就无法得到正确的结果。

实际上只要有权重为负权的回路在,就无法得到真正的最短路径,因为只要在负权回路上不断兜圈子,所得的最短路径长度可以任意小。

虽然得不到最短路径,但我们有时需要知道图中是否存在负权回路,而Bellman-Ford算法就可以帮我们解决这个问题。

2、Bellman-Ford算法

定义:如果从结点s到结点v的某条路径上存在权重为负值的环路(必须是环路,不同于负权边),我们定义《Bellman-Ford算法—求解带负权边的最短路径》详见CLRS图24-1.

Bellman-Ford算法返回一个布尔值,以表明是否存在一个从源节点可以到达的权重为负值的环路(这样我们就知道这个图是不存在最短路径的了)。

如果存在这样一个环路,算法将告诉我们不存在解决方案;如果没有这种环路存在,算法将给出最短路径和它们的权重。

代码如下:

#include <iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 99999;

// 边
typedef struct Edge{
	int u, v;    // 起点,重点
	int weight;  // 边的权值
}Edge;

Edge edge[maxnum];     // 保存边的值
int  dist[maxnum];     // 结点到源点最小距离

int nodenum, edgenum, source;    // 结点数,边数,源点

// 初始化图
void init( )
{
	// 输入结点数,边数,源点
	cin >> nodenum >> edgenum >> source;
	for(int i=1; i<=nodenum; ++i)
		dist[i] = maxint;
	dist[source] = 0;
	for(int i=1; i<=edgenum; ++i)
	{
		cin >> edge[i].u >> edge[i].v >> edge[i].weight;
		if(edge[i].u == source)          //注意这里设置初始情况
			dist[edge[i].v] = edge[i].weight;
	}
}

// 松弛计算
void relax(int u, int v, int weight)
{
	if(dist[v] > dist[u] + weight)
		dist[v] = dist[u] + weight;
}

bool Bellman_Ford()
{
	for(int i=1; i<=nodenum-1; ++i)
		for(int j=1; j<=edgenum; ++j)
			relax(edge[j].u, edge[j].v, edge[j].weight);
	bool flag = 1;
	// 判断是否有负环路
	for(int i=1; i<=edgenum; ++i)
		if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)
		{
			flag = 0;
			break;
		}
	return flag;
}
int main()
{
        init( );
	if(Bellman_Ford())                     //不存在负环路时才能输出最短(从源点到每个顶点都有一个最短路径)
	    for(int i = 1 ;i <= nodenum; i++)  //记住这种输出方法!
			cout << dist[i] << endl;
	return 0;
}

算法描述:
     1,.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
  2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
  3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。
 

对于Bellman-Ford算法我们有两个问题需要解决:

问题一:为什么要循环|V| -1次,即为什么算法要对图的每条边都进行了|V| -1 次 松弛操作?

描述性证明:
首先指出,最短路径肯定是个简单路径,不可能包含回路。如果包含回路,且回路的权值和为正的,那么去掉这个回路,可以得到更短的路径;如果回路的权值是负的,那么肯定没有解了。
其次,从源点s可达的所有顶点如果存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。图有|V| 个点,又不能有回路,所以最短路径最多|V| -1边(所有的顶点都在最短路径上),因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。

其实说白了,如果我们从源点开始严格按照图固有的层级顺序依次松弛所有的边,那边一次循环下来就可以得到最短路径,而无需循环|v|-1 次,但现在我们不能预先设定每条边松弛的先后顺序,即边的松弛顺序是随机的,我们就只能去做最保险的事情——循环|v|-1 次,这样,无论是以什么样的顺序来松弛所有的边,有一点可以保证就是每次循环至少会有一条边被松弛了,就像上面所说的那样,每次循环至少会把这棵最短路径树向外拓展一层(无论是以怎样“恶劣”的边的次序),那么最远的也就第|v|-1层(一般很少出现这种情况的,所以后面的循环会有大量的松弛操作是浪费的),循环|v|-1 次足以松弛到它。

看一幅图就什么都明白了:

《Bellman-Ford算法—求解带负权边的最短路径》

设源点为 s ,

1)如果我们以如下次序来松弛所有的边:( s,t )、( s,y )、( t,x )、( t,y )、( t,z )、( y,x )、( y,z )、( z,x )、( z,s )、( x,t ),即严格的层序顺序,结果一次循环就得到了最短路径;

《Bellman-Ford算法—求解带负权边的最短路径》

2)如果我们以如下次序来松弛所有的边:( z,x )、( z,s )、( x,t )、( y,x )、( y,z )、( t,x )、( t,y )、( t,z )、( s,t )、( s,y ),即最恶劣的次序来松弛所有的边,这时就不是一次循环可以解决的了(但至少每次循环会使最短路径树从源点向外扩展一层)。

另外,每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)
如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达
如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

问题二:如何保证算法的正确性,即存在负环路时,算法返回false,不存在负环路时,算法返回true?

分两点证明,1)不存在负环路时,都有 v.d < = u.d + w ( u , v )——即 所有的边都松弛到了,这一点我们在问题一已经论证。这时算法返回true,而这显然是成立的;2)存在负环路时,一定存在某条边使得 v.d >u.d + w ( u , v ),此时falg=0,即false. 举例如下

《Bellman-Ford算法—求解带负权边的最短路径》

此时,点A的值为-2,点B的值为5,边AB的权重为5,5 > -2 + 5. 检查出来这条边没有收敛。

注1:一定要区分负权边和负权环路!!!

注2:参考资料 Bellman-Ford算法

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/langqing12345/article/details/44625795
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞