【数据结构与算法】Dijkstra算法、Floyd算法

最短路径

前文提到,对于不带权图来说,BFS可以解决最短路径问题,因为它是类似于树的层次遍历那样,同一个结点的后继的访问顺序相邻,这使得BFS首次访问结点时的路径必然是最短的。

但是对于带权图来说,BFS就无法解决了最短路径问题了,除非图中各边的权值相等。所以我们需要用一些其他的算法来解决带权图的最短路径问题。

最短路径问题大致上可以分为两类:一种是单源最短路径,即求图中某一点到其他任意一点的最短路径。另一种是求每对顶点之间的最短路径。

经典的求带权图的单源最短路径的算法有Dijkstra(迪杰斯特拉)算法,求每对顶点间的最短路径的算法有Floyd(弗洛伊德)算法。

Dijkstra和Floyd也能够解决不带权图中的最短路径问题,因为不带权图可以视为每一条边的权值都相同的带权图。

Dijkstra算法

Dijkstra基于贪心算法,它的核心是找出当前集合中顶点可达的,与源点最近的顶点,把它与源点之间的路径视为源点到该点的最短路径,并把它加入到集合中。

例如:

在这里插入图片描述

假设我们找从1到其他各个顶点的最短路径,那么按照Dijkstra算法:

初始顶点集 S = { 1 } S=\{1\} S={1}

顶点第一轮第二轮第三轮第四轮
21->2=5
31->3=61->3=6
41->2->4=71->2->4=7
51->2->5=171->2->5=171->2->4->5=15
61->3->6=91->3->6=9
71->2->4->7=11
8
S{1,2}{1,2,3}{1,2,3,4}
最短路径1->2=51->3=61->2->4=71->3->6=9

第一轮,集合S中的顶点可达的顶点有2、3。但1->2的距离最短,所以 1->2 的最短路径 = 5, 把2加入到S中,此时 S = { 1 , 2 } S=\{1,2\} S={1,2}

第二轮,集合S中的顶点可达的顶点有3、4、5。但1->3的距离最短,所以1->3的最短路径 = 6,把3加入到S中,此时 S = { 1 , 2 , 3 } S=\{1,2,3\} S={1,2,3}

第三轮,集合S中的顶点可达的顶点有4、5、6。但1-2->4的路径最短,所以1->4的最短路径 = 7,把4加入到S中,此时 S = { 1 , 2 , 3 , 4 } S=\{1,2,3,4\} S={1,2,3,4}

第四轮,集合S中的顶点可达的顶点有5、6、7。但1->3->6的路径是最短的,所以1->6的最短路径 = 9,把6加入到S中,此时 S = { 1 , 2 , 3 , 4 , 6 } S=\{1,2,3,4,6\} S={1,2,3,4,6}

后面依次类推,直到所有的顶点都被包括在S中。

实现

Dijkstra实际上是在利用贪心的原则更新维护源点与集合中的顶点可达的顶点之间的路径,并且每次选择一个未被加入到集合中且到源点距离最近的顶点,记录它和源点之间的距离为最短路径,把它加入到集合中。

也正是因为贪心的原则,所以Dijkstra无法处理带负权的的带权图。

例如:如果上图中的2—4之间的距离为-300,在第二轮,使用Dijkstra算法就已经确定了1–3的最短路径,但是实际上的最短路径 = 1->2->4->3 = 5-300+6 = -289

代码实现如下:

// C++

#include <vector>

const int INF = 0x3f3f3f3f;

/**
 * @param E 正权图的边集
 * @param source 源点
 */
std::vector<int> dijkstra(std::vector<std::vector<int>> E, int source)
{
    int n = E.size();
    std::vector<int> distance(n, INF);   // 顶点到源点的距离
    std::vector<bool> visited(n, false); // 顶点是否被加入到集合中。

    distance[source] = 0;

    for (int i = 0; i < n - 1; i++)
    {
        int cur = -1;
        // 选择一个到源点距离最近的顶点
        for (int j = 0; j < n; j++)
        {
            if (!visited[j] && (cur == -1 || distance[j] < distance[cur]))
                cur = j;
        }

        // 标记已加入到集合中
        visited[cur] = true;

        // 贪心原则,更新与它相邻的顶点的路径
        for (int j = 0; j < n; j++)
        {
            distance[j] = std::min(distance[j], distance[cur] + E[cur][j]);
        }
    }

    return distance;
}

