算法学习(6)-最短路径

目录

Floyd-Warshall算法

Dijkstra算法

Bellman-Ford算法

Bellman-Ford的队列优化

最短路径算法对比分析


Floyd-Warshall算法

         现在回到问题:如何求任意两点之间的最短路径呢?

        通过之前的学习, 我们知道通过深度或广度优先搜索可以求出两点之间的最短路径。所以进行n^2遍深度或广度优先搜索, 即对每两个点都进行一次深度或广度优先搜索, 便可以求得任慈两点之间的最短路径。可是还有没有别的方法呢?

        当任意两点之间不允许经过第三个点时, 这些城市之间的最短路程就是初始路程,如下。

for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (e[i][j] > e[i][1] + e[1][j]) {e[i][j] = e[i][1] + e[1][j];}}
}

        接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过 1 号顶点时任意两点的最短路程的结果下, 再判断如果经过 2 号顶点是否可以使得 i 号顶点到 j 号顶点之间的路程变得更短,即判断 e[i][2] + e[2][j] 是否比 e[i][j] 要小,代码实现如下。

//经过1号顶点
for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (e[i][j] > e[i][1] + e[1][j]) {e[i][j] = e[i][1] + e[1][j];}}
}//经过2号顶点
for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (e[i][j] > e[i][2] + e[2][j]) {e[i][j] = e[i][2] + e[2][j];}}
}

        在只允许经过1 和2号顶点的情况下, 任意两点之间的最短路程更新为:

        通过上图得知,在相比只允许通过1号顶点进行中转的情况下,这里允许通过1和2号顶点进行中转, 使得 e[1][3] 和 e[4][3] 的路程变得更短了。
        同理, 继续在只允许经过1 、2和3号顶点进行中转的情况下,求任意两点之间的最短路程。任意两点之间的最短路程更新为: 

        最后允许通过所有顶点作为中转, 任意两点之间最终的最短路程为: 

        整个算法过程虽然说起来很麻烦,但是代码实现却非常简单,核心代码只有五行:

for (k = 1; k <= n; k++) {for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (e[i][j] > e[i][k] + e[k][j]) {e[i][j] = e[i][k] + e[k][j];}}}
}

        这段代码的基本思想就是: 最开始只允许经过1号顶点进行中转, 接下来只允许经过1号和2号顶点进行中转...…允许经过l~n号所有顶点进行中转, 求任意两点之间的最短路程。用一句话概括就是: 从 i 号顶点到 j 号顶点只经过前 k 号点的最短路程。其实这是一种“ 动态规划” 的思想。下面给出这个算法的完整代码:

#include <stdio.h>int main() {int e[10][10], k, i, j, n, m, tl, t2, t3;int inf = 99999999; // 用inf (infinity的缩写)存储一个我们认为的正无穷值// 读入n和m, n表示顶点个数, 呻畟示边的条数scanf("%d %d", &n, &m);// 初始化for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (i == j)e[i][j] = 0;elsee[i][j] = inf;}}// 读入边for (i = 1; i <= m; i++) {// 第6章最短路径scanf("%d %d %d", &t1, &t2, &t3);e[t1][t2] = t3;}// Floyd-Warshall算法核心语句for (k = 1; k <= n; k++) {for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (e[i][j] > e[i][k] + e[k][j])e[i][j] = e[i][k] + e[k][j];}}}// 输出朵终的结果for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {printf("%10d", e[i][j]);}printf("\n");}return 0;
}

        此算法由RobertW. Floyd(罗伯特·弗洛伊德)千1962年发表在Communications of the ACM上。同年Stephen Warshall(史蒂芬·沃舍尔)也独立发表了这个算法。Robert W. Floyd原本在芝加哥大学读的文学, 但是因为当时美国经济不太景气, 找工作比较困难, 无奈之下到西屋电气公司当了一名计算机操作员, 在IBM650机房值夜班, 并由此开始了他的计算机生涯。此外他还和J.W.J. Williams(威廉姆斯)于1964年共同发明了著名的堆排序算法HEAPSORT。Robert W. Floyd在1978年获得了图灵奖。


Dijkstra算法

         完整的 Dijkstra 算法代码如下:

