图解:最短路径之迪杰斯特拉算法

一时失言乱红尘 2023-03-13 13:12 67阅读 0赞

点击关注上方“五分钟学算法”,

设为“置顶或星标”,第一时间送达干货。

format_png

转自景禹

今天我们主要看一个最短路径算法,迪杰斯特拉算法(Dijkstra Algorithm)( 计算从某个源点到其他顶点的最短路径的算法 )。

小禹禹: 景禹,什么是最短路径,什么又是源点,还有最短路径算法有啥子用呢?

景禹: 小禹禹一下子提出这么多疑问,我们一个一个来看看吧~~

基础概念

什么是源点?

路径起始的第一个顶点称为源点(Source),最后一个顶点称为终点(Destination)。图下图中,我们用红色标注出的就可以认为是一个路径()的源点和终点,但不要有误区,其实图中的任何一个顶点都可作为源点或者终点,源点与终点只是相对一条路径而言的。

format_png 1

什么是最短路径?

对于无向图而言,从源点到终点的最短路径就是从源点到终点所包含的边最少的路径。我们只需要从源点出发对图做广度优先搜索,一旦遇到终点就终止。我们可以具体来看看如何得到无向图中源点到终点的最短路径。

第一步:遍历顶点:

format_png 2

第二步:遍历顶点的邻接顶点和(具体操作中我们使用队列来进行实现):

format_png 3

第三步:遍历顶点的邻接顶点和,遍历顶点的邻接顶点:

format_png 4

第四步:遍历顶点的邻接顶点和的邻接顶点:

format_png 5

第五步:遍历顶点的邻接顶点,发现正好是终点(Destination):

format_png 6

由此可以得到,图中从源点到终点的(第一条)一条最短路径 ():

format_png 7

最短路径的用处有哪些?

简单来说,我们要从大兴机场到北京天安门,如何规划路线才能换乘最少,并且耗时最少呢?这时候,最短路径算法就派上用场了,你每天使用的导航系统进行道路规划也同样依赖于底层的算法!虽然现实情况可能更复杂一些,但是学习最基础的算法对于我们日后的提升总有莫大的帮助。我们接着看今天的主角:迪杰斯特拉算法。

format_png 8

迪杰斯特拉算法

迪杰斯特拉算法是一个单源点最短路径算法,即该算法会求得从指定一个顶点(源点)到其余所有顶点的最短路径。

首先,引进一个辅助向量D,它的每一个分量表示 当前所找到的从源点到每一个顶点的最短路径的长度。它的初态为:若从到有弧(边),则为弧上的权值;否则为无穷(INF)。显然,长度为

的路径就是从出发的长度最短的一条最短路径。此路径为。

小禹禹:这都是什么呀,看不懂???

景禹: 那我们就直接看栗子吧,看完你绝对懂!

format_png 9

我们以上图为例来演示迪杰斯特拉算法的详细执行过程。

第一步:初始化辅助向量D,路径向量P 和 当前已求得顶点到的最短路径 的向量Final。

format_png 10

辅助向量D的初态为:若从到有弧,则为弧上的权值;否则为无穷(INF)。对应到图中,到有弧,则;到有弧,则;到其他顶点没有弧,则相应的用无穷(INF)表示。路径向量P用于存储最短路径下标的数组,初始时全部置为零;向量Final中值为1表示顶点到的最短路径已求得,到的最短路径当然是已求得,所以将设置为1。接下来就是迪杰斯特拉算法的核心了,认真看奥。

第二步:遍历源点, 找到源点的邻接顶点中距离最短的顶点,即,到的最短路径为1已经求出,更新.

format_png 11

第三步:遍历顶点,找到顶点的邻接顶点、、、(其中已经遍历过了,不需要考虑)。从到的距离是 3,所以到的距离是 1+3=4,小于辅助向量D中的距离 5,则更新;从到的距离是 3,所以到的距离是 1+7=8,小于辅助向量中的,则更新;从到的距离是 5,所以到的距离是 1+5=6,小于辅助向量中的,则更新;相应的将顶点、、的前驱结点更新为的下标 1。

format_png 12

接下来就是重复第二步、第三步。

