[算法笔记]Dijkstra最短路径算法

Dijkstra算法工作过程

Dijkstra算法是给定一个起点也就是原点,然后该原点通过与它临近的顶点不断向外扩张,最终得到该原点到其它所有顶点得最短距离。

算法核心流程


v或者u代表顶点和其它节点,(u,v)表示两个节点连接的线,weight(u,v)则表示v到u的权重(也可以理解为距离或者到达该顶点的成本),以上图为例子,A(0)原点到上方顶点表述为(u,v),weight(u,v)=3。

定义一个数组dist,记录原点(s)到其它所有节点的最短距离,该数组长度为所有顶点数量,下标则代表各个顶点,在初始化的时候dist(s)=0而到其它顶点的距离都为dist(u)=∞(无穷大),因为除了原点以外,到其它节点的距离都还是未知的。

定义队列q,将所有顶点都添加到队列q中,当到所有顶点的距离都计算完成后,该队列为空。

定义集合s,将所有已经访问过的顶点都添加到该集合s中,当所有顶点的距离都计算完成后,该集合包含所有顶点。

1.如果队列不为空,则从队列q中pop出一个顶点v并且该顶点v不存在于集合s中,因为原点是第一个被初始化的顶点,所以q队列中pop出的第一个顶点一定是原点。

2.将顶点v添加到集合s中,表示该顶点已经被访问过了。

4.取出与顶点v相关联的临近点u,并计算原点顶点v到u的权重(成本)dist(v) + weight(u,v) ,如果得到的权重小于当前临近点u的权重,也就是dist(v) + weight(u,v) < dist(u),则更新临近顶点u的权重。

第4步是非常核心的步骤,整个计算过程就是靠着第四步,靠着第一个已经初始化的原点不断去初始化或者更新临近点的权重(距离成本)并重复这个过程得到原点到所有顶点的最短距离。

下面是伪码实现:


function Dijkstra(Graph, source):
       dist[source]  := 0                     // 初始化原点(起点)的距离为0
       for each vertex v in Graph:            //初始化队列q流程
           if v ≠ source
               dist[v]  := infinity           // 除了原点以为的顶点都定义为无穷大
           add v to Q                         // 将顶点添加到队列Q

      while Q is not empty:                  // 开始处理队列Q中的所有顶点
          v := vertex in Q with min dist[v]  // 从队列Q中得到最小距离顶点v,因为只有原点初始化为0所以第一个是原点
          remove v from Q 

          for each neighbor u of v:           // 临近点还未从队列Q中移除
              alt := dist[v] + length(v, u)
              if alt < dist[u]:               // 如果找到了最短距离
                  dist[u]  := alt            // 更新最短距离

      return dist[]
  end function

图解

根据前面描述的流程,一开始只初始化起点也就是原点A的距离为0,而A到其它顶点的距离都是无穷大,因为都还未知,而顶点与各个节点之间的权重(成本)都以灰色数字表示。


按照伪码描述的核心流程,与A临近的3个点都被找到,而刚开始的临近的3个节点值都为无穷大,以临近的第一个点为例子,原点距离dist[v]=0,原点v到临近的第一个顶点u的权重为length(v,u)=3,按照之前描述的方式alt = dist[v] + length(v, u)得到3,因为这个时候第一个临近点的权重为无穷大,所以if alt < dist[u]成立,并更新了第一个临近点的权重为3,也就是原点到这个点的成本为3,其它两个点都是以此方式计算。


与开头时候说的一样,该算法依靠以一个原点为基础不断向外扩张,最终得到原点到所有节点的最短距离,下图描述了这个情况,与原点A临近的三个顶点都更新了权重,不再是无穷大,这个时候以权重3的节点为例子,权重3的节点找到与自己临近的两个点,这两个点同样刚开始都是无穷大,以同样的方式计算,并以第一个临近点为例子,权重3的节点权重为3,dist[v]=3,权重3节点到临近第一个节点的权重为length(v,u)=7,一样以dist[v] + length(v,u)方式计算,得到原点经过权重3节点到权重3临近的第一个节点权重(成本)为10,权重3临近的另一个节点同样以这个方式计算。


