Dijkstra算法(单源最短路)

1.储存

这里仍用一个二维数组来储存顶点之间边的关系。
除此之外,我们还需要一个dis数组来储存1号顶点到其余各个顶点的初始路程。如:dis = {0,1,12,inf,inf,inf}(这里inf是无穷大)
我们将此时dis数组中的值称为最短路径的“估计值”(没有计算过最短路径之前1号顶点到其余各点之间的路程)。

2.松弛

既然是计算1号顶点到其余各点之间的最短路程,那就先找一个离1号顶点最近的顶点。通过数组可知离1号顶点最近的顶点是2号顶点。接下来看2号顶点有哪些出边呢?有2->3和2->4这两条边。先讨论通过2->3这条边能否让1号顶点到3号顶点的路程变短,也就是说现在来比较dis[3]和dis[2] + e[2][3]的大小。这里的dis[3]表示1号顶点到3号顶点的路程;dis[2] + e[2][3]中dis[2] 表示1号顶点到2号顶点的路程;e[2][3]表示2->3这条边。所以dis[2] + e[2][3]就表示从1号顶点到2号顶点,再通过2->3这条边到达3号顶点的路程。
我们发现dis[3] = 12,dis[2] + e[2][3] = 1 + 9 = 10,dis[3] > dis[2] + e[2][3],因此dis[3]要更新为10.这个过程有个专业术语叫做“松弛”,1号顶点到3号顶点的路程,即dis[3]通过2->3这条边松弛成功。
这就是Dijkstra算法的主要思想:通过“边”来松弛1号顶点到其余各个顶点的路程。

3.基本步骤

OK,现在来总结一下刚刚的算法。
**算法的基本思想是:**每次找到离源点(上面例子的源点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩散,最终得到源点到其余所有点的最短路径。
基本步骤如下:
1.将所有的顶点分为两部分:已知最短路程的顶点的集合P,和未知最短路径的顶点的集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book数组来记录哪些顶点在集合P中。例如,对一个顶点i,如果book[i]为1则表示这个顶点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。
2.设置源点s到自己的最短路径为0,即dis[s] = 0。若存在有源点能直接到达的顶点i,则把dis[i]设为e[s][i]。同时把所有其他(源点不能直接到达的)顶点的最短路径设置为inf(无穷大)。
3.在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u] + e[u][v]。如果这个值比目前已知的dis[v]的值要小,我们可以用新值来替代当前dis[v]中的值。
4.重复第三步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。

4.代码

//Dijkstra算法完整代码
#include <stdio.h>
#include <iostream>
#define inf 0x3f3f3f3f
using namespace std;
int main(){
	int e[10][10], dis[10], book[10], i, j, n, m, t1, t2, t3, u, v, min;
	//读入n,m,n表示顶点个数,m表示边的条数。 
	cin >> n >> m;
	//初始化
	for (int i = 1; i <= n; ++i){
		for (int j = 1; j <= n; ++j){
			if(i == j) e[i][j] = 0;
			else e[i][j] = inf;
		}
	} 
	//读入边
	for (int i = 1; i <= m; ++i){
		cin >> t1 >> t2 >> t3;
		e[t1][t2] = t3;
	} 
	//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
	for (int i = 1; i <= n; ++i){
		dis[i] = e[1][i];
	} 
	//book数组初始化
	for (int i = 1; i <= n; ++i){
		book[i] = 0;
	} 
	book[1] = 1;
	//Dijkstra算法核心语句
	for (int i = 1; i <= n - 1; ++i){
		min = inf;
		for (int j = 1; j <= n; ++j){//选择离源点最近的点u 
			if(book[j] == 0 && dis[j] < inf){//book[j] == 0语句判断j点是否在集合Q中,如果在集合Q中 
				min = dis[j];//更新离源点最近的点u 
				u = j;
			}
		}
		book[u] = 1; //则把它加入集合P 
		for (int v = 1; v <= n; ++v){//以u为顶点,uv为边开始进行松弛操作 
			if(e[u][v] < inf){
				if(dis[v] > dis[u] + e[u][v]) dis[v] = dis[u] + e[u][v]; 
			}
		} 
	} 
	//输出最终结果
	for (int i = 1; i <= n; ++i){
		cout << dis[i] << ' ';
	} 
	return 0;
} 