第四步:遍历源点, 找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 13

第五步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 14

第六步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 15

第七步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 16

第八步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 17

第九步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 18

第十步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 19

第十一步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 20

第十二步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 21

第十三步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 22

第十四步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新.

format_png 23

第十三步:遍历顶点并更新辅助向量 D 和 路径向量 P 。

format_png 24

第十二步:找到从源点出发距离最短的且final=0的顶点,发现为, 更新。整个迪杰斯特拉算法终止。

format_png 25

根据路径向量我们则可以得到从源点到终点的最短路径,即图中红色线所标注的路径。

format_png 26

按照上面的思路,我们来看下面的代码将觉得豁然开朗了。

  1. #define MAXVEX 9
  2. #define INFINITY 65535
  3. typedef int Patharc[MAXVEX]; // 用于存储最短路径下标的数组
  4. typedef int ShortPathTable[MAXVEX]; // 用于存储到各点最短路径的权值和
  5. void ShortestPath_Dijkstar(MGraph G, int V0, Patharc *P, ShortPathTable *D)
  6. {
  7. int v, w, k, min;
  8. int final[MAXVEX]; // final[w] = 1 表示已经求得顶点V0到Vw的最短路径
  9. // 初始化数据
  10. for( v=0; v < G.numVertexes; v++ )
  11. {
  12. final[v] = 0; // 全部顶点初始化为未找到最短路径
  13. (*D)[V] = G.arc[V0][v]; // 将与V0点有连线的顶点加上权值
  14. (*P)[V] = 0; // 初始化路径数组P为0
  15. }
  16. (*D)[V0] = 0; // V0至V0的路径为0
  17. final[V0] = 1; // V0至V0不需要求路径
  18. // 开始主循环,每次求得V0到某个V顶点的最短路径
  19. for( v=1; v < G.numVertexes; v++ )
  20. {
  21. min = INFINITY;
  22. for( w=0; w < G.numVertexes; w++ )
  23. {
  24. if( !final[w] && (*D)[w]<min )
  25. {
  26. k = w;
  27. min = (*D)[w];
  28. }
  29. }
  30. final[k] = 1; // 将目前找到的最近的顶点置1
  31. // 修正当前最短路径及距离
  32. for( w=0; w < G.numVextexes; w++ )
  33. {
  34. // 如果经过v顶点的路径比现在这条路径的长度短的话,更新!
  35. if( !final[w] && (min+G.arc[k][w] < (*D)[w]) )
  36. {
  37. (*D)[w] = min + G.arc[k][w]; // 修改当前路径长度
  38. (*p)[w] = k; // 存放前驱顶点
  39. }
  40. }
  41. }
  42. }

时间复杂度分析

初始化辅助向量 D 和路径向量 P,以及final数组的时间复杂度为.

迪杰斯特拉算法的核心代码中,外层for循环从 1 遍历到 n-1,执行了n-1次,内层的两个for循环分别从0 遍历到 n-1,分别执行了n次,共执行了2n次;外层循环执行一次,内层循环执行2n次,则总的执行次数为(n-1)*2n,所以迪杰斯特拉算法的时间复杂度为.

细心的朋友肯定发现,我们在辅助向量D中查找从源顶点到最短路径未知顶点最短距离(也就是final=0的顶点)的顶点时,时间复杂度,更新选择出的顶点的邻接顶点时(内层循环的第二for循环)也耗时。因此我们在实现上可以使用小顶堆数据结构进行优化,这样就可以将时间复杂度降到。

迪杰斯特拉算法(堆优化)

