今天介绍一种计算单源最短路径的算法Bellman-Ford算法,对于图G=(V,E)来说,该算法的时间复杂度为O(VE),其中V是顶点数,E是边数。Bellman-Ford算法适用于任何有向图,并能报告图中存在负环路(边的权重之和为负数的环路,这使得图中所有经过该环路的路径的长度都可以通过反复行走该环路而使路径长度变小,即没有最短路径)的情况。以后会介绍运行速度更快,但只适用于没有负权重边的图中的Dijkstra算法。Dijkstra算法可以参考我的下一篇博客 单源最短路径之Dijkstra算法
在介绍Bellman-Ford算法之前,先介绍在计算图的单源最短路径的各种算法中都会用到的松弛操作。
// 松弛操作,检查的距离是否比大,是则更新为
void relax(Vertex *u, Vertex *v, int w)
{
if (u->weight == INF || w == INF) return;
if (v->weight > u->weight + w)
{
v->weight = u->weight + w;
v->p = u;
}
}
Vertex是顶点的数据类型,是图G中的一条边。顶点Vertex的属性weight记录了该顶点当前距离源点的最短距离,p记录了顶点在其最短距离中的前一个顶点。松弛操作要做的工作就是检查路径Bellman-Ford算法的思想就是反复对图G中的边进行松弛操作,知道所有顶点到s的距离都被最小化为止。这里的图使用邻接表表示,下面给出图的定义和算法程序,Bellman-Ford算法需要的参数包括图g、权重矩阵w和源点编号s(顶点编号从1开始)。
typedef struct GNode
{
int number; // 顶点编号
struct GNode *next;
} GNode;
typedef struct Vertex
{
int number;
int weight; // 该顶点到源点的距离
struct Vertex *p;
} Vertex;
typedef struct Graph
{
GNode *LinkTable;
Vertex *vertex;
int VertexNum;
} Graph;
/**
* Bellman Ford 单源最短路径算法
* @return true 没有负环路; false 有负环路,最短路径构造失败
*/
bool Bellman_Ford(Graph *g, int **w, int s)
{
initialize(g, s);
GNode *linkTable = g->LinkTable;
for (int i = 1; i < g->VertexNum; i++)
{
// 反复将边加入到已有的最小路径图中,检查是否有更优路径
for (int j = 0; j < g->VertexNum; j++)
{
GNode *node = (linkTable + j)->next;
Vertex *u = g->vertex + j;
while (node != NULL)
{
Vertex *v = g->vertex + node->number - 1;
int weight = *((int*)w + j * g->VertexNum + node->number - 1);
relax(u, v, weight);
node = node->next;
}
}
}
// 通过检查是否都已达到最短路径来检查是否存在负环路
for (int j = 0; j < g->VertexNum; j++)
{
GNode *node = (linkTable + j)->next;
Vertex *u = g->vertex + j;
while (node != NULL)
{
Vertex *v = g->vertex + node->number - 1;
int weight = *((int*)w + j * g->VertexNum + node->number - 1);
if (v->weight > u->weight + weight)
{
return false;
}
node = node->next;
}
}
return true;
}
void initialize(Graph *g, int s)
{
Vertex *vs = g->vertex;
for (int i = 0; i < g->VertexNum; i++)
{
Vertex *v = vs + i;
v->p = NULL;
v->weight = INF;
}
(vs + s - 1)->weight = 0;
}
上述算法代码实现的Bellman-Ford算法进行了V次对所有边的松弛操作,这是考虑到了最坏情况,假设图G是一条单链,则从表头s到表尾的路径计算需要进行V次松弛操作。下面给出一个演示例子。
Graph graph;
graph.VertexNum = 5;
Vertex v[5];
Vertex v1; v1.number = 1; v1.p = NULL; v[0] = v1;
Vertex v2; v2.number = 2; v2.p = NULL; v[1] = v2;
Vertex v3; v3.number = 3; v3.p = NULL; v[2] = v3;
Vertex v4; v4.number = 4; v4.p = NULL; v[3] = v4;
Vertex v5; v5.number = 5; v5.p = NULL; v[4] = v5;
graph.vertex = v;
GNode nodes[5];
GNode n1; n1.number = 1;
GNode n2; n2.number = 2;
GNode n3; n3.number = 3;
GNode n4; n4.number = 4;
GNode n5; n5.number = 5;
GNode a; a.number = 2; GNode b; b.number = 4; n1.next = &a; a.next = &b; b.next = NULL;
GNode c; c.number = 3; GNode x; x.number = 4; GNode z; z.number = 5; n2.next = &c; c.next = &x; x.next = &z; z.next = NULL;
GNode d; d.number = 2; n3.next = &d; d.next = NULL;
GNode f; f.number = 5; GNode g; g.number = 3; n4.next = &f; f.next = &g; g.next = NULL;
GNode h; h.number = 1; GNode i; i.number = 3; n5.next = &h; h.next = &i; i.next = NULL;
nodes[0] = n1;
nodes[1] = n2;
nodes[2] = n3;
nodes[3] = n4;
nodes[4] = n5;
graph.LinkTable = nodes;
int w[5][5] = { 0, 6, INF, 7, INF,
INF, 0, 5, 8, -4,
INF, -2, 0, INF, INF,
INF, INF, -3, 0, 9,
2, INF, 7, INF, 0 };
int s = 1;
if (Bellman_Ford(&graph, (int **)w, s))
{
for (int i = 0; i < graph.VertexNum; i++)
{
if (i != s - 1)
{
Vertex *v = graph.vertex + i;
printf("路径长度为%d , 路径为 : ", v->weight);
while (v->p != NULL)
{
printf("%d <- ", v->number, v->p->number);
v = v->p;
}
printf("%d\n", s);
}
}
}
上面的例程构建的图如下图所示。
Bellman-Ford算法运行过程中各顶点v到源点s=1的距离变化如下所示。
0 INF INF INF INF
0 6 4 7 2
0 2 4 7 2
0 2 4 7 -2
以顶点1到顶点2的路径变化为例,对应上面距离变化的顺序,如下所示。
无路径 ---> <1,2> ---> <1,4,3,2> ---> <1,4,3,2>
算法运行的最终结果如下图所示。
完整的程序可以看到我的github项目 数据结构与算法
这个项目里面有本博客介绍过的和没有介绍的以及将要介绍的《算法导论》中部分主要的数据结构和算法的C实现,有兴趣的可以fork或者star一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~