2021.10.24 力扣-网络延迟时间-关于最短路径的几种算法

本文详细介绍了四种图论算法在解决最短路径问题上的应用,包括朴素Dijkstra算法、堆优化的Dijkstra算法、Floyd算法和Bellman-Ford算法。同时,文章还探讨了SPFA算法,一种适用于负权边的优化算法,并提供了两种不同的实现方式。通过对各种算法的分析,帮助读者理解和掌握它们的实现细节及时间复杂度。
摘要由CSDN通过智能技术生成

目录

题目描述

方法一(朴素dijkstra算法):

方法二(堆优化的dijkstra算法):

方法三(Floyd算法):

方法四(Bellman Ford算法):

方法五(SPFA算法,数组实现邻接表):

方法五(SPFA算法,结构体实现邻接表):


题目描述

有 n 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例:

方法一(朴素dijkstra算法):

class Solution {
public:
    const int inf = INT_MAX / 2;
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        //初始化邻接矩阵,节点到自身的权值为0,到其他节点的权值为无穷大
        vector<vector<int>> w(n + 1, vector<int>(n + 1, inf));   //记录节点间的权值
        //节点到自身的距离为0
        for (int i = 1; i <= n; i++)
        {
            w[i][i] = 0;
        }
        //初始化距离为无穷大
        vector<int> minlen(n + 1, inf);    //minlen[x]表示起点到达节点x的最短距离
        //初始化各个节点的是否确定了最短路径为false
        vector<int> visit(n + 1, false);    //visit[x]表示节点是否已经确定了最短路径

        //根据times存图
        for (int i = 0; i < times.size(); i++)
        {
            int a = times[i][0];
            int b = times[i][1];
            w[a][b] = times[i][2];
        }
        //开始dijkstra算法
        //首先初始化起点k到自身的最短距离为0
        minlen[k] = 0;
        //i并无实际用途,这里只是循环n次,每次确定一个最短路径
        for (int i = 1; i <= n; i++)
        {
            int t = -1;
            //遍历minlen,找到那些还未确定最短路径的节点里面,路径最短的那个节点
            for (int j = 1; j <= n; j++)
            {
                //n次循环刚好能够取到所有节点,而且每个节点肯定能够被取到一次,
                //所以不用担心出现未能进入该判断语句而导致t为-1的情况
                if (!visit[j] && (t == -1 || minlen[t] > minlen[j]))
                {
                    t = j;
                }
            }
            visit[t] = true;    //选出的节点t可以确定它现在的路径值,就是最短路径
            //遍历minlen,通过以t作为中间点,更新k到各个节点的路径值
            for (int j = 1; j <= n; j++)
            {
                minlen[j] = min(minlen[j], minlen[t] + w[t][j]);
            }
        }

        int maxtime = INT_MIN;
        //这里不能写成for(int len : minlen),因为minlen[0]没有被用到,是没有意义的
        for (int i = 1; i <= n; i++)
        {
            maxtime = max(maxtime, minlen[i]);
        }
        //初始化时就已经将minlen[k]赋为0了,所以肯定不是判断maxtime是否为INT_MIN
        //有可能出现k无法到达的节点,这种情况下k和其的距离为无穷大,所以是和inf比较
        return maxtime == inf ? -1 : maxtime;
    }
};

djikstra算法,用于求某个起点到其它节点的最短路径,不能用于负权图,参考这篇: 

数据结构--Dijkstra算法最清楚的讲解_heroacool的专栏-CSDN博客_数据结构dijkstra算法

忘了自己大二上数据结构时有没有亲手写过dijkstra算法了,写起来异常费劲。

注意最大值不能直接使用INT_MAX,否则下面的这条语句,某个正数加上INT_MAX之后,将会变为一个负数,导致出错。

            for (int j = 1; j <= n; j++)
            {
                minlen[j] = min(minlen[j], minlen[t] + w[t][j]);
            }
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

方法二(堆优化的dijkstra算法):

class Solution {
public:
    const int inf = INT_MAX / 2;
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<int>> w(n + 1, vector<int>(n + 1, inf));
        //节点到自身的距离为0
        for (int i = 1; i <= n; i++)
        {
            w[i][i] = 0;
        }
        vector<int> minlen(n + 1, inf);
        vector<int> visit(n + 1, false);
        for (int i = 0; i < times.size(); i++)
        {
            int a = times[i][0];
            int b = times[i][1];
            w[a][b] = times[i][2];
        }
        //优先队列中的元素(d, x),表示起点到节点x的路径为d
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
        //可以不用初始化minlen[k] = 0,因为在下一步循环中将会赋值
        que.emplace(0, k);
        while (!que.empty())
        {
            int t = que.top().second;
            //某个节点可能多次更新了最短路径后,才被访问,
            //而在被访问之前优先队列中将会存入多次的到这个节点的路径值,
            //所以在后续需要将这些无用的值去掉
            if (visit[t])
            {
                que.pop();
                continue;
            }
            visit[t] = true;
            minlen[t] = que.top().first;
            //注意这里需要及时将头结点弹出,因为后续的循环中将会向优先队列中加入新的结点,
            //可能会出现某个路径值更小的元素排到头结点去,
            //等循环过后再弹出的就不一定是原来的头结点了
            que.pop();
            for (int i = 1; i <= n; i++)
            {
                if (!visit[i] && minlen[i] > minlen[t] + w[t][i])
                {
                    minlen[i] = minlen[t] + w[t][i];
                    que.emplace(minlen[i], i);
                }
            }
        }

