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

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

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

转自景禹

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

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

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

基础概念

什么是源点?

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

什么是最短路径?

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

第一步:遍历顶点

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

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

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

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

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

最短路径的用处有哪些?

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

迪杰斯特拉算法

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#define MAXVEX 9
#define  INFINITY 65535

typedef  int  Patharc[MAXVEX]; // 用于存储最短路径下标的数组
typedef int  ShortPathTable[MAXVEX]; // 用于存储到各点最短路径的权值和

void ShortestPath_Dijkstar(MGraph G, int V0, Patharc *P, ShortPathTable *D)
{
  int v, w, k, min;
  int final[MAXVEX]; // final[w] = 1 表示已经求得顶点V0到Vw的最短路径
  
  // 初始化数据
  for( v=0; v < G.numVertexes; v++ )
  {
    final[v] = 0; // 全部顶点初始化为未找到最短路径
    (*D)[V] = G.arc[V0][v]; // 将与V0点有连线的顶点加上权值
    (*P)[V] = 0; // 初始化路径数组P为0
  }
  (*D)[V0] = 0; // V0至V0的路径为0
  final[V0] = 1; // V0至V0不需要求路径
  
  // 开始主循环,每次求得V0到某个V顶点的最短路径
  for( v=1; v < G.numVertexes; v++ )
  {
    min = INFINITY;
    for( w=0; w < G.numVertexes; w++ )
    {
      if( !final[w] && (*D)[w]<min )
      {
        k = w;
        min = (*D)[w];
      }
    }
    final[k] = 1; // 将目前找到的最近的顶点置1
    
    // 修正当前最短路径及距离
    for( w=0; w < G.numVextexes; w++ )
    {
      // 如果经过v顶点的路径比现在这条路径的长度短的话,更新!
      if( !final[w] && (min+G.arc[k][w] < (*D)[w]) )
      {
        (*D)[w] = min + G.arc[k][w]; // 修改当前路径长度
        (*p)[w] = k; // 存放前驱顶点
      }
    }
  }
}

时间复杂度分析

初始化辅助向量 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调试):

#include<bits/stdc++.h>
using namespace std;
# define INF 0x3f3f3f3f

typedef pair<int, int> iPair;

class Graph
{
    int V; // 顶点个数

    //带权图中需要保存顶点的权值。
    list< pair<int, int> > *adj;

public:
    Graph(int V); // 构造器

    // 添加边和权值
    void addEdge(int u, int v, int w);

    // 打印源点s到其他顶点的最短路径
    void shortestPath(int s);
};

// 为邻接表分配空间
Graph::Graph(int V)
{
    this->V = V;
    adj = new list<iPair> [V];
}

void Graph::addEdge(int u, int v, int w)
{
    adj[u].push_back(make_pair(v, w));
    adj[v].push_back(make_pair(u, w));
}

// 打印从源顶点到其他顶点的最短路径
void Graph::shortestPath(int src)
{
    // 创建一个优先队列实现小顶堆
    priority_queue< iPair, vector <iPair> , greater<iPair> > pq;

    // 创建一个距离向量,初始化为INF
    vector<int> dist(V, INF);

    // 将源顶点入堆,并将到自身的距离初始化为0
    pq.push(make_pair(0, src));
    dist[src] = 0;

    /*循环直到堆为空*/
    while (!pq.empty())
    {
        // 取出堆顶的顶点并保存在 u中
        int u = pq.top().second;
        pq.pop();

        // 设置迭代器,遍历顶点 u 的所有邻接顶点。
        list< pair<int, int> >::iterator i;
        for (i = adj[u].begin(); i != adj[u].end(); ++i)
        {
            // 获取邻接顶点的标签和权值,即weight(u,v)
            int v = (*i).first;
            int weight = (*i).second;

            // 如果从u到v存在一条最短路径且当前顶点u的最短距离+weight(u,v)dist[v]的值,则更新
            if (dist[v] > dist[u] + weight)
            {
                dist[v] = dist[u] + weight;
                pq.push(make_pair(dist[v], v));
            }
        }
    }

    // 打印最短路径
    printf("Vertex Distance from Source\n");
    for (int i = 0; i < V; ++i)
        printf("%d \t\t %d\n", i, dist[i]);
}


int main()
{

    int V = 9;
    Graph g(V);

    g.addEdge(0, 1, 1);
    g.addEdge(0, 2, 5);
    g.addEdge(1, 2, 3);
    g.addEdge(1, 4, 5);
    g.addEdge(1, 3, 7);
    g.addEdge(2, 4, 1);
    g.addEdge(2, 5, 7);
    g.addEdge(3, 4, 2);
    g.addEdge(3, 6, 3);
    g.addEdge(4, 5, 3);
    g.addEdge(4, 6, 6);
    g.addEdge(4, 7, 9);
    g.addEdge(5, 7, 5);
    g.addEdge(6, 7, 2);
    g.addEdge(6, 8, 7);
    g.addEdge(7, 8, 4);

    g.shortestPath(0);

    return 0;
}

总结

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


推荐阅读

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


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

  • 20
    点赞
  • 103
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值