最短路问题合集


前言

最短路问题是图论中很重要的问题,还有一个很重要的是拓扑排序。
最短路问题有很多应用,研究这个问题有相当的价值。
这里以题列举方法。选取了

我的题解不以结构体等格式表示点,看了很多人的题解根本看不下去,其命名完全没有含义,由单个字母组成,不具备可读性。

在阅读这篇文章前,请确保你已经掌握对应的方法。如果没有,请到最下面的参考中学习,那是我看过的写的相对较好的几篇文章。


符号约定

  • graph : 或是邻接表或是邻接矩阵
  • dist : 最短距离数组
  • used : 已走过数组
  • mmax : 最终结束距离最大值,若为 INF 则说明没走到
  • i, j : 只是下标
  • E : edge 表示边数 在分析性能时写成M
  • V : vertex 表示点数 在分析性能时写成N

题目1:

有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

本题数据最大100,故选取 1e4 为 INF

方法1 朴素dijkstra

采用邻接矩阵存储
其中 mmin 的使用需要注意

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<int>> graph(n, vector<int>(n, 1e4));
        
        for (auto &x : times) {
            graph[x[0] - 1][x[1] - 1] = x[2];
        }
        vector<int> dist(n, 1e4);
        vector<bool> used(n);
        
        int i, j;
        dist[k - 1] = 0;
        while (true) {
            int mmin = -1;
            for (i = 0; i < n; ++i) {
                if (!used[i] && (mmin == -1 || dist[mmin] > dist[i]))
                    mmin = i;
            }
            if (mmin == -1)
                break;
            used[mmin] = true;
            for (j = 0; j < n; ++j) {
                if(!used[j])
                    dist[j] = min(dist[j], dist[mmin] + graph[mmin][j]);
            }
        }
        int mmax = *max_element(dist.begin(), dist.end());
        return mmax >= 1e4 ? -1 : mmax;  
    }

性能
时间复杂度: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( N 2 ) O(N^2) O(N2)

方法2 堆优化dijkstra

用邻接表实现,普通的邻接表就不写了,跟上面差不多。
堆优化的好处在于每次选取最小值都是 O ( 1 ) O(1) O(1),插入、删除是 O l o g n O{logn} Ologn
需要用 pair 记录出结点和距离
其中优先队列的重载符号方法比较特别,需要注意,我们并不能直接在括号里使用 lambda 重载。
我们让其以距离大小作为对比,STL默认大根堆,所以比较大小请反过来。

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<pair<int, int>>> graph(n);
        int i;
        for (i = 0; i < times.size(); ++i) {
            auto& temp = times[i];
            graph[temp[0] - 1].emplace_back(make_pair(temp[1] - 1, temp[2]));
        }

        auto cmp = [](const pair<int, int>& a, const pair<int, int>& b){
            return a.second > b.second;
        };
        priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> 
            minHeap(cmp); 
        vector<int> dist(n, INT_MAX >> 2);
        vector<bool> used(n);
        minHeap.emplace(k - 1, 0);
        dist[k - 1] = 0;
        while (!minHeap.empty()) {
            int t = minHeap.top().first;
            int time = minHeap.top().second;

            minHeap.pop();
            if (used[t])    continue;
            used[t] = true;

            for (auto & x : graph[t]) {
                if (!used[x.first] && dist[t] + x.second < dist[x.first]) {
                    dist[x.first] = dist[t] + x.second;
                    minHeap.emplace(x.first, dist[x.first]);
                }
            }
        }

        int mmax = *max_element(dist.begin(), dist.end());
        if (mmax == INT_MAX >> 2)
            return -1;
        return mmax;
    }
};

性能
这里的时间复杂度仅是一个上界,我们考虑每一次遍历都会遍历所有边,所以至少有一个M
那堆的大小是多大呢?显然上界是N ,理由是一个已经进入过堆的结点不会再次入堆 故 2logn
时间复杂度: O ( M l o g N ) O(MlogN) O(MlogN)
空间复杂度: O ( N + M ) O(N + M) O(N+M)

这里足可以看出,对于稠密图,即 M>>N 堆优化的方法可能比上面的方法慢一点


方法3 bellman-ford

此方法可以解决负权问题,上面的方法不可以,因为 dijkstra 基于贪心,基于眼下的判断一定会先选择负的,看不到未来。
明天补

  • bellman-ford 2023/05/07

对这个方法的理解在于每次让每个距离已知的点沿它的边走一步(如果这样走更短的话),最后的结果相当于从源点走了 n - 1 步,所以可以看到在下面的 i 循环中,n - 1 是最紧的,n也可以。

