狄克斯特拉算法(Dijkstra)——算法思想及代码实现

这个算法可是属于真正的老经典算法了,这学期离散数学里学到的唯一一个算法也就是这个Dijkstra算法,这个算法实际上就有贪心算法的味道在里面,即每次都确定一个顶点的最优路径,直到遍历全图。由于本人水平真的过低,这学期在离散数学中学这个算法的时候真的是花了很多很多时间才弄明白算法原理,曾一度怀疑人生并且无数次想要撕碎这个课本。我真的搞不懂明明很简单的一个算法,为什么放到教科书上面会给花里胡哨地加上一些专有名词,然后变得极度抽象,我是真的是很无语。。。个人感觉我们学校订的教科书编的不是很好(纯粹个人感觉。)

这边我尽量用最简洁易懂的方式,将这个算法表示清楚。因为我太清楚深受教科书毒害的学生们的感受了。。

首先,我们要明白,这个算法到底有什么用?(学一个新东西,要么是知道它有什么用(功利化学习),要么就是很喜欢它,就是想学(去功利化),当然如果你能做到二者兼顾,那最好了)这个算法,是求最短路径的,也就是求一个点到另一个点的最短路径,这里的最短路径指的是权值大于等于0的路,为什么是这样,这个我会在后文说到。至于为什么是带权,是为了与BFS广度优先搜索算法区分,BFS也是求最短路径,但是求的是不带权的路,也就是段数最少的路径。

在这之后,我们就可以开始学习这个算法了,这个算法的核心思想完全可以用一句话概括,“每次都确定初始点到一个顶点的最短路径,直到遍历全部顶点”。因为如果直接阐述思想,会过于抽象,难以理解,因此我这边就打算结合例子解释。这是我从书上找到的一个比较好的例子,这个例子弄懂了基本上这个算法也就掌握了。
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第1张图片
首先,我们要明确我们的目标,我们要用Dijkstra算法算出b点到图上所有点的最短路径。在明确了目标之后,我们要先定义一个map cost,存储从初始点点到各点的当前最短距离(当然这里不一定要用map,思想一样就可以,我这里用map是为了更好地理解),然后就是让计算机存储图的信息,这里我选择用map > graph,(是不是感觉这里有点复杂。。其实完全不是的,这里完全就可以看成一个二维数组,a[x][y],里面存储的值是x到y的距离),再然后就是我们需要再定义一个map parents,存储最短路径上一个顶点的前一个顶点,也就是存储父节点,用来进行回溯,这样才能把最短路径经过的节点给一一求出来(如果你不需要求最短路径经过的节点,这个就不需要定义了,当然,最好还是要求出来,这样才能让这个算法更加elegant)。最后就是定义一个vector< char > over,用来存储已经确定好最短路径的顶点(这是Dijkstra算法思想中最关键的一环,就是你每次确定好的那个最短路径的顶点,它的最短路径到程序结束都不会再改变了,因此将其放到vector中,避免再对它进行操作),做好了上述三步,就可以开始我们的算法了。