        int maxtime = INT_MIN;
        for (int i = 1; i <= n; i++)
        {
            maxtime = max(maxtime, minlen[i]);
        }
        return maxtime == inf ? -1 : maxtime;
    }
};

与朴素的dijkstra算法不同的是,这里使用了优先队列来求更新最短路径,所以会时间复杂度会降低一些。

时间复杂度:O(mlogm),其中 mm 是数组 times 的长度。

空间复杂度:O(n+m)。

方法三(Floyd算法):

class Solution {
public:
    const int inf = INT_MAX / 2;
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<int>> w(n + 1, vector<int>(n + 1, inf));
        //节点到自身的距离为0
        for (int i = 1; i <= n; i++)
        {
            w[i][i] = 0;
        }
        //存图
        for (int i = 0; i < times.size(); i++)
        {
            int a = times[i][0];
            int b = times[i][1];
            w[a][b] = times[i][2];
        }
        //开始floyd算法
        for (int t = 1; t <= n; t++)    //依次在能通过节点t的情况下,求最短路径
        {
            //更新节点i和节点j之间的最短路径
            for (int i = 1; i <= n; i++)
            {
                for (int j = 1; j <= n; j++)
                {
                    w[i][j] = min(w[i][j], w[i][t] + w[t][j]);
                }
            }
        }

        int maxtime = INT_MIN;
        for (int i = 1; i <= n; i++)
        {
            maxtime = max(maxtime, w[k][i]);
        }
        return maxtime == inf ? -1 : maxtime;
    }
};

Floyd算法,能够求出任意两点之间的最短路径,同样不能用于负全图,参考这篇:

Floyd-傻子也能看懂的弗洛伊德算法(转) - Yuliang.wang - 博客园

这题用Floyd算法不是优先解,不过可以用来复习一下,毕竟我都忘了Floyd算法是干嘛的了hh

  • 时间复杂度:O(n³)
  • 空间复杂度:O(n²)

方法四(Bellman Ford算法):

class Solution {
public:
    const int inf = INT_MAX / 2;
    //表示由节点a到节点b的一条边,其权值为w
    struct edge 
    {
        int a;
        int b;
        int w;
    };
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        //这里是易错点,Bellman-ford算法通过存储边来存储图,所以数组edges的大小应该设置为m,而不是n
        int m = times.size();
        vector<edge> edges(m);
        vector<int> minlen(n + 1, inf);
        //存图
        for (int i = 0; i < m; i++)
        {
            edges[i].a = times[i][0];
            edges[i].b = times[i][1];
            edges[i].w = times[i][2];
        }
        minlen[k] = 0;
        //开始 Bellman-Ford算法
        //每一轮确定一个到起点的最短路径,除了节点k外共有n - 1个节点,所以循环n - 1轮
        for (int i = 0; i < n - 1; i++)     //这里i无实际作用,只用于循环
        {
            for (int index = 0; index < m; index++)
            {
                int cura = edges[index].a;
                int curb = edges[index].b;
                int curw = edges[index].w;
                //如果当前起点先到节点cura,再到节点curb的路径更短,就更新最短路径
                minlen[curb] = min(minlen[curb], minlen[cura] + curw);
            }
        }

        int maxtime = INT_MIN;
        for (int i = 1; i <= n ; i++)
        {
            maxtime = max(maxtime, minlen[i]);
        }
        return maxtime == inf ? -1 : maxtime;
    }
};

Bellman Ford算法用于解决带有负权边的单源最短路径问题,参考这篇:

Bellman-ford(解决负权边)_yuewenyao的博客-CSDN博客

这里使用了邻接表来存储图,Bell Ford算法的思想并不难,跟dijkstra算法一样,找最短路径的关键在于松弛。

Bell Ford还可以用于判断图中是否含有负权环,当循环n - 1次之后,若还能够更新最短路径,就说明图中含有负权环。

  • 时间复杂度:O(n*m)O(n∗m)
  • 空间复杂度:O(m)O(m)

方法五(SPFA算法,数组实现邻接表):