#include <stdio.h>int main() {int e[10][10], dis[10], book[10], i, j, n, m, t1, t2, t3, u, v, min;int inf = 99999999; // 用inf (infinity的缩写)存储一个我们认为的正无穷值// 读入n和m, n表示顶点个数, 咕贬瓦边的条数scanf("%d %d", &n, &m);// 初始化for (i = 1; i <= n; i++) {for (j = 1; j <= n; j++) {if (i == j)e[i][j] = 0;elsee[i][j] = inf;}}// 读入边for (i = 1; i <= m; i++) {scanf("%d %d %d", &t1, &t2, &t3);e[t1][t2] = t3;}// 初始化dis数组, 这里是1号顶点到其余各个顶点的初始路程for (i = 1; i <= n; i++) {dis[i] = e[1][i];}// book数组初始化for (i = 1; i <= n; i++) {book[i] = 0;}book[1] = 1;// Dijkstra算法核心语句for (i = 1; i <= n - 1; i++) {//找到离1号顶点最近的顶点min = inf;for (j = 1; j <= n; j++) {if (book[j] == 0 && dis[j] < min) {min = dis[j];u = j;}}book[u] = 1;for (v = 1; v <= n; v++) {if (e[u][v] < inf) {if (dis[v] > dis[u] + e[u][v]) {dis[v] = dis[u] + e[u][v];}}}}// 输出最终的结果for (i = 1; i <= n; i++) {printf("%d ", dis[i]);}getchar();getchar();return 0;
}

        通过上面的代码我们可以看出, 这个算法的时间复杂度是O(N2)。其中每次找到离1号顶点最近的顶点的时间复杂度是O (N), 这里我们可以用“堆”(将在下一章学到)来优化,使得这一部分的时间复杂度降低到O(logN)。另外对于边数M少于N^2的稀疏图来说(我们把M远小于N^2的图称为稀疏图, 而M相对较大的图称为稠密图), 我们可以用邻接表来代替邻接矩阵, 使得整个时间复杂度优化到O(M+N)logN。请注意!在最坏的情况下M就是N^2, 这样的话(M+N)logN要比N^2还要大。但是大多数情况下并不会有那么多边, 因此(M+N)logN要比N^2小很多。

        现在用邻接标来存储这个图,先给出代码如下:

int n, m, i;
// u、v和w的数组大小要根据实际情况来设置,要比m的最大值要大1
int u[6], v[6], w[6];
//first和next的数组大小要根据实际情况来设置,要比n的最大值要大1
int first[5], next[5];
scanf("%d %d", &n, &m);
//初始化first数组下标1~n的值为-1,表示1~n顶点暂时没有边
for (i = 1; i <= n; i++) {first[i] = 1;
}
for (i = 1; i <= m; i++) {scanf("%d %d %d", &u[i], &v[i], &w[i]); // 读入每一条边//下面两句是关键next[i] = first[u[i]];first[u[i]] = i;
}

k = first[1];
while(k != -1) {printf("%d %d %d\n", u[k], v[k], w[k]);k = next[k];
}

        不难发现, 此时遍历某个顶点的边的时候的遍历顺序正好与读入时候的顺序相反。因为在为每个顶点插入边的时候都是直接插入“ 链表” 的首部而不是尾部。不过这并不会产生任何问题, 这正是这种方法的奇妙之处。遍历每个顶点的边, 其代码如下。

for (i = 1; i <= n; i++) {k = first[i];while(k != -1) {printf("%d %d %d\n", u[k], v[k], w[k]);k = next[k];}
}

        可以发现使用邻接表来存储图的时间空间复杂度是O(M), 遍历每一条边的时间复杂度也是O(M)。如果一个图是稀疏图的话, M要远小于N^2。因此稀疏图选用邻接表来存储要比用邻接矩阵来存储好很多。
        最后, 本节介绍的求最短路径的算法是一种基千贪心策略的算法。每次新扩展一个路程最短的点, 更新与其相邻的点的路程。当所有边权都为正时, 由千不会存在一个路程更短的没扩展过的点, 所以这个点的路程永远不会再被改变, 因而保证了算法的正确性。不过根据这个原理, 用本算法求最短路径的图是不能有负权边的, 因为扩展到负权边的时候会产生更短的路程, 有可能就破坏了已经更新的点路程不会改变的性质。既然用这个算法求最短路径的图不能有负权边, 那有没有可以求带有负权边的指定顶点到其余各个顶点的最短路径算法呢?请看下一节。
        最后说一下, 这个算法的名字叫做Dijkstra。该算法是由荷兰计算机科学家Edsger Wybe Dijkstra于1959年提出的。其实这个算法Edsger Wybe Dijkstra在1956年就发现了, 当时他正与夫
人在一家咖啡厅的阳台上晒太阳喝咖啡。因为当时没有专注千离散算法的专业期刊, 直到1959年, 他才把这个算法发表在Numerische Mathematik的创刊号上。


