【图】(五)单源最短路径 - Dijkstra与Bellman-Ford详解 - C语言

图相关文章:

1. 图的建立 - 邻接矩阵与邻接表icon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/127552328?spm=1001.2014.3001.55012. 图的遍历 - DFS与BFSicon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/127558368?spm=1001.2014.3001.55013. 顶点度的计算icon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/127558599?spm=1001.2014.3001.55014. 最小生成树 - Prim与Kruskalicon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/127589852?spm=1001.2014.3001.55015. 单源最短路径 - Dijkstra与Bellman-Fordicon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/127630356?spm=1001.2014.3001.55016. 多源最短路径 - Floydicon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/128039852?spm=1001.2014.3001.55017. 拓扑排序AOV网icon-default.png?t=M85Bhttps://blog.csdn.net/m15253053181/article/details/128042358?spm=1001.2014.3001.5501


目录

最短路径

一、 问题背景

二、单源最短路径问题

1 Dijkstra算法

1.1 算法思想

1.2 实现

1.3 算法分析

2 Bellman-Ford算法

2.1 算法思想

2.2 实现

2.3 算法分析


最短路径

一、 问题背景

最短路径问题是图论研究中的一个经典算法问题, 旨在寻找图中两结点之间的最短距离最短路径

最短路径问题大致可以分为两类:

① 单源最短路径问题,例如:求顶点0到顶点4的最短距离与最短路径;代表有Dijkstra算法、Bellman-Ford算法。

② 多源最短路径问题,例如:求任意两顶点间的最短距离与最短路径;代表有Floyd算法。


二、单源最短路径问题

1 Dijkstra算法

Dijkstra算法是采用贪心策略,使得源点到某点的距离dist[i]不断地逼近真实最短路径。

适用范围:

Dijkstra算法对于有向图无向图都适用,但是不可以有负权边。因为算法基于贪心策略,如果有负权边,则会反复遍历以减少dist,造成死循环。

1.1 算法思想

构造3个辅助数组:

辅助数组作用
distdist[u]表示源点s到顶点u的距离上界
visitedvisited[u] = 1 表示该点的最短路径已确定,0为不确定
predpred[u] = 顶点u的最短路径中的前驱顶点

算法的核心思想如下:

首先初始化所有顶点dist = ∞,visited = 0,pred = -1;源点的dist[s] = 0。

① 在未确定最短路径的点中选出dist最小(源点到该顶点距离最小)的顶点u。

② 对于顶点u的所有邻接点vi,如果dist[u] + w(u, vi) < dist[vi],那么就把dist[vi]更新为dist[u] + w(u, vi),将vi的前驱pred[vi] = u。

③ 将visited[u] = 1。

④ 重复①~④,直至所有顶点的visited = 1。

让我们来简单思考一下算法的正确性

简单画了一张示意图,便于下述解释的时候直观理解。

(1)对于dist数组的解释

首先,dist[u]在算法结束前,并不能确定是最短路径,而第②步的作用是确定“局部最优”。“局部最优”是什么意思呢?首先dist[vi]表示的是目前源点s到顶点vi的一种可能路径的长度,我们将这条路径记为L1。同样,dist[u]也是源点s到顶点u的一种可能路径的长度,路径记为L2。如果说从源点沿着L2这条路到顶点u,再到其邻接点vi的距离是比沿着路径L1直接到顶点vi的距离更近的话,那么就把vi从L1这条路拽到L2这条路中。

为什么说dist数组中的值在算法结束前,并不一定是最短路径呢?因为如果所有可能的路没有走完的话,上述“把一个顶点从原先的路径拽到新的路径”这一现象对于同一个顶点可能会重复出现。而一旦所有可能的路都走完了(所有顶点都已经做为路径的末尾结点尝试把它邻接顶点拽过来),dist便是最小的了。

(2)对于visited数组的解释

刚才我们提到“走完所有可能的路”,其实依靠的就是visited数组。在判断dist[u] + w(u, vi) < dist[vi]这一步中,实际上就是在以u为路径的末尾结点尝试把它邻接顶点拽过来的过程,而对于u的所有邻接顶点都判断完之后,以u为起点的所有可能就都尝试完了,当所有顶点的visited都等于1时,就是“走完了所有可能的路”。

其实,上述的算法中还有一个问题没有解释,那就是每次都是局部最短,怎么合到一起就是全局最短了呢?

其实这个问题也是比较显然的,我们给出一个定理并证明,读者就明白了。

定理:最短路径中的任意一条子路径都是最短的。

证明:

假设路径<s, ...x, u, v>为s到v的最短路径,不失一般性,有以下证明:

由定理知,<x, u>为x到u的最短路径。假设存在x与u之间存在一条更短路径<x, y, u>,使得距离小于<x, u>的距离,则s到v的最短路径为<s, ...x, y, u, v>,与题设矛盾,故不存在这样的路径。

1.2 实现

使用邻接表的数据结构便于寻找邻接点,因此使用邻接表实现。邻接矩阵的实现与其类似。

测试用例:

 由顶点1到5:

源码:

为了模块化设计,将Dijkstra的结果打了个包,结构如下:

/* 单源最短路径Dijkstra */
 typedef struct DijkstraPackage
 {
     AdjList L;    // 图
     int source;   // 起点
     int *dist;    // 距离上界
     int *visited; // 已访问
     int *pred;    // 顶点前驱
 } * DijkstraResult;

Dijkstra算法实现:

/* Dijkstra算法 */
 DijkstraResult Dijkstra(AdjList L, int source)
 {
     int i, j;
     int minDist, rec; // dist最小的点
     LGNode pMove = NULL;
     DijkstraResult result = (DijkstraResult)malloc(sizeof(struct DijkstraPackage));
     result->L = L;
     result->source = source;
     result->dist = (int *)malloc(sizeof(int) * L->numV);
     result->visited = (int *)malloc(sizeof(int) * L->numV);
     result->pred = (int *)malloc(sizeof(int) * L->numV);
 ​
     // 初始化
     for (i = 0; i < L->numV; i++)
     {
         result->dist[i] = INF;
         result->visited[i] = 0;
         result->pred[i] = -1;
     }
     result->dist[source] = 0;
 ​
     // 执行单源最短路径算法
     for (i = 0; i < L->numV; i++)
     {
         // 找到dist最小的那个点
         minDist = INF;
         for (j = 1; j < L->numV; j++)
         {
             if (result->visited[j] == 0 && result->dist[j] < minDist)
             {
                 minDist = result->dist[j];
                 rec = j;
             }
         }
         // 对于每个邻接点,尝试更新dist
         pMove = L->list[rec].next;
         while (pMove)
         {
             if (result->dist[rec] + pMove->dis < result->dist[pMove->v])
             {
                 result->dist[pMove->v] = result->dist[rec] + pMove->dis;
                 result->pred[pMove->v] = rec;
             }
             pMove = pMove->next;
         }
         // 标记为已访问
         result->visited[rec] = 1;
     }
     return result;
 }

输出函数:

void fineShortestPath(DijkstraResult result, int goal)
 {
     int i, j;
     int count = 0;
     int predV = goal;
     int path[MaxVertexNum];
     printf("Source: %c(%d)\tGoal: %c(%d)\n", result->L->nameV[result->source], result->source, result->L->nameV[goal], goal);
     printf("Shortest distance: %d\n", result->dist[goal]);
     printf("Shortest path:\n");
     while (1)
     {
         path[count] = predV;
         if (result->pred[predV] == -1)
             break;
         predV = result->pred[predV];
         count++;
     }
     for (i = 0; i <= count; i++)
     {
         printf("%c(%d)", result->L->nameV[count - i], path[count - i]);
         if (i != count)
             printf(" -> ");
     }
     printf("\n");
 }

1.3 算法分析

伪代码如下:

所以整体的时间复杂度为O(V²+E)

优化:

使用最小堆来进行dist最小的顶点的查询,可以将O(V)降到O(logV),因此优化后的总体时间复杂度为O(ElogV)


2 Bellman-Ford算法

刚才已经提到,图中如果存在负权边,则Dijkstra算法不再适用,那么有什么解决方案呢?

首先,我们需要定义一下存在负权边的图中最短路径是什么。

① 当图中存在负环时

如果说图长这样:

 从源点s到顶点a的最短距离是多少?

显然,这是无法计算的,路径:s->b->a->s->...->b->a...,只要沿着这个环走下去,我可以让s到a的距离为负无穷。

此时,我们说s到a存在负环。

② 当图中不存在负环时

 可以看到,只要我们规定不可以走例如s->j->i->j->i->...j->k这样反复刷负权边的路径,最短路径的含义就与无向图中相同了。

那么如何解决这类问题,我们下面介绍Bellman-Ford算法

算法适用范围:

该算法适用于有向图(无论带不带负权边),不适用于带负权边的无向图,但对于不含负权边的无向图还是可以使用的,下面我们会介绍为什么。

2.1 算法思想

首先我们给出一种操作的定义:松弛

其实在Dijkstra算法中我们已经使用到了这种操作,不过为了减少概念,我没有给出这个定义。而此处引入概念后可以使得表述变得简洁,因此我们来描述一下什么叫做松弛。

 给出以下两个示例,圈中的值表示dist[v]。

 我们先给出结论:

(1)每轮对所有边进行松弛,迭代|V|-1轮。

(2)进行第|V|次松弛,如果松弛成功,则存在源点s可达的负环;否则已经找到单源最短路径。

让我们以一个动图先来直观看一下算法的整个过程:

现在回答Bellman-Ford算法适用范围的问题:

对于有向图,只需要不存在负环即可;而对于无向图,不妨假设有一条负权边w(u, v) < 0,试想,在每轮松弛的时候,dist[u]与dist[v]是不是都会松弛成功?其本质原因就是因为u与v之间可以来回走,因此,无向图中的一条负权边其实就等价于有向图中的一个负环,故不适用与带负权边的无向图。

现在来解释下Bellman-Ford算法的正确性

为什么需要松弛|V|-1次?其实这是基于最坏情况的分析。

考虑最坏情况,即为一条直线的时候。想要对V_i松弛成功,就必须对V_(i-1)松弛成功,那么到最后一个顶点时,就需要松弛|V|-1次。如gif所示:

 实际上情况一般不会这么极端,即如果没有负环存在,在小于|V|-1次的松弛成功后,就已经找到了单源最短路径。在之后的松弛操作中,都不会松弛成功。

那么为什么还要松弛第|V|次呢?因为如果存在负环,实际上是可以松弛成功无数次的,这多出来的最后一次松弛操作,就是为了判断是否存在负环的。如果第|V|次松弛成功,就说明存在负环。

2.2 实现

请读者根据2.3算法分析中的伪代码自行实现(很详细),如果确实有困难,可以在评论区留言,我再实现。

2.3 算法分析

 可以看到,Bellman-Ford算法的时间复杂度为O(VE),且没有优化空间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

友人帐_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值