单源最短路径之Dijkstra算法

原创 2016年06月01日 21:54:57

上一篇博客介绍了适用于任何有向图(即使存在负环路)的Bellman-Ford算法,对图G=(V,E)来说,该算法的时间复杂度为O(VE)。本文介绍时间复杂度为O(V^2+E)=O(V^2)的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);
		}
	}
上述例程构造的图就是上面给出的图,其运行结果如下所示。

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

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

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

贪心算法解决单源最短路径问题

贪心算法总是做出在当前看来最好的选择,也就是说贪心算法并不从整体最优考虑,它所做出的选择只是在某种意义上的局部最优选择。贪心算法的基本要素 1. 贪心选择性质 2. 最优子结构性质...

一个例子让你明白一个算法-Dijkstra(求源点到各顶点最短路径)

算法思想1.在一个图中,把所有顶点分为两个集合P,Q(P为最短路径集合,Q为待选集合),用dis数组保存源点到各个顶点的最短路径(到自身为0)。 2.初始化P集合,就是加入源点到该集合,并在mark...

算法导论--单源最短路径问题(Dijkstra算法)

转载请注明出处:勿在浮沙筑高台http://blog.csdn.net/luoshixian099/article/details/51918844单源最短路径是指:给定源顶点s∈Vs \in V到分...

【算法导论】单源最短路径之Dijkstra算法

Dijkstra算法解决了有向图上带正权值的单源最短路径问题,其运行时间要比Bellman-Ford算法低,但适用范围比Bellman-Ford算法窄。、 迪杰斯特拉提出的按路径长度递增次序来产生源点...

Dijkstra算法求单源最短路径

1.最短路径在一个连通图中,从一个顶点到另一个顶点间可能存在多条路径,而每条路径的边数并不一定相同。如果是一个带权图,那么路径长度为路径上各边的权值的总和。两个顶点间路径长度最短的那条路径称为两个顶点...

单源最短路径( Dijkstra算法)JAVA实现

单源最短路径( Dijkstra算法)JAVA实现 package dijkstra; public class Graph { final int max=100; /* * 顶点节点...

单源最短路径问题[Dijkstra实现]

单源最短路径问题[Dijkstra实现]一、问题 带权有向图G(E,V), 找出从给定源顶点s到其它顶点v的权最小路径。 “最短路径” = 最小权二、问题求解:求1到5的最短路径值? 三、执行过程: ...

单源最短路径(Dijkstra)——贪心算法

Dijkstra算法是解单源最短路径问题的贪心算法。其基本思想是,设置顶点集合点集合S并不断地做贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。初始时,S中仅含有源。...

单源最短路径Dijkstra算法C++实现

// 单源最短路径Dijkstra算法实现.cpp : Defines the entry point for the console application. // #include "stda...

单源最短路径 dijkstra算法实现

本文记录一下dijkstra算法的实现,图用邻接矩阵表示,假设图为无向图,并且连通,有向图,不连通图的做法类似。算法简述: 首先确定“单源”的源,假设是第0个顶点。 维护三个数组dist[], col...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:单源最短路径之Dijkstra算法
举报原因:
原因补充:

(最多只允许输入30个字)