【刷穿 LeetCode】787. K 站中转内最便宜的航班 : 运用 Bellman Ford 求解有限制的最短路问题

简介: 【刷穿 LeetCode】787. K 站中转内最便宜的航班 : 运用 Bellman Ford 求解有限制的最短路问题

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 787. K 站中转内最便宜的航班 ,难度为 中等


Tag : 「最短路」、「Bellman Ford」


n 个城市通过一些航班连接。给你一个数组 flights,其中 flights[i] = [from_i, to_i, price_i]flights[i]=[fromi,toi,pricei] ,表示该航班都从城市 from_ifromi 开始,以价格 price_ipricei 抵达 to_itoi


现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 srcdst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1


示例 1:


输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 1
输出: 200
解释: 
城市航班图如下
从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。
复制代码


网络异常,图片无法展示
|


示例 2:

输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 0
输出: 500
解释: 
城市航班图如下
从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。
复制代码


网络异常,图片无法展示
|


提示:


  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)
  • flights[i].length == 3
  • 0 <= fromi, toi < n
  • fromi != toi
  • 1 <= pricei <= 10^4104
  • 航班没有重复,且不存在自环
  • 0 <= src, dst, k < n
  • src != dst


基本分析



从题面看就能知道,这是一类「有限制」的最短路问题。


「限制最多经过不超过 kk 个点」等价于「限制最多不超过 k + 1k+1 条边」,而解决「有边数限制的最短路问题」是 SPFA 所不能取代 Bellman Ford 算法的经典应用之一(SPFA 能做,但不能直接做)。


Bellman Ford/SPFA 都是基于动态规划,其原始的状态定义为 f[i][k]f[i][k] 代表从起点到 ii 点,且经过最多 kk 条边的最短路径。这样的状态定义引导我们能够使用 Bellman Ford 来解决有边数限制的最短路问题。


同样多源汇最短路算法 Floyd 也是基于动态规划,其原始的三维状态定义为 f[i][j][k]f[i][j][k] 代表从点 ii 到点 jj,且经过的所有点编号不会超过 kk(即可使用点编号范围为 [1, k][1,k])的最短路径。这样的状态定义引导我们能够使用 Floyd 求最小环或者求“重心点”(即删除该点后,最短路值会变大)。


如果你对几类最短算法不熟悉,可以看 这里,里面涵盖所有的「最短路算法」和「存图方式」。


Bellman Ford + 邻接矩阵



回到本题,「限制最多经过不超过 kk 个点」等价于「限制最多不超过 k + 1k+1 条边」,因此可以使用 Bellman Ford 来求解。


点的数量只有 100100,可以直接使用「邻接矩阵」的方式进行存图。


需要注意的是,在遍历所有的“点对/边”进行松弛操作前,需要先对 distdist 进行备份,否则会出现「本次松弛操作所使用到的边,也是在同一次迭代所更新的」,从而不满足边数限制的要求。


举个 🌰,例如本次松弛操作使用了从 aabb 的当前最短距离来更新 dist[b]dist[b],直接使用 dist[a]dist[a] 的话,不能确保 dist[a]dist[a] 不是在同一次迭代中所更新,如果 dist[a]dist[a] 是同一次迭代所更新的话,那么使用的边数将会大于 kk 条。 因此在每次迭代开始前,我们都应该对 distdist 进行备份,在迭代时使用备份来进行松弛操作。


网络异常,图片无法展示
|


代码:


class Solution {
    int N = 110, INF = 0x3f3f3f3f;
    int[][] g = new int[N][N];
    int[] dist = new int[N];
    int n, m, s, t, k;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n = _n; s = _src; t = _dst; k = _k + 1;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                g[i][j] = i == j ? 0 : INF;
            }
        }
        for (int[] f : flights) {
            g[f[0]][f[1]] = f[2];
        }
        int ans = bf();
        return ans > INF / 2 ? -1 : ans;
    }
    int bf() {
        Arrays.fill(dist, INF);
        dist[s] = 0;
        for (int limit = 0; limit < k; limit++) {
            int[] clone = dist.clone();
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dist[j] = Math.min(dist[j], clone[i] + g[i][j]);
                }
            }
        }
        return dist[t];
    }
}
复制代码


  • 时间复杂度:O(k * n^2)O(kn2)
  • 空间复杂度:O(n^2)O(n2)


Bellman Ford + 类