Bellman-Ford算法

        Dijkstra算法虽然好, 但是它不能解决带有负权边(边的权值为负数)的图。本节我要来介绍一个无论是思想上还是代码实现上都堪称完美的最短路算法: Bellman-Ford。Bellman-Ford算法非常简单, 核心代码只有4行, 并且可以完美地解决带有负权边的图, 先来看看它长啥样:

for (k = 1; k <= n-1 k++) {for (i = 1; i <= m; i++) {if (dis[v[i] > dis[u[i] + w[i]) {dis[v[i]] = dis [u[i]] + w[i];}}
}

if (dis[v[i]] > dis[u[i]] + w[i]) {dis[v[i]] = dis[u[i]] + w[i];
}

for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {dis[v[i]] = dis[u[i]] + w[i];
}

for (k = 1; k <= n-1; k++) { //进行n-1轮松弛for (i = 1; i <= m; i++) { //枚举每一条边if (dis[v[i]] > dis[u[i]] + w[i]) { //尝试对每一条边进行松弛dis[v[i]] = dis[u[i]] + w[i];}}
}

#include <stdio.h>
int main() {int dis[10], i, k, n, m, u[10], v[10], w[10];int inf = 99999999;//用inf存储我们认为的无穷大//读入n和m, n表示顶点个数,m表示边的条数scanf("%d %d", &n, &m);//读入边for (i = 1; i <= m; i++) {scanf("%d %d %d", &u[i], &v[i], &w[i]);}//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程for (i = 1; i <= n; i++) {dis[i] = inf;}dis[1] = 0;//Bellman-Ford算法核心语句for (k = 1; k <= n-1; k++) {for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {dis[v[i]] = dis[u[i]] + w[i];}}}//输出最终结果for (i = 1; i <= n; i++) {printf("%d", dis[i]);}getchar();getchar();return 0;
} 

//Bellman-Ford算法核心语句
for (k = 1; k <= n-1; k++) {for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {dis[v[i]] = dis[u[i]] + w[i];}}
}
//检测负权回路
flag = 0;
for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {flag = 1;}
}
if (flag == 1) {printf("此图含有负权回路");
}

        显然,Bellman-Ford 算法的时间复杂度是 O(NM) ,这个时间复杂度貌似比Dijkstra算法还高,我们还可以对其进行优化。在实际操作中,Bellman-Ford 算法经常会在未达到n-1轮松弛前就已经计算出最短路, 之前我们已经说过,n-1 其实是最大值。因此可以添加一个一维数组用来备份数组dis。如果在新一轮的松弛中数组dis 没有发生变化, 则可以提前跳出循环, 代码如下:

#inclide <stdio.h>
int main() {int dis[10], bak[10], i, k, n, m, u[10], v[10], w[10], check, flag;int inf = 99999999;//用inf存储正无穷//读入n和m,n表示顶点个数,m表示边的条数scanf("%d %d", &n, &m);//读入边for (ii = 1; i <= m; i++) {scanf("%d %d %d", &u[i],&v[i], &w[i]);}//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程for (i = 1; i <= n; i++) {dis[i] = inf;}dis[1] = 0;//Bellman-Ford算法核心语句for (k = 1; k <= n-1; k++) {//将dis数组备份至bak数组中for (i = 1; i <= n; i++) {bak[i] = dis[i];}//进行一轮松弛for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {dis[v[i]] = dis[u[i]] + w[i];}}//松弛完毕后检测dis数组是否有更新check = 0;for (i = 1; i <= n; i++) {if (bak[i] != dis[i]) {check = 1;break;}}if (check == 0) {break;}}//检测负权回路flag = 0;for (i = 1; i <= m; i++) {if (dis[v[i]] > dis[u[i]] + w[i]) {flag = 1;}}if (flag == 1) {printf("此图含有负权回路");}else {//输出最终结果for (i = 1; i <= n; i++) {printf("%d ", dis[i]);}}getchar();getchar();return 0;
}

        Bellman-Ford 算法的另外一种优化在文中已经有所提示:在每实施一次松弛操作后, 就会有一些顶点已经求得其最短路, 此后这些顶点的最短路的估计值就会一直保待不变, 不再受后续松弛操作的影响, 但是每次还要判断是否需要松弛, 这里浪费了时间。这就启发我们:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。详悄请看下一节:Bellman- Ford 的队列优化。
        美国应用数学家Richard Bellman (理查德·贝尔曼)千1958 年发表了该算法。此外Lester Ford, Jr在1956 年也发表了该算法。因此这个算法叫做Bellman-Ford 算法。其实EdwardF. Moore 在1957 年也发表了同样的算法, 所以这个算法也称为Bellman- Ford -Moore 算法.Edward F. Moore 很熟悉对不对?就是那个在“ 如何从迷宫中寻找出路” 问题中提出了广度优先搜索算法的那个家伙。