class Solution {
public:
    const int inf = INT_MAX / 2;
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        int m = times.size();           //共有m条边
        vector<int> u(m), v(m), w(m);   //u[i],v[i],w[i]分别表示第i条边的起点、终点和权值
        //firstedge数组的大小由节点的个数决定,nextedge数组的大小由边的个数决定
        vector<int> firstedge(n + 1, -1);   //firstedge[a]表示从a这个节点出发的某一条边,初始化为-1
        vector<int> nextedge(m);        //nextedge[i]表示与i这条边相连的下一条边,当已经没有下一条边时,nextedge[i] = -1
        //存图
        for (int i = 0; i < m; i++)
        {
            int a = times[i][0];
            int b = times[i][1];
            u[i] = a;
            v[i] = b;
            w[i] = times[i][2];
            nextedge[i] = firstedge[a];
            firstedge[a] = i;
        }

        queue<int> que;
        vector<int> inque(n + 1, false);  //用来判断某个节点是否在队列中
        vector<int> minlen(n + 1, inf); //起点到某个节点的最短路径
        minlen[k] = 0;
        que.push(k);
        inque[k] = true;
        //开始SPFA算法
        while (!que.empty())
        {
            //从队中取出一个节点
            int t = que.front();
            inque[t] = false;
            que.pop();
            //遍历从这个节点出发的所有边
            for (int i = firstedge[t]; i != -1; i = nextedge[i])
            {
                int a = u[i];
                int b = v[i];
                //如果先到a,再到b的路径比较短,就更新
                if (minlen[b] > minlen[a] + w[i])
                {
                    minlen[b] = minlen[a] + w[i];
                    //如果点b已经在队列里了,就继续
                    if (inque[b])
                    {
                        continue;
                    }
                    //否则将点b加入队列
                    que.push(b);
                    inque[b] = true;
                }
            }
        }

        int maxtime = INT_MIN;
        for (int i = 1; i <= n; i++)
        {
            maxtime = max(maxtime, minlen[i]);
        }
        return maxtime == inf ? -1 : maxtime;
    }
};

最后的SPFA算法看到要吐了,主要是一直理解不了用数组实现的邻接表,看了老半天,最终还是专门研究了一下,参考这篇:

巧妙的邻接表(数组实现) - 阿玛尼迪迪 - 博客园

作为bellman ford算法的优化,SPFA算法在计算含负权边的图中的单源最短路径问题时,更加高效。同样它也能用于判断图中是否存在负权环,只要某个节点在队列中的出现次数大于n次(n为节点个数)就说明含有负权环。参考这篇文章:

SPFA 算法详解( 强大图解,不会都难!)_muxi@Achilles的专栏-CSDN博客

用数组来实现邻接表确实是个巧妙的方法,相比于利用链表来实现,用数组虽然较为抽象,但是只要理解了之后就会发现其方便又简洁。

SPFA算法和前面的dijkstra算法、floyd算法、bellman-ford算法类似,都是要一个点一个点地进行松弛操作,思想是一样的。

时间复杂度:通常情况下复杂度为 O(k∗m),kk 一般为 4 到 5,最坏情况下仍为 O(n∗m),当数据为网格图时,复杂度会从 O(k∗m) 退化为 O(n∗m)。

方法五(SPFA算法,结构体实现邻接表):

class Solution {
public:
    const int inf = INT_MAX / 2;
    struct node
    {
        //存储与当前节点相邻的节点,第一个数字表示节点,第二个数字表示权值
        list<pair<int, int>> adjnode;   
    };
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<node> nodes(n + 1);
        vector<int> minlen(n + 1, inf);     //存储各个节点的最短路径
        int m = times.size();
        //存图
        for (int i = 0; i < m; i++)
        {
            int a = times[i][0];
            int b = times[i][1];
            int w = times[i][2];
            nodes[a].adjnode.push_back({ b, w });
        }
        //开始SPFA算法
        queue<int> que;                 //存储节点的队列
        vector<int> inque(n + 1, false);    //判断某个节点是否在队列中
        minlen[k] = 0;
        que.push(k);
        inque[k] = true;
        while (!que.empty())
        {
            int t = que.front();
            que.pop();
            inque[t] = false;
            list<pair<int, int>>::iterator it;
            for (it = nodes[t].adjnode.begin(); it != nodes[t].adjnode.end(); it++)
            {
                int b = it->first;
                int w = it->second;
                //将刷新了的节点入队
                if (minlen[b] > minlen[t] + w)
                {
                    minlen[b] = minlen[t] + w;
                    //已经在队列中的节点不用重复入队
                    if (inque[b])
                    {
                        continue;
                    }
                    que.push(b);
                    inque[b] = true;
                }
            }
        }

        int maxtime = INT_MIN;
        for (int i = 1; i <= n; i++)
        {
            maxtime = max(maxtime, minlen[i]);
        }
        return maxtime == inf ? -1 : maxtime;
    }
};

改用了结构体来构造邻接表,其余SPFA算法步骤类似。

第一次使用list这个容器,配合结构体一起使用的话,能够很方便地构造图的邻接表,相比于使用数组实现要更加轻松不少,而且也更形象易懂。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值