但如果路上有负环,则即使过了 n - 1 次,也还会更新。理由是其会反复走负边因为负边永远更短。这个特性允许我们检测负环
需要注意的是需要一个clone 数组保持上一轮的状态,否则很有可能这一轮更新的距离被这一轮的点拿来使用,即沿某个点走了两步。

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<int>> graph(n, vector<int>(n, 1e4));

        for (auto &x: times) {
            graph[x[0] - 1][x[1] - 1] = x[2];
        }
        vector<int> dist(n, 1e4);
        vector<int> clone(n);

        dist[k - 1] = 0;
        int i, j, t;
        for (i = 0; i < n - 1; ++i) {
            clone = dist;
            for (j = 0; j < n; ++j) {
                for (t = 0; t < n; ++t) {
                    dist[t] = min(dist[t], clone[j] + graph[j][t]);   
                }
            }
        }

        int mmax = *max_element(dist.begin(), dist.end());
        return mmax >= 1e4 ? -1 : mmax; 
    }
};

性能
时间复杂度: O ( N 3 ) O(N^3) O(N3)
空间复杂度: O ( N 2 ) O(N^2) O(N2)


这里的used数组意思为在不在队列里。SPFA必须使用邻接表,否则多出的会破坏其时间复杂度。

方法4 SPFA

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        vector<vector<pair<int, int>>> graph(n);
        for (auto &x: times) {
            graph[x[0] - 1].emplace_back(x[1] - 1, x[2]);
        }
        vector<int> dist(n, 1e4);
        // used to show is it in the queue
        vector<bool> used(n);
        int i;
        queue<int> order;
        order.push(k - 1);
        dist[k - 1] = 0;
        used[k - 1] = true;
        while (!order.empty()) {
            int temp = order.front();
            order.pop();
            used[temp] = false;
            for (auto &[to, time]: graph[temp]) {
                if (dist[to] > dist[temp] + time) { 
                    dist[to] = dist[temp] + time;
                    if (!used[to]) {
                        used[to] = true;
                        order.push(to);
                    }
                }
            }
        }
        int mmax = *max_element(dist.begin(), dist.end());
        return mmax >= 1e4 ? -1 : mmax; 
    }
};

性能
时间复杂度: O ( K M ) O(KM) O(KM)
空间复杂度: O ( N 2 ) O(N^2) O(N2)
其中,K是节点被平均入队的次数。一般情况下是2,但最坏情况是 O ( N M ) O(NM) O(NM)
所以为了避免最坏情况,在全是正权边时使用 dijkstra


题目1总结

本题数据

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000

M >> N,稠密图。
所以方法一的做法比方法二快些
方法一、二、四均大概90ms,方法三160ms


题目二:

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。

本题数据最大10000,故选取 1e5 为 INF
这里我们看出需要控制步数,那么 bellman-ford 和 spfa将是可行的方法

方法1 Bellman-ford

直接基于边走,不用建图

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
        vector dist(n, 1e5);
        vector clone(n);
        int i, j, f = flights.size();
        dist[src] = 0;
        for (i = 0; i <= k; ++i) {
            clone = dist;
            for (j = 0; j < f; ++j) {
                int from = flights[j][0];
                int to = flights[j][1];
                int dist_ = flights[j][2];
                if (dist[to] > clone[from] + dist_) {
                    dist[to] = clone[from] + dist_;
                }
            }
        }
        return dist[dst] >= 1e5 ? -1 : dist[dst];
    }
};

性能
时间复杂度: O ( k M + N ) O(kM + N) O(kM+N)
空间复杂度: O ( N ) O(N) O(N)
对于这个方法,使用 java 跑只需4ms,用c++需要32ms。非常奇怪,原因我觉得是
java 的 clone 或许自带优化?


方法二 SPFA

这里亦需要 clone 数组,用来限制步数,不用便会错。

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
        vector<vector<pair<int, int>>> graph(n);
        for (auto &x : flights) {
            graph[x[0]].emplace_back(x[1], x[2]);
        }

        vector<int> dist(n, 1e5);
        vector<int> clone(n);
        vector<bool> used(n);
        queue<int> order;
        order.push(src);
        used[src] = true;
        dist[src] = 0;
        
        int i, j;
        for (i = 0; i <= k; ++i) {
            int t = order.size();
            clone = dist;
            for (j = 0; j < t; ++j) {
                int temp = order.front();
                order.pop();
                used[temp] = false;

                for (auto &[to, dist_] : graph[temp]) {
                    if (dist[to] > dist_ + clone[temp]) {
                        used[to] = true;
                        dist[to] = dist_ + clone[temp];
                        order.push(to);
                    }
                }
            }
        }
        return dist[dst] >= 1e5 ? -1 : dist[dst]; 
    }
};

性能
时间复杂度: O ( k N 2 ) O(kN^2) O(kN2)
空间复杂度: O ( N ) O(N) O(N)
其中,最坏情况为队列中有所有的点,而每个点往外的边数上界是 N - 1
但实际情况一般不会这么差。


题目2总结

本题数据

  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)

稠密图。
这题 bellman-ford 不建图效率挺高
SPFA最快,大概12ms


参考:

图–最短路径(四种算法详解)

最短路问题 Bellman-Ford(单源最短路径

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值