要使用堆数据结构对迪杰斯特拉算法进行优化,我们可以直接使用C++的STL 中的 priority_queue 进行实现,注意我们下方的实现采用的是邻接表的存储结构(大家可以直接复制DeBug调试):

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. # define INF 0x3f3f3f3f
  4. typedef pair<int, int> iPair;
  5. class Graph
  6. {
  7. int V; // 顶点个数
  8. //带权图中需要保存顶点的权值。
  9. list< pair<int, int> > *adj;
  10. public:
  11. Graph(int V); // 构造器
  12. // 添加边和权值
  13. void addEdge(int u, int v, int w);
  14. // 打印源点s到其他顶点的最短路径
  15. void shortestPath(int s);
  16. };
  17. // 为邻接表分配空间
  18. Graph::Graph(int V)
  19. {
  20. this->V = V;
  21. adj = new list<iPair> [V];
  22. }
  23. void Graph::addEdge(int u, int v, int w)
  24. {
  25. adj[u].push_back(make_pair(v, w));
  26. adj[v].push_back(make_pair(u, w));
  27. }
  28. // 打印从源顶点到其他顶点的最短路径
  29. void Graph::shortestPath(int src)
  30. {
  31. // 创建一个优先队列实现小顶堆
  32. priority_queue< iPair, vector <iPair> , greater<iPair> > pq;
  33. // 创建一个距离向量,初始化为INF
  34. vector<int> dist(V, INF);
  35. // 将源顶点入堆,并将到自身的距离初始化为0
  36. pq.push(make_pair(0, src));
  37. dist[src] = 0;
  38. /*循环直到堆为空*/
  39. while (!pq.empty())
  40. {
  41. // 取出堆顶的顶点并保存在 u中
  42. int u = pq.top().second;
  43. pq.pop();
  44. // 设置迭代器,遍历顶点 u 的所有邻接顶点。
  45. list< pair<int, int> >::iterator i;
  46. for (i = adj[u].begin(); i != adj[u].end(); ++i)
  47. {
  48. // 获取邻接顶点的标签和权值,即weight(u,v)
  49. int v = (*i).first;
  50. int weight = (*i).second;
  51. // 如果从u到v存在一条最短路径且当前顶点u的最短距离+weight(u,v)dist[v]的值,则更新
  52. if (dist[v] > dist[u] + weight)
  53. {
  54. dist[v] = dist[u] + weight;
  55. pq.push(make_pair(dist[v], v));
  56. }
  57. }
  58. }
  59. // 打印最短路径
  60. printf("Vertex Distance from Source\n");
  61. for (int i = 0; i < V; ++i)
  62. printf("%d \t\t %d\n", i, dist[i]);
  63. }
  64. int main()
  65. {
  66. int V = 9;
  67. Graph g(V);
  68. g.addEdge(0, 1, 1);
  69. g.addEdge(0, 2, 5);
  70. g.addEdge(1, 2, 3);
  71. g.addEdge(1, 4, 5);
  72. g.addEdge(1, 3, 7);
  73. g.addEdge(2, 4, 1);
  74. g.addEdge(2, 5, 7);
  75. g.addEdge(3, 4, 2);
  76. g.addEdge(3, 6, 3);
  77. g.addEdge(4, 5, 3);
  78. g.addEdge(4, 6, 6);
  79. g.addEdge(4, 7, 9);
  80. g.addEdge(5, 7, 5);
  81. g.addEdge(6, 7, 2);
  82. g.addEdge(6, 8, 7);
  83. g.addEdge(7, 8, 4);
  84. g.shortestPath(0);
  85. return 0;
  86. }

总结

最短路径问题在日常生活中很普遍,今日主要讨论了迪杰斯特拉算法,一种计算从指定顶点到其他所有顶点的最短路径算法。实现方式上,我们可以采用最原始的思想进行实现,此外可以采用小顶堆进行优化,从而降低时间复杂度。但是有一个问题,我们如何计算 任意两点间的最短路径 呢?因为迪杰斯特拉算法仅仅只能计算从一个顶点到其他顶点的最短路径。待景禹改日再给你们道来。


推荐阅读

• C++是如何从代码到游戏的?• 告诉你一个学习编程的诀窍(建议收藏)• 自学编程的八大误区!克服它!• 新手如何有效的刷算法题(LeetCode)• 10款VS Code插件神器,第7款超级实用!• 在拼多多上班,是一种什么样的体验?我tm心态崩了呀!• 写给小白,从零开始拥有一个酷炫上线的网站!


欢迎关注我的公众号“五分钟学算法”,如果喜欢,麻烦点一下“在看”~

format_png 27

发表评论

表情:
评论列表 (有 0 条评论,67人围观)

还没有评论,来说两句吧...

相关阅读