以这个流程最后得到原点到各个顶点的最短距离路线为下图所示


C++代码实现

下面我们以C++代码实现该算法,不过这里我们用以网格图(8x8)来进行实现,原理过程都是一样的。

区别与伪码,我们以一个struct结构来省掉伪码中需要的一些定义。

typedef struct _vertoceNode
{
    unsigned short      isObstacle; //表示该顶点是否是障碍物
    unsigned short      visited; //表示该顶点是否被访问过
    size_t              row_index; //表示该顶点在二维数组中的竖下标位置
    size_t              col_index; //表示该顶点在二维数组中的横下标位置
    size_t              distance; //表示原点到该顶点的权重(距离成本)
    _vertoceNode        *pervNode; //到该顶点最短距离的上一个顶点(可以以该字段反推出到一个具体顶点的最短访问路径)
    char                name[0]; //表示打印出来的字符
} VertoceNode;

定义一个二维数组表示该网格图结构,这里定义8 x 8的网格大小

#define COL_SIZE 8
#define ROW_SIZE 8

static VertoceNode* graph[ROW_SIZE][COL_SIZE];

我们还需要一个创建该顶点结构体的方法,这里我们以UINT_MAX宏来代表无穷大,方法定义如下

VertoceNode *createAndInitNode(size_t nameSize)
{
    VertoceNode *node = (VertoceNode *)malloc(sizeof(VertoceNode) + nameSize);
    if (node != NULL) {
        node->isObstacle = IS_FALSE;
        node->visited = IS_FALSE;
        node->row_index = 0;
        node->col_index = 0;
        node->distance = UINT_MAX;
        node->pervNode = NULL;
        memcpy(node->name, STRL("-"));
    }
    return node;
}

然后我们在定义一个初始化网格图结构的方法,该方法主要是初始化每个网格节点的顶点结构


void initGraph()
{
    for (size_t i = 0; i < ROW_SIZE; i++)
    {
        for (size_t j = 0; j < COL_SIZE; j++)
        {
            VertoceNode *node = createAndInitNode(3);
            if (node == NULL) {
                printf("initGraph:call function[createAndInitNode(3)] error!\n");
                exit(-1);
            }
            node->row_index = i;
            node->col_index = j;
            graph[i][j] = node;
        }
    }
}

为了最直观的体现最短路径,这里我们在网格中设置一些障碍物(用符号*表示),障碍物设置代码如下


void placeObstaclesToGraph()
{

    graph[0][2]->isObstacle = IS_TRUE;
    graph[0][3]->isObstacle = IS_TRUE;
    graph[0][4]->isObstacle = IS_TRUE;
    graph[0][5]->isObstacle = IS_TRUE;
    graph[0][6]->isObstacle = IS_TRUE;
    graph[0][7]->isObstacle = IS_TRUE;

    graph[1][2]->isObstacle = IS_TRUE;
    graph[2][2]->isObstacle = IS_TRUE;
    graph[2][3]->isObstacle = IS_TRUE;
    graph[2][4]->isObstacle = IS_TRUE;
    graph[2][5]->isObstacle = IS_TRUE;
    graph[2][6]->isObstacle = IS_TRUE;
    graph[2][7]->isObstacle = IS_TRUE;
    graph[1][7]->isObstacle = IS_TRUE;
    graph[3][4]->isObstacle = IS_TRUE;
    graph[3][5]->isObstacle = IS_TRUE;
}

当我们调用以上方法完成初始化后,打印出来的表格应该是这样('-'为路,'*'为障碍物)


这个时候来实现我们的核心寻找最短路径算法,基本类似之前描述的伪码,整个核心算法的定义如下

size_t length(VertoceNode *v, VertoceNode *u)
{
    //计算节点v到临近节点u的距离,因为生成的是网格图
    //所以每个点到每个点的距离都是1
    return v->distance + 1;
}