我们知道 Bellman Ford 需要遍历所有的边,而使用「邻接矩阵」的存图方式让我们不得不遍历所有的点对,复杂度为 O(n^2)O(n2)


而边的数量 mm 的数据范围为 0 <= flights.length <= (n * (n - 1) / 2)0<=flights.length<=(n(n1)/2),因此我们可以使用「类」的方式进行存图,从而确保在遍历所有边的时候,复杂度严格为 O(m)O(m),而不是 O(n^2)O(n2)


网络异常,图片无法展示
|


代码:


class Solution {
    class Edge {
        int x, y, w;
        Edge(int _x, int _y, int _w) {
            x = _x; y = _y; w = _w;
        }
    }
    int N = 110, INF = 0x3f3f3f3f;
    int[] dist = new int[N];
    List<Edge> list = new ArrayList<>();
    int n, m, s, t, k;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n = _n; s = _src; t = _dst; k = _k + 1;
        for (int[] f : flights) {
            list.add(new Edge(f[0], f[1], f[2]));
        }
        m = list.size();
        int ans = bf();
        return ans > INF / 2 ? -1 : ans;
    }
    int bf() {
        Arrays.fill(dist, INF);
        dist[s] = 0;
        for (int i = 0; i < k; i++) {
            int[] clone = dist.clone();
            for (Edge e : list) {
                int x = e.x, y = e.y, w = e.w;
                dist[y] = Math.min(dist[y], clone[x] + w);
            }
        }
        return dist[t];
    }
}
复制代码


  • 时间复杂度:共进行 k + 1k+1 次迭代,每次迭代备份数组复杂度为 O(n)O(n),然后遍历所有的边进行松弛操作,复杂度为 O(m)O(m)。整体复杂度为 O(k * (n + m))O(k(n+m))
  • 空间复杂度:O(n + m)O(n+m)


Bellman Ford



更进一步,由于 Bellman Ford 核心操作需要遍历所有的边,因此也可以直接使用 flightsflights 数组作为存图信息,而无须额外存图。


网络异常,图片无法展示
|


代码:


class Solution {
    int N = 110, INF = 0x3f3f3f3f;
    int[] dist = new int[N];
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        Arrays.fill(dist, INF);
        dist[src] = 0;
        for (int limit = 0; limit < k + 1; limit++) {
            int[] clone = dist.clone();
            for (int[] f : flights) {
                int x = f[0], y = f[1], w = f[2];
                dist[y] = Math.min(dist[y], clone[x] + w);
            }
        }
        return dist[dst] > INF / 2 ? -1 : dist[dst];
    }
}
复制代码


  • 时间复杂度:共进行 k + 1k+1 次迭代,每次迭代备份数组复杂度为 O(n)O(n),然后遍历所有的边进行松弛操作,复杂度为 O(m)O(m)。整体复杂度为 O(k * (n + m))O(k(n+m))
  • 空间复杂度:O(n)O(n)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.787 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
机器学习/深度学习
【刷穿 LeetCode】1109. 航班预订统计 :「差分」&「线段树」
【刷穿 LeetCode】1109. 航班预订统计 :「差分」&「线段树」
LeetCode 1109. 航班预订统计
这里有 n 个航班,它们分别从 1 到 n 进行编号。
102 0
|
算法
[leetcode] 787 K 站中转内最便宜的航班
题目大意: 给定一个有向图,从u → v 有一条长度为w 的边 问从s 到t 的路径中,最多经过k 次中转的最短路径是多少?
170 0
[leetcode] 787 K 站中转内最便宜的航班
|
5天前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
2月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
44 6
|
2月前
|
Python
【Leetcode刷题Python】剑指 Offer 26. 树的子结构
这篇文章提供了解决LeetCode上"剑指Offer 26. 树的子结构"问题的Python代码实现和解析,判断一棵树B是否是另一棵树A的子结构。
38 4
|
2月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
82 2
|
5天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
2月前
|
索引 Python
【Leetcode刷题Python】从列表list中创建一颗二叉树
本文介绍了如何使用Python递归函数从列表中创建二叉树,其中每个节点的左右子节点索引分别是当前节点索引的2倍加1和2倍加2。
38 7
|
2月前
|
Python
【Leetcode刷题Python】剑指 Offer 30. 包含min函数的栈
本文提供了实现一个包含min函数的栈的Python代码,确保min、push和pop操作的时间复杂度为O(1)。
20 4