草庐IT

最短路径(Dijkstra算法和Floyd算法)

法苏ovo 2023-05-11 原文

最短路径

​ 在图中,不可避免要解决的一个问题就是计算两点之间的最短路径,对于图结构来说,两个点之间不一定只有一条路径,那么如何才能找出最短的那一条就是图中最短路径问题。最短路径问题在实际生活中应用十分广泛。接下来主要介绍两种较为常用的最短路径算法— D i j k s t r a Dijkstra Dijkstra算法和 F l o y d Floyd Floyd算法。

文章目录


​ 首先需要对最短路径问题进行一些说明,图的类型既可以是有向图也可以是无向图,为了统一,之后统一使用有向图来进行解释。接下来也对于该问题中的一些定义进行区分:

  1. 路径长度:一条路径上所经过的边的数目
  2. 带权路径长度:路径上所经过边的权值之和
  3. 最短路径:带权路径长度值最小的那条路径

迪杰斯特拉 D i j k s t r a Dijkstra Dijkstra算法

D i j k s t r a Dijkstra Dijkstra算法是一种较为经典的最短路径求解算法,它的整体思路和前面的 P r i m Prim Prim算法非常相似,但是又有一些不同之处。接下来首先对 D i j k s t r a Dijkstra Dijkstra算法的整体流程进行一个大致的了解:

  1. 设置两点顶点的集合 U U U T T T,集合 U U U中存放已找到最短路径的顶点,集合 T T T中存放当前还未找到的最短路径的顶点。
  2. 初始状态时,集合 U U U中只包含源点,设为 v 0 v_0 v0
  3. 然后从集合 T T T中选择到源点 v 0 v_0 v0路径长度最短的顶点 u u u加入到集合 U U U中。
  4. 集合 U U U中每加入一个新的顶点 u u u都要修改源点 v 0 v_0 v0带集合 T T T中剩余顶点的当前最短路径值,集合 T T T中各顶点的新的当前最短路径长度值,为原来的当前最短路径长度值与从源点过顶点 u u u到达该顶点的带权路径长度中的较小者。
  5. 回到3,此过程不断重复,直到集合 T T T中的顶点全部加入到集合 U U U中为止。

从上述算法流程中可以看出, D i j k s t r a Dijkstra Dijkstra算法和 P r i m Prim Prim算法的相似之处在于在寻找路径时,都是逐点添加,添加之后也需要重新更新路径。

​ 该算法在计算的时候将所有的点分为两个集合,一个是目标点集 U U U,初始时只有起点, D i j k s t r a Dijkstra Dijkstra算法的功能是,给定一个起点,计算其到其他所有点的最短路径,也就是 1    t o    n 1\,\,to\,\, n 1ton的问题。之后在集合 T T T中找到起点 v 0 v_0 v0能够达到的,且距离最短的点,将其加入到 U U U中,之后根据新加入的点,重新计算一次 v 0 v_0 v0 T T T中剩余点的当前最短距离即可。

​ 接下来通过一个例子来对该算法进行更进一步的理解。






​ 上述例子中,有图中圆圈中的字母代表节点的信息,圆圈上面的内容代表源点到其最短距离以及前驱节点,前驱节点用于后续重现最短路径。这里主要是使用到了一个简单的定理,**如果起点 v 0 v_0 v0到目标点 u u u之间的某一条路径是最短路径,那么在该路径上面,任何一个点 u ′ u' u v 0 v_0 v0的路径都是 u ′ u' u v 0 v_0 v0的最短路径。**这个证明十分简单,使用反证法即可。那么这里在记录最短路径的信息时,如果需要重现路径中的顶点,那么只需要对于每一个结点设置一个前驱结点即可。

​ 接下来继续查看一个例子,这里使用表格的形式来对整个算法流程进行分析。

​ 在对该算法有了一定的理解之后,接下来就是考虑如何使用代码来实现它。由于该算法和 P r i m Prim Prim算法十分相近,所以在实现部分和 P r i m Prim Prim算法的实现部分基本一致,除了多了一个前驱数组,在初始化时,依旧是根据起点和邻接矩阵来对所有的距离进行初始化。对于源点到 T T T中点的最短距离,也是借助一个访问数组 v i s i t visit visit和距离数组 d i s dis dis来进行保存, v i s i t [ i ] = f a l s e visit[i]=false visit[i]=false代表第 i i i个点在集合 T T T中。之后在每次循环中,找出距离最短的,然后将其加入到集合 U U U中。最后就是最关键的一步,也就是根据新加入的点来更新到 T T T中剩余点的最短距离,这一步和 P r i m Prim Prim算法中的判断条件不同,需要注意。