vector<VertoceNode *> *findShortestPath(VertoceNode *srcNode, VertoceNode *dstNode)
{
     //初始化队列Q,并将所有顶点都添加到队列Q中
    queue<VertoceNode *> q;

    for (size_t i = 0; i < ROW_SIZE; i++)
    {
        for (size_t j = 0; j < COL_SIZE; j++)
        {
            VertoceNode *node = graph[i][j];
              //如果是起点也就是原点,则距离初始化为0,上一个最短距离节点为NULL
              //因为起点到起点本身的距离就是0,所以也就不存在上一个最短距离节点
            if (i == srcNode->row_index && j == srcNode->col_index) {
                node->distance = 0;
                node->pervNode = NULL;
            }

            q.push(node);
        }
    }

    //开始处理所有顶点
    while (!q.empty())
    {
        VertoceNode *v = q.front();
        q.pop();

        //标识该顶点已经被访问过
        v->visited = IS_TRUE;

        //障碍物和未初始化的节点(因为存在障碍物,所以存在未初始化的节点)不参与寻路计算
        if (IS_TRUE == v->isObstacle || v->distance == UINT_MAX) {
            continue;
        }

        vector<VertoceNode *> *u = getVerticeNeighborList(v);
        for (auto uNode = u->begin(); uNode != u->end(); uNode++) {

            size_t alt = length(v, (*uNode));
            if (alt < (*uNode)->distance) {
                //找到最短距离
                (*uNode)->distance = alt;
                //记录最短距离的上一个节点
                (*uNode)->pervNode = v;
            }

        }

        delete u;
        u = NULL;

    }

    ....
  
}

之前的伪码有描述到,从队列q中得到一个顶点v后,要取到该v周围的顶点,并计算到达他们所需要的权重(成本),如果小于他们现有的权重则代表找到最短距离并更新它们,查找V附近的顶点的代码实现如下:

vector<VertoceNode *> *getVerticeNeighborList(VertoceNode *node)
{
    vector<VertoceNode *> *neighborList = new vector<VertoceNode *>();

    //上方的的点
    if (node->row_index > 0) {
        VertoceNode *topNode = graph[node->row_index - 1][node->col_index];
        if (IS_TRUE != topNode->isObstacle) {
            neighborList->push_back(topNode);
        }
    }

    //下方的点
    if (node->row_index < ROW_SIZE - 1) {
        VertoceNode *botNode = graph[node->row_index + 1][node->col_index];
        if (IS_TRUE != botNode->isObstacle) {
            neighborList->push_back(botNode);
        }
    }

    //左边的点
    if (node->col_index > 0) {
        VertoceNode *leftNode = graph[node->row_index][node->col_index - 1];
        if (IS_TRUE != leftNode->isObstacle) {
            neighborList->push_back(leftNode);
        }
    }

    //右边的点
    if (node->col_index < COL_SIZE - 1) {
        VertoceNode *rightNode = graph[node->row_index][node->col_index + 1];
        if (IS_TRUE != rightNode->isObstacle) {
            neighborList->push_back(rightNode);
        }
    }

    return neighborList;

}

因为我们实现的时候用的是网格图(8x8),所以一个点的周围有固定的四个顶点,也就是上下左右,我们需要考虑位置处于最顶端没有上方顶点以及处于最下端没有下方顶点等情况,当然周围的障碍物是不算在里面的,上面的实现逻辑应该能很清楚的表明这个处理过程。

下面详细讲解一下核心处理逻辑一些可能不明白的地方,整体逻辑与伪码实现是差不多的,首先从队列里面取出一个顶点v,然后从队列中删除它,并标记它为已访问,因为障碍物是不能到达以及寻路的,所以这里不参与下面的逻辑,那么后面的v->distance == UINT_MAX条件又是具体做什么的,因为我们是从原点开始初始化原点或者更新原点周围顶点的权重,并重复这个过程直到所有顶点都更新了权重也就得到了原点到所有顶点的最短距离,那么,考虑有障碍物的这种情况:


以这一行为例子,因为是网格图,一个节点的更新肯定是由它周围的节点进行的,也就是[3][1]是由它周围的比如[3][0]、[3][2]、[2][1]或者[4][1]来进行初始化或者更新,具体更新的值取决于谁更小(最短距离),所以,我们来看[3][3],因为[3][4]是障碍物,所以没有继续初始化它右边开始的节点,也就是[3][6]和[3][7]都是无穷大状态未被初始化为具体的权重值,如果这个时候[3][6]由[4][6]进行初始化,那么以上面的逻辑,在length(v, u)计算两个顶点之间的权重(成本)时,会让我们的无穷大代表值UINT_MAX + 1,因为我们的结构体的distance成员是size_t类型,这是一个无符号类型,也就是这个类型的最大值+1会造成溢出变成0(因为无符号),这样这个distance本来是表示最短距离的成员,因为溢出被赋值为0,导致后面无法正确计算最短路径。所以这里不处理节点是无穷大的,让后面的逻辑以它周围的节点去更新它们。

当整个逻辑完成后,各个顶点结构的distance字段代表了原点到各个顶点的最短距离,那么,我们不仅想知道这个最短距离值,还想知道最短距离路径怎么办?值我们已经得到了,最短路径我们将利用我们顶点结构体里面的pervNode成员来完成。

我们在更新最短距离的时候,会把这个最短距离相关联的顶点更新到成员pervNode中,也就是说,pervNode成员代表了原点到该节点的最短距离中要通过的节点之一,一直到原点,pervNode为NULL,我们利用这个不断反推出完成的路径,逻辑如下:



    //将到目标点的最短路径添加到一个容器中返回
    vector<VertoceNode *> *shortestPathList = new vector<VertoceNode *>();
    VertoceNode *iterNode = dstNode;
    while (iterNode != NULL)
    {
        shortestPathList->push_back(iterNode);
        iterNode = iterNode->pervNode;
    }

    reverse(shortestPathList->begin(), shortestPathList->end());

dstNode是我们想知道的最短路径的终点,因为前面的逻辑处理后,所有的顶点成员都已经是就绪状态(已经是原点到顶点的最短距离相关值)了,所以我们通过二维数组取出终点节点,利用它按照我们上述逻辑进行反推,并全部添加到一个list中,因为反推后是一个反方向的路径(终点 -> 起点),所以当整体路径得到后,我们利用C++标准库的反转方法reverse,把这个包含路径的list反转一下,这个时候列表就是正确的(起点->终点)的顺序了。

最终目标效果如下,起点:S([0][0]) 终点:D([7][7]) 路:- 障碍物:*

完整代码实现:https://github.com/ZhiyangLeeCN/algorithms/blob/master/Dijkstra's%20Algorithm/main.cpp

参考文档:
https://brilliant.org/wiki/dijkstras-short-path-finder/
https://www.geeksforgeeks.org/greedy-algorithms-set-6-dijkstras-shortest-path-algorithm/
https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

by zhiyanglee | email:zhiyanglee@foxmail.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 参见贪心算法——最短路径Dijkstra算法参见动态规划 目录 0.最短路径问题0.1 最短路径问题描述 0.1....
    王侦阅读 4,761评论 1 9
  • https://zh.visualgo.net/graphds 浅谈图形结构https://zh.visualgo...
    狼之独步阅读 4,113评论 0 0
  • 1736年,瑞士数学家Euler(欧拉)在他的一篇论文中讨论了格尼斯七桥问题,由此诞生了一个全新的数学分支——图论...
    不困于情阅读 4,368评论 0 9
  • 现实生活中有很大一类问题可以用简洁明了的图论语言来描述,可以转化为图论问题。 相关定义 图可以表示为G=(V, E...
    芥丶未央阅读 1,650评论 0 7
  • 最近挺郁闷的,为了钱愁的满脸痘痘! 话说不知道什么时候开始,我才突然意识到金钱的重要性!努力的拼了命去挣钱,而后不...
    苏塔苏塔阅读 239评论 0 0