根据上述三步,我们一开始确定的cost散列表(第一列是节点,第二列是初始点到各个节点的当前最短距离),如下·狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第2张图片
这里切记,一开始我们只能确定b点到b点的距离是0(这是由于程序编写的需要),其他点默认正无穷。 而我们每一次都需要在cost里面找到一个最短的距离,通过这个最短距离确定一个节点。然后这就算确定了一个顶点的最短路径,也就是确定了b点到b点的最短路径,将其加入到over容器中,之后就不再需要求b点到b点的最短路径了(这个是不是在我们人看起来很傻。。其实我们的目的是让计算机识图,因此要把计算机当成一个蠢的不能再蠢的人看待

之后我们就要开始更新这个cost散列表了,这也是Dijkstra算法很关键的一步,就是由于我们已经确定了到b点的最短路径,因此我们可以搜索b点到各个直接相邻的点的权值(也就是相邻边的长度),让已经确定的cost中b点的最短路径径加上权值,就能求出来b点到各个相邻点的距离(记住,这里是一个距离,不是最短距离,如果要是最短距离,一定一定要比较才能得出),此时我们将这个距离和我们存储在cost散列表里的当前最短距离比较,如果这个距离比当前最短距离小,就进行更新,将cost里面对应位置的值变成这个距离,然后把b设定为这个节点的父节点。在更新完后,我们的cost散列表就变成了这样,
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第3张图片
b节点后面加上*就表示它是已经确定的节点了(也就是已经求出了初始点到这个节点的最短路径了),之后再在cost里面找,到一个顶点的最短路径,就不再需要考虑它了,切记切记!

之后就非常简单了,就是重复上面的步骤。再从cost里面找cost里面最小的值代表的节点(除已经确定的节点以外),找出这个节点,然后重复上面步骤就可以。这次我们找出的节点是c节点,然后cost散列表更新后就变成,
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第4张图片
之后就是重复到所有顶点后面都加上*后,这个算法就算结束了,然后cost最终存储的值就是初始点到各个点的最短路径,若想具体求出,通过parents回溯即可。

这里我给出参考书上的解析图,
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第5张图片
这里面每一行代表一次计算,每一次计算确立一个顶点,找到一条最短路径,然后括号的第一个参数是初始点到这个点的当前最短距离,也就是cost存储的值,然后第二个参数就是存储这个节点的父节点,λ表示暂未确定父节点。

最终cost的结果如下,
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第6张图片
具体路径如下,
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第7张图片
是不是看到这里感觉这个算法难度还好,不是很难。但是这个算法有几个很有想法的点,如果你有真正的去思考这个算法,你应该可以感受到。一,为什么每次确定下来的那个节点,之后就不需要再需要对它进行更新了?二,为什么就能很肯定每次确定下来的节点它对应的cost值就是最终的最短路径?三,为什么每次只能确定一个节点?四,为什么这个算法无法解决有负权边的情况?五,为什么要根据每次确定的节点对cost进行更新?想清楚了这几个问题,你才能能说真正的懂这个算法,要不然永远只能根据框架去套它,一辈子在门外徘徊。

首先,前四个问题其实是统一的,你只要弄清楚其中任何一个,其他几个就豁然开朗了。我这边就从第一个问题切入,首先要明白我们每次计算是如何确定那一个节点的?是在cost当前最短路径中找到目前最快能到达的节点(也就是目前初始节点到这个节点的距离最短),而当我们找到了这个节点,我们就当在cost中存储的值是初始节点到这个节点的最短路径,后续就不用再对这个节点在cost中的值进行更新了。这是为什么?

就是因为我们伟大的Dijkstra算法只应用于权值大于等于0的图,回到这个问题的一开始,我们每次要确定的节点是在cost当前最短路径中找到目前最快能到达的节点,如果我之后又能找到其他到这个节点的路径,那么我能找到的到这个节点的除cost中的最短路径的路径必然是cost中的未确定节点中除这个节点以外的节点对应的值再加上一个连接这俩个节点的边的权值(为什么这里说是cost中的未确定的节点,因为未确定的节点已经更新过了,我们不用再管它,若是你读了后面问题五的解释,你再回过头来仔细想想,就能明白,其实经过那些已经确定的节点的最短路径加上一条连接这俩个节点的边得到的值我们已经求过了!),而我们这次确定的节点中cost的值已经是除未确定节点中最小的了,其他节点再加上权值大于等于0的边,必然会比这个值大的!这里我要举个例子,因为这个是比较难理解的点,空讲的话就会有点抽象。

狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第8张图片
这里我定义的初始节点是家,根据算法,我第一次确定的节点是家,然后第二次确定的节点就是学校,也就是说到学校的最短路径是2,那有没有可能将到学校的路径缩短到少于2呢?答案是完全不可能,因为你到除家以外的其他节点已经要花比到这个节点更长的距离了,再加上大于等于0的权边,就完全不可能实现了,因此我上面说的一大段话就是说明这个道理。

至于最后的第五个问题,为什么要根据每次确定的节点对cost进行更新,这是因为,一个节点的最短路径必然是与它相邻节点的最短路径加上相邻边之后得到的。我们确定的这个节点是我们已经知道了它的最短路径,然后我们根据这个最短路径进行扩展,试图根据这个最短路径扩展到其他节点,得到到其他节点的路径(但是这个不一定是最短路径,这是很容易犯的错误,我之前也懵逼了很久,因为原先cost里面存的那个值也是与它相邻节点的最短路径加上相邻边之后得到的),根据上面我对问题一的解释,我们每次最终确定下来的节点它对应的cost值就是最终的最短路径,然后我们在确定了一个节点之后就需要确定下一个节点,直到所有节点都被确定,

那么,我们就需要时刻保持cost中存储的是初始节点到各个节点的当前最短路径,而每当我们确定下一个节点之后,我们就可以利用与这个节点相邻的边的信息,从而得到如上面所说的到其他节点的路径长度,而将其求出来的路径长度和我们cost里面存储的路径长度比较,更短的那个就是当前最短路径。

因此,从这里可以看出,根据每次确定的节点对cost进行更新的很大一部分原因就是在合理利用所有边的信息的情况下,时刻保持cost中存储的是初始节点到各个节点的当前最短路径。因为当前最短路径只有俩种可能,一是根据上一个已经确定的节点进行扩展得到的路径,二是之前已经存储到cost中的当前最短路径(这个是根据上一个之前已经确定的节点进行扩展得到的路径)。(一个节点的最短路径必然是它与相邻节点的最短路径加上相邻边之后得到的,这个是最最最最重要的!

如果不是根据确定的节点对cost进行更新的话,那么我们就会将第一个情况变成根据未确定的节点进行扩展得到的路径,而到未确定的节点的最短路径我们还没有求出来,也就是得到了一条很有可能不是由最短路径加上相邻边得到的路径,这就会导致算法出错了,使得我们有可能漏掉更短的最短路径,从而有可能让cost散列表没有处于“时刻存储的是当前最短路径”的情况,最终让算法执行失败。

额,可能是我的水平实在过低,我原本的想法是尽可能简洁易懂,但是一不小心还是打了这么多字才把我认为核心的地方讲完,可能在某些人看来是啰里啰唆搞了半天吧。。。不过我还是很开心的,因为我在解释这五个问题的同时,也对我之前学习的知识进行重塑,也让我对这个算法有了进一步的认识,很多之前没有想清楚的地方也都豁然开朗了,果然,学无止境这个成语真的很有道理。。。

真正弄懂了这个算法的思想之后,剩下的就是相对比较简单的代码实现了。我这边就直接给出代码,

#include
using namespace std;
int n, m;
int find_lowest_cost(map<int, int> cost, vector<int>& over)//找到当前开销最小的节点(不包括over中的节点)
{
     
	int low;
	for (int i = 1; i <= n; i++)
	{
     
		if (find(over.begin(), over.end(), i) == over.end()) low = i;
	}
	for (int i = 1; i <= n; i++)
	{
     
		if (cost[i] < cost[low] && (find(over.begin(), over.end(), i) == over.end())) low = i;
	}
	return low;
}
int main()
{
     
	int x, y, w, low;
	long long sum = 0;
	map<int, map<int, int> > graph;
	map<int, int>cost;
	map<int, int>parents;
	vector<int>over;
	while (cin >> n >> m)
	{
     
		while (m--)	//输入图的信息
		{
     
			cin >> x >> y >> w;
			if (graph[x][y] > 0)
			{
     
				if (graph[x][y] > w) graph[x][y] = w;
			}
			else  graph[x][y] = w;
		}
		cost[1] = 0;
		for (int i = 2; i <= n; i++) cost[i] = 10001; //10001是因我我假设最大边为10000,设为10001就表示无法直接到达这个节点
		low = 1;
		over.push_back(low); 
		for (int i = 1; i <= n; i++)	//每次循环代表一次更新,以及找到一个节点的最终的最短路径
		{
     
			for (int i = 1; i <= n; i++)	//更新cost
			{
     
				if ((graph[low][i]>0) && (graph[low][i] + cost[low] < cost[i]))
				{
     
					cost[i] = graph[low][i] + cost[low];
					parents[i] = low;	//父节点更新必须放到这里,做到时刻更新 
				}
			}
			low = find_lowest_cost(cost, over);
			over.push_back(low);
		}
		int x=5;
		int y;
		while (x != 1&& graph[parents[x]][x]>0)	//判断路径是否存在,根据回溯计算长度,这里不用回溯直接用cost中的值也是可以的
		{
     
			y = parents[x];
			sum = sum + graph[y][x];
			x = parents[x];
		}
		if (x==1) cout << sum << endl;
		else cout << "Sorry" << endl;
	}
}

这个代码我是根据这道题目打下来的,主要是为了实现这个算法的代码找的这道题目,不过我没有仔细做这道题,只是利用它实现了Dijkstra算法。

题目如下

5855: 数据结构实验:最短路

时间限制(普通/Java):1000MS/3000MS 内存限制:65536KByte
总提交: 177 测试通过:26

描述

给定n个点的带权有向图,若从顶点x到顶点y之间存在一条路径,那么这条路径的长度定义为路径上各条边的权值之积。

现在请你求出从顶点1到顶点n的最短路径。

输入

第一行为两个正整数n和m(n<=1000,m<=5000),n表示顶点数,m表示边数。

接下来有m行,每行三个正整数x,y,w,表示顶点x到y有一条边权为w的边。

1<=x, y<=n,w不大于10000。

两个顶点之间可能存在多条边。

输出

输出题目定义的最短路径值,由于数可能很大,因此你只需要输出总共有几位数即可。

如果不存在路径,则输出Sorry。

样例输入

3 3
1 2 3
2 3 3
1 3 11

样例输出

1

提示

最短路径为9,1位,因此输出1。

如果觉得有帮助,可以关注一下我的公众号,我的公众号主要是将这些文章进行美化加工,以更加精美的方式展现出来,同时记录我大学四年的生活,谢谢你们!
狄克斯特拉算法(Dijkstra)——算法思想及代码实现_第9张图片

你可能感兴趣的:(算法,算法,dijkstra)