5.优化

通过上面的代码我们可以看出,这个算法的时间复杂度是O(N^2)。其中每次找到离1号顶点最近的顶点的时间复杂度是O(N),这里我们可以用“堆”来优化,使这一部分的时间复杂度降到O(logN)。另外对于边数M少于N2的稀疏图(我们把M远少于N2的图称为稀疏图,M比较大的图称为稠密图),我们可以用
邻接表来代替邻接矩阵,使得整个时间复杂度优化到O(M+N)logN。请注意!在最坏的情况下M就是N^2,这样的话O(M+N)logN要比N的平方还大,但是大多数情况下不会有那么多边,因此O(M+N)logN要比N的平方小很多。
用邻接表储存图的代码:

//用邻接表储存图的代码
	int n, m,d i;
	//u、v、w 数组的大小要根据实际情况来设置,要比m的最大值大1 
	int u[6], v[6], w[6];
	//first和next的数组大小也要根据实际情况来定,first要比n的最大值大1,next要比m的最大值大1
	int first[5], next[6];
	cin >> n >> m; 
	//初始化first数组下标1~n的值为-1,表示1~n顶点暂时没有边
	for (int i = 1; i <= n; ++i){
		first[i] = -1;
	} 
	for (int i = 1; i <= m; ++i){
		cin >> u[i] >> v[i] >> w[i];//读入每一条边 
		//下面两句是关键
		next[i] = firsy[u[i]];
		first[u[i]] = i; 
	}

这种方法为每个顶点i(i从1~n)都设置了一个链表,里面保存了从顶点i出发的所有的边。
首先我们需要给每条边进行1~m的编号。用u,v,w三个数组来记录每条边的信息。即u[i],v[i[,w[i]表示第i条边是从u[i]号顶点到v[i]号顶点(u[i]->v[i]),且权值为w[i]。first数组的1到n号单元格分别用来储存1到n号顶点的第一条边的编号,初始的时候因为没有边加入所以都是“-1”。即first[u[i]]保存顶点u[i]的第一条边的编号,next[i]储存“编号为i的边”的“下一条边”的编号。
图片看邻接表的实现:
读入一条边后:
在这里插入图片描述
读入第二条边之后:
在这里插入图片描述

读入第三条边后:
在这里插入图片描述
读入第四条边后:
在这里插入图片描述
读入第五条边后:
在这里插入图片描述

如何遍历每一条边呢?我们之前说过其实first数组储存的就是每个顶点i(i从1~n)的每条边。比如1号顶点的第一条边是编号为5的边(1 3 7),2号顶点的第一条边是编号为4的边(2 4 6),3号顶点没有出向边,4号顶点的第一条边是编号为2的边(4 3 8),那么如何遍历1号顶点的每一条边呢?也很简单,请看下图:

在找到1号顶点的第一条边之后,剩下的边都可以在next数组中依次找到。
代码如下:

//遍历1号顶点的所有边 
k = first[1];
while(k != -1){
	cout << u[k] << v[k] << w[k] << endl;
	k = next[k];
} 

细心的宝贝会发现,此时遍历某个顶点的边的时候遍历的顺序和读入的顺序正好相反。因为在每个顶点插入边的时候都是直接插入“链表”的首部而不是尾部。
遍历每个顶点的每条边,代码如下:

//遍历每个顶点的所有边 
for (int i = 1; i <= n; ++i){
	k = first[i];
	while(k != -1){
		cout << u[k] << v[k] << w[k] << endl;
		k = next[k];
	} 
}

6.总结

这是一个基于贪心策略的算法。每次新扩展一个路程最短的点,更新与其相邻的点的路程。当所有边权都为正时,由于不会存在一个更短的没有扩展过的点,所以这个点的路程永远不会被改变,因而保证了算法的正确性。
不过根据这个原理,求单源最短路径不能有负权边,因为扩展到负权边时,会产生更短的路程,有可能就已经破坏了已经更新的点路程不会改变的性质。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值