单源最短路径之Dijkstra算法

上一篇博客介绍了适用于任何有向图(即使存在负环路)的Bellman-Ford算法,对图G=(V,E)来说,该算法的时间复杂度为O(VE)。本文介绍时间复杂度为O(V^2+E)=O(V^2)的Dijkstra算法,但只适用于没有负权重边的有向图。

基本上所有计算图的最短路径的算法都基于一个性质:一条最短路径的子路径肯定也是一条最短路径。该性质用反证法就可以轻易证明。也就是说,一条最短路径要么只包含一条直接相连的边,要么就要经过一条或多条到达其它顶点的最短路径,于是我们就可以基于一条最短路径构造出另一条到达另一个顶点的最短路径,只要其长度比原有路径小。

先给出一个例子来说明Dijkstra算法的运行过程。

《单源最短路径之Dijkstra算法》

首先要明确的一点是,虽然上图中存在权重为负值的边,但是不存在负环路,这说明可以应用Dijkstra算法计算单源最短路径,否则只能使用Bellman-Ford算法。Bellman-Ford算法可以参考我上一篇博客单源最短路径之Bellman-Ford算法

下面给出一个表格来说明Dijkstra算法计算从顶点1到其它顶点的最短路径的过程。改表格每一列为算法运行的一步,每一个单元格为顶点1到某个顶点的当前路径及其长度。

<1,2> 6——————
<1,3> INF<1,2,3> 11<1,4,3> 10——
<1,4> 7<1,4> 7————
<1,5> INF<1,2,5> 10<1,2,5> 10<1,2,5> 10

下面来解释一下算法的过程。

首先,根据权重图初始化顶点1到各个顶点的距离,没有直接相连的顶点距离为无穷大INF,即第一列。

然后,进入循环,选出当前距离最短的路径<1,2>,其长度为6。此时我们可以确定顶点1到顶点2的最短路径肯定为<1,2>。可以用反证法简单地证明一下。假设<1,2>不是顶点1到顶点2的最短路径,那么肯定存在一个或者一些中间顶点u1,…,un构成一条最短路径<1,u1,…,un,2>,该路径的长度肯定比<1,2>小,那么边<1,u1>肯定比边<1,2>小,这与<1,2>是图中权重最小的边相违背。因此,原问题得证。Dijkstra算法自底向上构造最短路径的方法的正确性也可以这样证明。再说回算法流程。选出路径<1,2>之后,用该路径来构造通往其它顶点的路径,如果新路径比原来的路径要短,则更新之。例如,顶点1到顶点3原来是没有路径的,但是<1,2,3>则是一条存在于图中的路径,于是更新之。而顶点1到顶点4的原路径为<1,4>,其长度为7,基于<1,2>构造的新路径<1,2,4>的长度为14,显然不必=比原路径短,于是忽略之。

下一步,继续循环,从第二列的路径中选出最短的,即路径<1,4>,同样该路径为顶点1到顶点4的最短路径,证明同上。然后使用该路径去更新其它顶点的路径。

Dijsktra算法一直循环上述工作,知道所有顶点都已经找到最短路径。循环次数为V次,因为要构造V个顶点的最短路径,而且一次循环就可以构造一个顶点的最短路径。

在给出Dijsktra算法的代码前,先说明一下图的结构定义。

typedef struct GNode
{
	int number;	// 顶点编号
	struct GNode *next;
} GNode;

typedef struct Vertex
{
	int number;
	int weight;		// 在计算最短路径时为该结点到源点的距离
	int f;			// 标记结点是否已经搜寻最短路径完毕
	struct Vertex *p;
} Vertex;

typedef struct Graph
{
	GNode *LinkTable;
	Vertex *vertex;
	int VertexNum;
} Graph;

图用邻接表表示,邻接表实际上是一个数组,其每个元素为GNode,每一个GNode记录了顶点的编号。Vertex是顶点的数据类型,其属性意义如注释所示。

下面给出Dijkstra算法的C实现程序。其中输入参数为图g、权重矩阵w,源点编号s,注意,图的顶点的编号从1开始。

/**
* Dijkstra算法,要求所有边的权重均为非负值,结点的编号从1开始
*/
void dijkstra(Graph *g, int **w, int s)
{
	initialize(g, s);

	Vertex *vs = g->vertex;
	GNode *linkTable = g->LinkTable;
	for (int i = 1; i < g->VertexNum; i++)
	{
		int min = INT_MAX;
		int number = 0;
		// 找到目前距离s最短的顶点,该顶点搜索最短距离结束
		for (int j = 0; j < g->VertexNum; j++)
		{
			if (min > (vs + j)->weight && (vs + j)->f == 0)
			{
				min = (vs + j)->weight;
				number = j + 1;
			}
		}
		if (number == 0)	return;
		(vs + number - 1)->f = 1;
		// 加入到各个与number相连的顶点中做松弛更新操作
		GNode *node = (linkTable + number - 1)->next;
		Vertex *u = vs + number - 1;
		while (node != NULL)
		{
			Vertex *v = vs + node->number - 1;
			int weight = *((int*)w + (number - 1)*g->VertexNum + node->number - 1);
			relax(u, v, weight);
			node = node->next;
		}
	}
}

上述程序使用到的一些方法定义如下。

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;
		v->f = 0;
	}
	(vs + s - 1)->weight = 0;
}

// 松弛操作,检查<s, ..., v>的距离是否比<s, ..., u, v>大,是则更新<s, ..., v>为<s, ..., u, v>
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;
	}
}

简单的解释一下上述代码的工作流程。

首先,初始化各个顶点到源点的距离为无穷大INF,并记录在顶点的weigt属性中,源点s的weight初始化为0。这个工作在initialize方法中完成。

然后,进入Dijkstra算法的循环流程,选出当前距离源点s最短的顶点,并将该顶点加入到最短路径图中,具体做法就是将顶点的f属性置为1。然后再使用该路径去跟其它顶点做松弛操作,这个操作在relax方法中实现,松弛操作就是检查<s, …, v>的距离是否比<s, …, u, v>大,是则更新<s, …, v>为<s, …, u, v>,其实这就是上面对Dijkstra算法工作流程对路径更新的操作的实现。

循环一直持续到所有顶点都已经被加入最短路径图中,即所有顶点的f属性都为1,算法结束。

下面给出测试上述程序的例程。

	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;
	dijkstra(&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);
		}
	}

上述例程构造的图就是上面给出的图,其运行结果如下所示。

《单源最短路径之Dijkstra算法》

上述介绍的算法实现可以进一步优化,要优化的地方主要在于选出最小距离的顶点这一块。上述代码只是简单地使用线性遍历去取出最小的元素,实际上如果使用二叉堆来实现可以得到更快的运行速度。

完整的程序可以看到我的github项目 数据结构与算法

这个项目里面有本博客介绍过的和没有介绍的以及将要介绍的《算法导论》中部分主要的数据结构和算法的C实现,有兴趣的可以fork或者star一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~

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