​ 在具体实现路径重现的时候,主要是借助前驱数组和栈来进行实现,具体代码如下所示。

// Dijkstra算法
    void Dijkstra(int begin)
    {
        int minpath = INF;
        int path[MAX_NUM];
        int dis[MAX_NUM];  // 保存到其他点的距离
        for (int i = 0; i < num; i++)
        {
            if (map[begin][i])  // 设置为原本的距离
            {
                dis[i] = map[begin][i];
                path[i] = begin;
            }
            else    // 不存在就设置距离为无穷大
            {
                dis[i] = INF;
                path[i] = -1;
            }
        }

        // 初始化访问数组全部为未访问
        for (int i = 0; i < num; i++)
            visit[i] = false;
        visit[begin] = true;    // 起始结点已经被访问
        
        // 已经添加了一个begin,还需要将剩下num-1个点加入
        for (int i = 0; i < num - 1; i++)
        {
            int min_index = -1;
            int min_dis = INF;
            // 找到当前最小的一条边
            for (int j = 0; j < num; j++)
            {
                if (min_dis > dis[j] && visit[j] == false)
                {
                    min_dis = dis[j];
                    min_index = j;
                }
            }
            if (min_dis == INF) // 存在最短距离
                continue;

            visit[min_index] = true;    // 该点加入到目标点集中

            // 更新距离
            for (int j = 0; j < num; j++)
            {
                if (map[min_index][j] != 0 && dis[j] > dis[min_index] + map[min_index][j])
                {
                    dis[j] = dis[min_index] + map[min_index][j];
                    path[j] = min_index;
                }
            }

        }

        // 输出路径
        for (int i = 0; i < num; i++)
        {
            if (i == begin) // 到自己的路径不必输出
                continue;
            // 先输出长度和信息
            cout << nodes[i] << " : " << dis[i] << endl;
            // 如果不存在路径
            if (dis[i] == INF)
            {
                cout << "no path!" << endl;
                continue;
            }
            // 使用栈来输出完整路径
            stack<int> s;
            int now = path[i];
            s.push(i);
            while (now != begin && now != -1)
            {
                s.push(now);
                now = path[now];
            }
            s.push(now);

            while (!s.empty())
            {
                cout << nodes[s.top()] << "--->";
                s.pop();
            }
            cout << endl;
        }
    }

​ 最后对 D i j k s t r a Dijkstra Dijkstra算法进行一点补充,该算法只能计算边的权重值大于0的情况,对于边权值小于0的一些情况,该算法无法进行正确计算,接下来简单列举两个例子。

​ 能正确进行计算:

​ 不能正确计算:

F l o y d Floyd Floyd算法

​ 前面介绍到的 D i j k s t r a Dijkstra Dijkstra算法是一个单源算法,也就是只能计算出某个点到其他点的最短距离。而接下来要介绍的 F l o y d Floyd Floyd算法则是多源算法,即一次性计算出所有点之间相互的最短距离。

F l o y d Floyd Floyd算法的核心,也就是算法思路,来自于动态规划,故其核心为该表达式。
m a p [ i ] [ j ] = min ⁡ ( m a p [ i ] [ j ] , m a p [ i ] [ k ] + m a p [ k ] [ j ] ) map\left[ i \right] \left[ j \right] =\min \left( map\left[ i \right] \left[ j \right] , map\left[ i \right] \left[ k \right] +map\left[ k \right] \left[ j \right] \right) map[i][j]=min(map[i][j],map[i][k]+map[k][j])
该公式从直观上也很好理解, m a p [ i ] [ k ] + m a p [ k ] [ j ] map[i][k]+map[k][j] map[i][k]+map[k][j]也就是 i i i通过 k k k的中转到达 j j j。该算法的正确性验证较为复杂,目前还未找到一个较为正式的证明过程,可以先参考该博客下的内容:

https://blog.csdn.net/ljhandlwt/article/details/52096932?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

代码实现十分简单,5行代码足矣,可以当作模板直接使用,如下所示。

// Floyd算法
    void Floyd()
    {
        for (int k = 0; k < num; k++)
        {
            for (int i = 0; i < num; i++)
            {
                for (int j = 0; j < num; j++)
                {
                    if (map[i][j] > map[i][k] + map[k][j])
                    {
                        map[i][j] = map[i][k] + map[k][j];
                    }
                }
            }
        }
    }

最小生成树与最短路径的区别

​ 在对最小生成树和最短路径进行学习和理解之后,接下来需要对这两个概念进行一个综合的区分。

​ 最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。最短路径是从一点出发,到达目的地的路径最小。