Bellman-Ford的队列优化

        下面是代码试下,还是用邻接表来存储图,代码如下:

#inlude <stdio.h>
int mian() {int n, m, i, j, k;//u、v和w的数组大小要根据实际情况来设置,要比m的最大值要大1int u[8], v[8], w[8];//first要比n的最大值大1,next要比m的最大值大1int first[6], next[8];int dis[6] = {0}, book[6] = {0};//book数组记录哪些顶点在队列中intque[101] = {0}, head = 1, tail = 1;//定义一个队列,并初始化int inf=99999999;//读入n和mscanf("%d %d", &n, &m);//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程for (i = 1; i <= n; i++) {dis[i] = inf;}dis[1] = 0;//初始化book数组,初始化为0,刚开始都不在队列中for (i =1; i <= n; i++) {book[i] = 0;}//初始化first数组下标1~n的值为-1,表示1~n顶点暂时没有边for (i = 1; i <= n; i++) {first[i] = -1;}for (i =1; i <= m; i++) {//读入每一条边scanf("%d %d %d", &u[i], &v[i], &w[i]);//下面两句是建立邻接表的关键next[i] = first[u[i]];first[u[i]] = i;}//1号顶点入队que[tail++] = 1;book[1] = 1;//标记1号顶点已经入队while(head < tail) {k = first[que[head]];//当前需要处理的队列队头while(k != -1) {//扫描当前顶点所以边if (dis[v[k]] > dis[u[k]] + w[k]) {//判断是否松弛成功dis[v[k]] = dis[u[k]] + w[k];//更新路程if (book[v[k]] == 0) {//0表示不在队列,入队//下面是入队操作que[tail++] = v[k];book[v[k]] = 1;}}k = next[k];}//出队book[que[head++]] = 0;}//输出1号顶点到其余各个顶点额最短路径for (i = 1; i <= n; i++) {printf("%d", dis[i]);}getchar();getchar();return 0;
}

        下面来总结一下。初始时将源点加入队列。每次从队首(head) 取出一个顶点, 并对与其相邻的所有顶点进行松弛尝试, 若某个相邻的顶点松弛成功, 且这个相邻的顶点不在队列中(不在head 和tail 之间), 则将它加入到队列中。对当前顶点处理完毕后立即出队, 并对下一个新队首进行如上操作, 直到队列为空时算法结束。这里用了一个数组book 来记录每个顶点是否处在队列中。其实也可以不要book 数组, 检查一个顶点是否在队列中, 只需要把que[head]到que[tail]依次判断一遍就可以了, 但是这样做的时间复杂度是O(N), 而使用book 数组来记录的话时间复杂度会降至0(1)。
        使用队列优化的Bellman-Ford 算法在形式上和广度优先搜索非常类似,不同的是在广度优先搜索的时候一个顶点出队后通常就不会再重新进入队列. 而这里一个顶点很可能在出队列之后再次被放入队列, 也就是当一个顶点的最短路程估计值变小后, 需要对其所有出边进行松弛, 但是如果这个顶点的最短路程估计值再次变小, 仍需要对其所有出边再次进行松弛,这样才能保证相邻顶点的最短路程估计值同步更新。需要特别说明一下的是, 使用队列优化的Bellman-Ford 算法的时间复杂度在最坏情况下也是0(NM)。通过队列优化的Bellman-Ford算法如何判断一个图是否有负环呢?如果某个点进入队列的次数超过n次, 那么这个图则肯定存在负环。
        用队列优化的Bellman-Ford 算法的关键之处在于:只有那些在前一遍松弛中改变了最短路程估计值的顶点, 才可能引起它们邻接点最短路程估计值发生改变。因此, 用一个队列来存放被成功松弛的顶点, 之后只对队列中的点进行处理, 这就降低了算法的时间复杂度。另外说一下, 西南交通大学段凡丁在1994 年发表的关千最短路径的SPFA 快速算法(SPFA,Shortest Path Faster Algorithm) , 也是基千队列优化的Bellman-Ford 算法的。


最短路径算法对比分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/679102.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度剖析muduo网络库1.1---面试提问(阻塞、非阻塞、同步、异步)