Floyd算法

Floyd算法基于动态规划,它的核心是一个表达式: d i s t [ i ] [ j ] = m i n { d i s t [ i ] [ j ] , d i s t [ i ] [ k ] + d i s t [ k ] [ j ] } dist[i][j] = min\{dist[i][j], dist[i][k] + dist[k][j]\} dist[i][j]=min{dist[i][j],dist[i][k]+dist[k][j]} d i s t [ i ] [ j ] dist[i][j] dist[i][j]表示当前状况下 i 到 j 的最短路径。

当然,只有这个表达式可能不容易理解,我们扩展一下

// 引入中间结点k,每次循环更新一次当前状况下的最短路径
for (int k = 0; k < n; k++)
{
    // 内层循环,更新当前状况下各顶点间的最短路径
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            // 当前状况下i到j的最短路径 = min(i到j的路径, i到k的路径 + k到j的路径)。
            dist[i][j] = std::min(dist[i][j], dist[i][k] + dist[k][j]);
        }
    }
}

很容易就能发现,Floyd算法实际上是一种非常简单暴力的求解算法。它通过枚举所有的情况,来获取每对顶点之间的最短路径。所以Floyd算法可以处理带负权的图的情况。

实现

// C++

#include <vector>

/**
 * @param E 边集
 */
std::vector<std::vector<int>> floyd(std::vector<std::vector<int>> E)
{
    int n = E.size();
    std::vector dist(E);

    // 引入中间结点k,每次循环更新一次当前状况下的最短路径
    for (int k = 0; k < n; k++)
    {
        // 内层循环,更新当前状况下各顶点间的最短路径
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                // 当前状况下i到j的最短路径 = min(i到j的路径, i到k的路径 + k到j的路径)。
                dist[i][j] = std::min(dist[i][j], dist[i][k] + dist[k][j]);
            }
        }
    }
    return dist;
}

上述的算法只保存了最短路径长度,有时候为了保存路径,我们也会引入一个path数组,用来记录路径的中转结点。

#include <vector>

/**
 * @param E 边集
 */
std::vector<std::vector<int>> floyd(std::vector<std::vector<int>> E)
{
    int n = E.size();
    std::vector dist(E);
    // 引入一个path数组,用来记录路径的中转结点。
    std::vector<std::vector<int>> path(n, std::vector<int>(n, -1));

    // 引入中间结点k,每次循环更新一次当前状况下的最短路径
    for (int k = 0; k < n; k++)
    {
        // 内层循环,更新当前状况下各顶点间的最短路径
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                // 当前状况下i到j的最短路径长度 = min(i到j的路径长度, i到k的路径长度 + k到j的路径长度)。
                if (dist[i][j] > dist[i][k] + dist[k][j])
                {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    // 记录i到j的最短路径中的中转结点。
                    path[i][j] = k;
                }
            }
        }
    }
    return dist;
}

通过 p a t h [ i ] [ j ] = k path[i][j] = k path[i][j]=k,我们可以知道最短路径中 必然包含 i − > k i -> k i>k k − > j k->j k>j,那么我们只需要使用类似的操作,沿着 p a t h [ i ] [ k ] path[i][k] path[i][k] p a t h [ k ] [ j ] path[k][j] path[k][j]一路递推下去直到 p a t h [ i ] [ j ] = − 1 path[i][j]=-1 path[i][j]=1,就可以得到一条最短路径 i − > ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ − > k − > ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ − > j i->······->k->······->j i>⋅⋅⋅⋅⋅⋅>k>⋅⋅⋅⋅⋅⋅>j了。

全篇结束,感谢阅读。

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值