遇到求所有路径之和最小的问题用最小生成树&并查集解决;遇到求两点间最短路径问题的用最短路,即从一个城市到另一个城市最短的路径问题。

​ 最小生成树构成后所有的点都被连通,而最短路只要到达目的地走的是最短的路径即可,与所有的点连不连通没有关系。

有关最短路径(Dijkstra算法和Floyd算法)的更多相关文章

  1. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  2. ruby-on-rails - Rails - 使用/自定义 URL : '/dashboard' 指定根路径 - 2

    如何使此根路径转到:“/dashboard”而不仅仅是http://example.com?root:to=>'dashboard#index',:constraints=>lambda{|req|!req.session[:user_id].blank?} 最佳答案 您可以通过以下方式实现:root:to=>redirect('/dashboard')match'/dashboard',:to=>"dashboard#index",:constraints=>lambda{|req|!req.session[:user_id].b

  3. ruby - 如何根据长度将路径数组转换为嵌套数组或散列 - 2

    我需要根据字符串路径的长度将字符串路径数组转换为符号、哈希和数组的数组给定以下数组:array=["info","services","about/company","about/history/part1","about/history/part2"]我想生成以下输出,对不同级别进行分组,根据级别的结构混合使用符号和对象。产生以下输出:[:info,:services,about:[:company,history:[:part1,:part2]]]#altsyntax[:info,:services,{:about=>[:company,{:history=>[:part1,:pa

  4. ruby-on-rails - 如何播种图像的路径? - 2

    Organization和Image具有一对一的关系。Image有一个名为filename的列,它存储文件的路径。我在Assets管道中包含这样一个文件:app/assets/other/image.jpg。播种时如何包含此文件的路径?我已经在我的种子文件中尝试过:@organization=...@organization.image.create!(filename:File.open('app/assets/other/image.jpg'))#Ialsotried:#@organization.image.create!(filename:'app/assets/other/i

  5. Ruby 和指南针路径与 yeoman 项目 - 2

    我安装了ruby​​、yeoman,当我运行我的项目时,出现了这个错误:Warning:Running"compass:dist"(compass)taskWarning:YouneedtohaveRubyandCompassinstalledthistasktowork.Moreinfo:https://github.com/gruUse--forcetocontinue.Use--forcetocontinue.我有进入可变session目标的路径,但它不起作用。谁能帮帮我? 最佳答案 我必须运行这个:geminstallcom

  6. 对象的 Ruby 方法查找路径 - 2

    是否有内置的Ruby方法或众所周知的库可以返回对象的整个方法查找链?Ruby查看一系列令人困惑的类(如thisquestion中所讨论)以查找与消息对应的实例方法,如果没有类响应消息,则调用接收方的method_missing。我将以下代码放在一起,但我确信它遗漏了某些情况或者它是否100%正确。请指出任何缺陷并指导我找到一些更好的代码(如果存在)。defmethod_lookup_chain(obj,result=[obj.singleton_class])ifobj.instance_of?Classreturnadd_modules(result)ifresult.last==B

  7. ruby-on-rails - rails 中的路径解析 - 2

    我正在寻找这样解析路由路径的方法:ActionController::Routing.new("post_path").parse#=>{:controller=>"posts",:action=>"index"}应该和url_for相反更新我发现:Whatistheoppositeofurl_forinRails?Afunctionthattakesapathandgeneratestheinterpretedroute?ActionController::Routing::Routes.recognize_path("/posts")所以现在我需要将posts_path转换为“/p

  8. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  9. python3获取路径方法 - 2

    一:os.path.dirname(__file__)和os.getcwd()importospath=os.path.dirname(__file__)print("os.path.dirname(__file__)方法的结果{}".format(path))path=os.getcwd()print("os.getcwd()方法的结果{}".format(path))该脚本路径为:/User/xxx/Work1.在当前目录/User/xxx/Work运行程序结果:2.在上一级目录/User/xxx运行程序:3.在其他目录/User/xxx/Work/python运行程序:\在其他目录/Us

  10. ruby - 用 ruby​​ 将 2 个破折号插入这个字符串的最短方法是什么? - 2

    这是字符串:04046955104021109我需要这样格式化:040469551-0402-1109用ruby​​做到这一点的最短/最有效的方法是什么? 最佳答案 两个简单的插入就可以了:example_string.insert(-9,'-').insert(-5,'-')负数表示您从字符串末尾开始计数。如果您愿意,也可以从头数起:example_string.insert(9,'-').insert(14,'-') 关于ruby-用ruby​​将2个破折号插入这个字符串的最短方法是

随机推荐