之前提到Dijkstra算法不能解决权值为负的情况。
Bellman-Ford算法能在更一般的情况下解决最短路径问题,即:允许权值为负。
注意,最短路径问题实际上均不允许有负值回路(当然是从源点可达的),因为这时不存在最短路径。总会有更短的办法-多绕负边回路走几趟就是了。
首先介绍一下松弛技术(Relaxation)
对每个顶点,都设置一个属性d[v], 用来描述从源点s到v的最短路径上权值的上界。
一步松弛操作的结果可以看作是对约束 d[v]<=d[u] + w(u,v)的松弛,即:每次比较dv和du+w(u,v)及更新dv叫做一次
松弛操做。
通俗来说,松弛可以认为是求最短路径时,每一次优化路径的过程。
总结来看,引入松弛概念其实主要是为了抽象一类步骤优化操作,为了思考问题,写伪代码方便。比如Dijkstra写成含有Relax的算法形式为:
void
Dijkstra(Graph& g, VertexNode& s)
{
for each vertex v in g
{
v.distance = INFINITY;
v.known = false;
v.path = NULL;
}
s.distance = 0;
for (int i=0; i<g.vertexnum; i++)
{
vertex v = smallest unknown distance vertex;
if (v==NULL)
break;
else
v.known = true;
for each w adjacent to v
{
if (!w.known)
{
Relax(w,v);
}
}
}
}
这样,Relax部分优化操作可以独立思考具体书写步骤。
回到原话题,Dijkstra算法实际上相当于对每条边执行一次Relax操作,所以其算法复杂度为O(|E|+×)。也就是说,顶点known标记的改变,记录了每条边的松弛操作。而对于含有负边的图,known顶点会出现标记过早的情况,即一次Relax操作是不够的,所以Dijkstra算法不起作用。
Bellman-Ford算法的核心则是对每条边执行多次Relax操作,从而可以取消known的标记,因为顶点的known意向可能被之后的Relax操作改变。
Relax操作有一个非常重要的性质-路径松弛性质:如果p<v0,v1,v2,...,vk>是一个从v0到vk的最短路径,而p的边是按照<v0,v1>,<v1,v2>...<vk-1,vk>的顺序进行松弛的,那么d[vk]就是最短路径长度。这个性质的保持不受其他松弛操作的影响,即使他们与p的边上的松弛操作混在一起。
利用这个性质,我们很容易得出结论:对于一个图所有边均进行|V|-1次松弛操作,一定能得到最短路径。
所以可以得到伪代码:
BellmanFord(g, s)
{
for (int i=0; i < g.VertexNum; i++)
for each edge(u, v) in g
Relax(u, v);
}
算法复杂度为O(|E||V|)
另外还有一种利用类似广度优先搜索算法实现该算法:
这里其实相当于对上面的算法做了一个优化:如果某次对所有边进行Relax操作,没有任何d值变化,则可以立即退出迭代而不需要|V|-1次迭代都做完。
void BellmanFord(Graph& g, Vertex& s)
{
queue<vertex> q;
for each vertex v in g
{
v.distance = INFINITY;
v.path = NULL;
}
s.distance = 0;
q.enqueue(s);
while (!q.Empty())
{
v = dequeue(q);
for each w adjenct to v
{
Relax(w,v)
{
if (w not in q)
q.enqueue(w);
}
}
}
}
代码实现:
#include <iostream>
#include <deque>
#include <algorithm>
using
namespace
std;
#define MAX_VERTEX_NUM 20
#define INFINITY 2147483647
struct
adjVertexNode
{
int
adjVertexPosition;
int
weight;
adjVertexNode
*
next;
};
struct
VertexNode
{
char
data
[
2
];
int
distance;
VertexNode
*
path;
adjVertexNode
*
list;
};
struct
Graph
{
VertexNode
VertexNode
[
MAX_VERTEX_NUM
];
int
vertexNum;
int
edgeNum;
};
void
CreateGraph (
Graph
&
g)
{
int
i
,
j
,
edgeStart
,
edgeEnd
,
edgeWeight;
adjVertexNode
*
adjNode;
cout
<<
"Please input vertex and edge num (vnum enum):"
<<
endl;
cin
>>
g
.
vertexNum
>>
g
.
edgeNum;
cout
<<
"Please input vertex information (v1)
/n
note: every vertex info end with Enter"
<<
endl;
for (
i
=
0;
i
<
g
.
vertexNum;
i
++)
{
cin
>>
g
.
VertexNode
[
i
].
data;
// vertex data info.
g
.
VertexNode
[
i
].
list
=
NULL;
}
cout
<<
"input edge information(start end weight):"
<<
endl;
for (
j
=
0;
j
<
g
.
edgeNum;
j
++)
{
cin
>>
edgeStart
>>
edgeEnd
>>
edgeWeight;
adjNode
=
new
adjVertexNode;
adjNode
->
weight
=
edgeWeight;
adjNode
->
adjVertexPosition
=
edgeEnd
-
1;
// because array begin from 0, so it is j-1
// 将邻接点信息插入到顶点Vi的边表头部,注意是头部!!!不是尾部。
adjNode
->
next
=
g
.
VertexNode
[
edgeStart
-
1
].
list;
g
.
VertexNode
[
edgeStart
-
1
].
list
=
adjNode;
}
}
void
PrintAdjList(
const
Graph
&
g)
{
for (
int
i
=
0;
i
<
g
.
vertexNum;
i
++)
{
cout
<<
g
.
VertexNode
[
i
].
data
<<
"->";
adjVertexNode
*
head
=
g
.
VertexNode
[
i
].
list;
if (
head
==
NULL)
cout
<<
"NULL";
while (
head
!=
NULL)
{
cout
<<
head
->
adjVertexPosition
+
1
<<
" ";
head
=
head
->
next;
}
cout
<<
endl;
}
}
void
DeleteGraph(
Graph
&
g)
{
for (
int
i
=
0;
i
<
g
.
vertexNum;
i
++)
{
adjVertexNode
*
tmp
=
NULL;
while(
g
.
VertexNode
[
i
].
list
!=
NULL)
{
tmp
=
g
.
VertexNode
[
i
].
list;
g
.
VertexNode
[
i
].
list
=
g
.
VertexNode
[
i
].
list
->
next;
delete
tmp;
tmp
=
NULL;
}
}
}
void
BellmanFord(
Graph
&
g
,
VertexNode
& s)
{
deque
<
VertexNode
*>
q;
for (
int
i
=
0;
i
<
g
.
vertexNum;
i
++)
{
g
.
VertexNode
[
i
].
distance
=
INFINITY;
g
.
VertexNode
[
i
].
path
=
NULL;
}
s
.
distance
=
0;
q
.
push_back(
&s);
int
counter
=
0;
while(
!
q
.
empty())
{
VertexNode
*
v
=
q
.
front();
q
.
pop_front();
if(
v
==
NULL)
break;
adjVertexNode
*
head
=
v
->
list;
while (
head
!=
NULL)
{
VertexNode
*
w
=
&
g
.
VertexNode
[
head
->
adjVertexPosition
];
if(
v
->
distance
+
head
->
weight
<
w
->
distance)
{
w
->
distance
=
v
->
distance
+
head
->
weight;
w
->
path
=
v;
if (
find(
q
.
begin
(),
q
.
end
(),
w)
==
q
.
end())
{
q
.
push_back(
w);
}
}
head
=
head
->
next;
}
counter
++;
if (
counter
>
g
.
vertexNum
*
g
.
edgeNum)
{
cout
<<
"This graph has minus value loop!"
<<
endl;
exit(
1);
}
}
}
void
PrintPath(
Graph
&
g
,
VertexNode
*
source
,
VertexNode
*
target)
{
if (
source
!=
target
&&
target
->
path
==
NULL)
{
cout
<<
"There is no shortest path from "
<<
source
->
data
<<
" to "
<<
target
->
data
<<
endl;
}
else
{
if (
target
->
path
!=
NULL)
{
PrintPath(
g
,
source
,
target
->
path);
cout
<<
" ";
}
cout
<<
target
->
data ;
}
}
int
main(
int
argc
,
const
char
**
argv)
{
Graph
g;
CreateGraph(
g);
PrintAdjList(
g);
VertexNode
&
start
=
g
.
VertexNode
[
0
];
VertexNode
&
end
=
g
.
VertexNode
[
6
];
BellmanFord(
g
,
start);
cout
<<
"print the shortest path from v1 to v7"
<<
endl;
PrintPath(
g
,
&
start
,
&
end);
cout
<<
endl;
DeleteGraph(
g);
return
0;
}
其中判断含有负值回路的方法很多,这里采用认为最多会有|V|×|E|次操作,否则存在负值回路。
使用 queue也是可以的,但要注意 relax 内部判断要写成:
if (find(q._Get_container().begin(), q._Get_container().end(), w) == q._Get_container().end())
略显臃肿。 而实际上 queue也是一个container adapter,他内部封装了deque。
运行示例:
1 含有负值权边,但不含负值回路。
运行结果
2 含有负值回路:v6->v7->v6 (-10)
运行结果