在面试过程中&#xff0c;如果被问到关于IO的阻塞、非阻塞、同步、异步时&#xff0c;我们应该如何回答呢&#xff1f; 结合最近学习的课程&#xff0c;我作出了以下的总结&#xff0c;希望能与大家共同探讨&#xff01; 先给出 陈硕大神原话&#xff1a;在处理IO的时候&…

水面垃圾清理机器人的视觉算法研究

卷积神经网络是一种分层的数据表示模型&#xff0c;通常由数据输入层、卷积层、池化层、 非线性激活函数、全连接层以及输出结果预测层等组成&#xff0c;其中卷积层、池化层和非线 性激活函数是卷积神经网络中的重要组成部分。此外&#xff0c;有些模型会增加其他的层(归一 化…

C++贪心算法

关于string的系统函数&#xff01; &#xff08;注&#xff1a;以下函数只可用于string&#xff0c;不适用其他类型的变量&#xff09; ① a.size(); 这个系统函数是用来获取这个string变量的长度的&#xff0c;我们通常会新建一个变量来保存他&#xff0c;以便之后使用。 …

linux centos7.9 快速安装python3环境

安装好linux centos7.9 默认只有python2的环境如下: python2.7.5这个很老旧的版本了,有很多新库不支持,性能可能也不行。 接下来快速安装python3环境,并设置源做到快速安装包,设置虚拟环境,打造强大的python实验环境: 1,下载,安装最小化的miniconda,可以通过这个自…

同一局域网如何从Windows系统拷贝文件到银河麒麟系统

1. 先将Windows下的、被拷贝文件所在文件夹设置为共享目录&#xff1a;在文件夹上单击右键选择“属性”菜单&#xff0c;弹出如下对话框&#xff1a; 按数字顺序单击鼠标左键&#xff0c;弹出如下对话框&#xff1a; 并将权限开放为Everyone&#xff0c;单击“共享”按钮。 在…

嵌入式开发八:STM32启动过程分析

本次给大家分析 STM32F4 的启动过程&#xff0c;这里的启动过程是指从 STM32 芯片上电复位执行的第一条指令开始&#xff0c;到执行用户编写的 main 函数这之间的过程。我们编写程序&#xff0c;基本都是用 C 语言编写&#xff0c;并且以 main 函数作为程序的入口。但是事实上&…

腾讯云服务器之ssh远程连接登录

一、创建密钥绑定实例 创建密钥会自动下载一个私钥&#xff0c;把这个私钥复制到c盘 二、设置私钥权限 1、删除所有用户权限 2、添加当前用户权限 查看当前用户名 echo %USERNAME%三、ssh远程连接到服务器 ssh ubuntu175.xxx.xxx.112 -i C:\Crack\cs2.pem四、修改root密码 s…

2024-05-08 问AI: 在深度学习中,介绍一下RMSProp 优化器

文心一言 在深度学习中&#xff0c;RMSProp&#xff08;Root Mean Square Propagation&#xff09;优化器是一种常用的优化算法&#xff0c;主要用于神经网络训练的梯度下降算法的变体。它是对Adagrad优化器的一种改进&#xff0c;旨在解决Adagrad中学习率过快下降的问题。 R…

可视化面板布局适配屏幕-基于 flexible.js + rem 智能大屏适配

可视化面板布局适配屏幕-基于 flexible.js rem 智能大屏适配 VScode 安装cssrem插件引入flexible.js在之后的开发都使用rem为单位&#xff0c;安装cssrem插件就是为了快捷将px转为rem我们的设计稿是1920px&#xff0c;设置最小宽度为1024px&#xff0c;最后&#xff0c;我们可…

刷题训练之模拟

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;熟练掌握模拟算法。 > 毒鸡汤&#xff1a;学习&#xff0c;学习&#xff0c;再学习 ! 学&#xff0c;然后知不足。 > 专栏选自&#xff1a;刷题训…

掌控网络流量,优化网络性能 - AnaTraf网络流量分析仪登场

在当今日新月异的网络环境中,网络流量监控和性能诊断已成为企业IT部门不可或缺的重要工作。只有充分了解网络流量状况,才能有效优化网络性能,提高业务运营效率。针对这一需求,全新推出的AnaTraf网络流量分析仪应运而生,为企业提供全面的网络监控和性能诊断解决方案。 快速定位…

GD32F103RCT6/GD32F303RCT6(9)高级定时器互补PWM波输出实验

本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发 后续项目主要在下面该专栏中发布&#xff1a; 手把手教你嵌入式国产化_不及你的温柔的博客-CSDN博客 感兴趣的点个关注收藏一下吧! 电机驱动开发可以跳转&#xff1a; 手把手教你嵌入式国产化-实战项目-无刷电机驱动&am…