n 个城市通过 m 个航班连接。每个航班都从城市 u 开始,以价格 w 抵达 v

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

  1. 示例 1:
  2. 输入:
  3. n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
  4. src = 0, dst = 2, k = 1
  5. 输出: 200
  6. 解释:
  7. 城市航班图如下
  8. 从城市 0 到城市 2 1 站中转以内的最便宜价格是 200,如图中红色所示。
  9. 示例 2:
  10. 输入:
  11. n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
  12. src = 0, dst = 2, k = 0
  13. 输出: 500
  14. 解释:
  15. 城市航班图如下
  16. 从城市 0 到城市 2 0 站中转以内的最便宜价格是 500,如图中蓝色所示。

提示:

  • n 范围是 [1, 100],城市标签从 0n`` - 1.
  • 航班数量范围是 [0, n * (n - 1) / 2].
  • 每个航班的格式 (src, ``dst``, price).
  • 每个航班的价格范围是 [1, 10000].
  • k 范围是 [0, n - 1].
  • 航班没有重复,且不存在环路

首先我们要建立这个图,选取的数据结构就是邻接链表的形式,具体来说就是建立每个结点和其所有能到达的结点的集合之间的映射,然后就是用DFS来遍历这个图了,用变量cur表示当前遍历到的结点序号,还是当前剩余的转机次数K,访问过的结点集合visited,当前累计的价格out,已经全局的最便宜价格res。在递归函数中,首先判断如果当前cur为目标结点dst,那么结果res赋值为out,并直接返回。你可能会纳闷为啥不是取二者中较小值更新结果res,而是直接赋值呢?原因是我们之后做了剪枝处理,使得out一定会小于结果res。然后判断如果K小于0,说明超过转机次数了,直接返回。然后就是遍历当前结点cur能到达的所有结点了,对于遍历到的结点,首先判断如果当前结点已经访问过了,直接跳过。或者是当前价格out加上到达这个结点需要的价格之和大于结果res的话,那么直接跳过。这个剪枝能极大的提高效率,是压线过OJ的首要功臣。之后就是标记结点访问,调用递归函数,以及还原结点状态的常规操作了,参见代码如下:

解法一:

  1. class Solution {
  2. public:
  3. int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
  4. int res = INT_MAX;
  5. unordered_map<int, vector<vector<int>>> m;
  6. unordered_set<int> visited{{src}};
  7. for (auto flight : flights) {
  8. m[flight[0]].push_back({flight[1], flight[2]});
  9. }
  10. helper(m, src, dst, K, visited, 0, res);
  11. return (res == INT_MAX) ? -1 : res;
  12. }
  13. void helper(unordered_map<int, vector<vector<int>>>& m, int cur, int dst, int K, unordered_set<int>& visited, int out, int& res) {
  14. if (cur == dst) {res = out; return;}
  15. if (K < 0) return;
  16. for (auto a : m[cur]) {
  17. if (visited.count(a[0]) || out + a[1] > res) continue;
  18. visited.insert(a[0]);
  19. helper(m, a[0], dst, K - 1, visited, out + a[1], res);
  20. visited.erase(a[0]);
  21. }
  22. }
  23. };

下面这种解法是用BFS来做的,还是来遍历图,不过这次是一层一层的遍历,需要使用queue来辅助。前面建立图的数据结构的操作和之前相同,BFS的写法还是经典的写法,但需要注意的是这里也同样的做了剪枝优化,当当前价格加上新到达位置的价格之和大于结果res的话直接跳过。最后注意如果超过了转机次数就直接break,参见代码如下:

解法二:

  1. class Solution {
  2. public:
  3. int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
  4. int res = INT_MAX, cnt = 0;
  5. unordered_map<int, vector<vector<int>>> m;
  6. queue<vector<int>> q{{{src, 0}}};
  7. for (auto flight : flights) {
  8. m[flight[0]].push_back({flight[1], flight[2]});
  9. }
  10. while (!q.empty()) {
  11. for (int i = q.size(); i > 0; --i) {
  12. auto t = q.front(); q.pop();
  13. if (t[0] == dst) res = min(res, t[1]);
  14. for (auto a : m[t[0]]) {
  15. if (t[1] + a[1] > res) continue;
  16. q.push({a[0], t[1] + a[1]});
  17. }
  18. }
  19. if (cnt++ > K) break;
  20. }
  21. return (res == INT_MAX) ? -1 : res;
  22. }
  23. };

再来看使用Bellman Ford算法的解法,关于此算法的detail可以上网搜帖子看看。核心思想还是用的动态规划Dynamic Programming,最核心的部分就是松弛操作Relaxation,也就是DP的状态转移方程。这里我们使用一个二维DP数组,其中dp[i][j]表示最多飞i次航班到达j位置时的最少价格,那么dp[0][src]初始化为0,因为飞0次航班的价格都为0,转机K次,其实就是飞K+1次航班,我们开始遍历,i从1到K+1,每次dp[i][src]都初始化为0,因为在起点的价格也为0,然后即使遍历所有的航班x,更新dp[i][x[1]],表示最多飞i次航班到达航班x的目的地的最低价格,用最多飞i-1次航班,到达航班x的起点的价格加上航班x的价格之和,二者中取较小值更新即可,参见代码如下:

解法三:

  1. class Solution {
  2. public:
  3. int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
  4. vector<vector<int>> dp(K + 2, vector<int>(n, 1e9));
  5. dp[0][src] = 0;
  6. for (int i = 1; i <= K + 1; ++i) {
  7. dp[i][src] = 0;
  8. for (auto x : flights) {
  9. dp[i][x[1]] = min(dp[i][x[1]], dp[i - 1][x[0]] + x[2]);
  10. }
  11. }
  12. return (dp[K + 1][dst] >= 1e9) ? -1 : dp[K + 1][dst];
  13. }
  14. };

我们可以稍稍优化下上面解法的空间复杂度,使用一个一维的DP数组即可,具体思路没有啥太大的区别,参见代码如下:

解法四:

  1. class Solution {
  2. public:
  3. int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
  4. vector<int> dp(n, 1e9);
  5. dp[src] = 0;
  6. for (int i = 0; i <= K; ++i) {
  7. vector<int> t = dp;
  8. for (auto x : flights) {
  9. t[x[1]] = min(t[x[1]], dp[x[0]] + x[2]);
  10. }
  11. dp = t;
  12. }
  13. return (dp[dst] >= 1e9) ? -1 : dp[dst];
  14. }
  15. };

方法一:计算到各节点的最短距离【通过】

思路和算法

假设 pre[node] 是到经过 T 个节点到达目的节点 node 的最短距离,然后求解经过 T+1 个节点到达目的节点的最短距离。对于每一条连接城市 u 和 v,成本为 w的航线,更新后的最短距离为 dis[v] = min(dis[v], pre[u] + w)。

实际上,初始令 dis = dist[0] 和 pre = dist[1],在下一步循环迭代 (i = 1) 时,可以重复使用 dis = dist[1] 和 pre = dist[0],以此类推。

java

  1. class Solution {
  2. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
  3. int[][] dist = new int[2][n];
  4. int INF = Integer.MAX_VALUE / 2;
  5. Arrays.fill(dist[0], INF);
  6. Arrays.fill(dist[1], INF);
  7. dist[0][src] = dist[1][src] = 0;
  8. for (int i = 0; i <= K; ++i)
  9. for (int[] edge: flights)
  10. dist[i&1][edge[1]] = Math.min(dist[i&1][edge[1]], dist[~i&1][edge[0]] + edge[2]);
  11. return dist[K&1][dst] < INF ? dist[K&1][dst] : -1;
  12. }
  13. }

python

  1. class Solution(object):
  2. def findCheapestPrice(self, n, flights, src, dst, K):
  3. dist = [[float('inf')] * n for _ in xrange(2)]
  4. dist[0][src] = dist[1][src] = 0
  5. for i in xrange(K+1):
  6. for u, v, w in flights:
  7. dist[i&1][v] = min(dist[i&1][v], dist[~i&1][u] + w)
  8. return dist[K&1][dst] if dist[K&1][dst] < float('inf') else -1

复杂度分析

时间复杂度:O(E∗K),其中 E 是 flights 的长度。

空间复杂度:O(n),存储 dis 和 pre。

方法二:Dijkstra【通过】

思路

寻找源到目标的最低花费,Dijkstra 是一个好的算法。

Dijstra 算法的基本思想就是:按照 cost 从小到大的顺序扩展所有可能的飞行路线,当城市被添加到 dst 时,dst 中对应的值就是到达该城市的最低花费。

算法

在 Dijkstra 算法中,借助优先级队列持续搜索花费最低的下一个城市。

如果查找到某个城市,它原本的路线成本更低或者中转次数过多,则无需再搜索它。否则,如果搜索到目的城市,那么当前花费就是最低成本,因为每次最先搜索的就是最低成本航线。

否则,如果从 node 城市出发的航线花费更低,则将该节点加入到优先级队列用于搜索。

java

  1. class Solution {
  2. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
  3. int[][] graph = new int[n][n];
  4. for (int[] flight: flights)
  5. graph[flight[0]][flight[1]] = flight[2];
  6. Map<Integer, Integer> best = new HashMap();
  7. PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
  8. pq.offer(new int[]{0, 0, src});
  9. while (!pq.isEmpty()) {
  10. int[] info = pq.poll();
  11. int cost = info[0], k = info[1], place = info[2];
  12. if (k > K+1 || cost > best.getOrDefault(k * 1000 + place, Integer.MAX_VALUE))
  13. continue;
  14. if (place == dst)
  15. return cost;
  16. for (int nei = 0; nei < n; ++nei) if (graph[place][nei] > 0) {
  17. int newcost = cost + graph[place][nei];
  18. if (newcost < best.getOrDefault((k+1) * 1000 + nei, Integer.MAX_VALUE)) {
  19. pq.offer(new int[]{newcost, k+1, nei});
  20. best.put((k+1) * 1000 + nei, newcost);
  21. }
  22. }
  23. }
  24. return -1;
  25. }
  26. }

python

  1. class Solution(object):
  2. def findCheapestPrice(self, n, flights, src, dst, K):
  3. graph = collections.defaultdict(dict)
  4. for u, v, w in flights:
  5. graph[u][v] = w
  6. best = {}
  7. pq = [(0, 0, src)]
  8. while pq:
  9. cost, k, place = heapq.heappop(pq)
  10. if k > K+1 or cost > best.get((k, place), float('inf')): continue
  11. if place == dst: return cost
  12. for nei, wt in graph[place].iteritems():
  13. newcost = cost + wt
  14. if newcost < best.get((k+1, nei), float('inf')):
  15. heapq.heappush(pq, (newcost, k+1, nei))
  16. best[k+1, nei] = newcost
  17. return -1

复杂度分析

时间复杂度:O(E+nlogn),其中 EE 是航线的数量。

空间复杂度:O(n)O(n),优先级队列的大小。

记忆化搜索

  1. class Solution {
  2. // 看到了DAG
  3. // dp[i][j][k],从i站到j站最多中转k站的最小代价!看题目描述自然想到这个状态
  4. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
  5. int[][] cost = new int[n][n];
  6. for(int i = 0; i < flights.length; i++){
  7. int st = flights[i][0];
  8. int en = flights[i][1];
  9. int price = flights[i][2];
  10. cost[st][en] = price;
  11. }
  12. int[][][] dp = new int[n][n][K+2];
  13. for(int i = 0; i < n; i++){
  14. for(int j = 0; j < n; j++){
  15. Arrays.fill(dp[i][j], -1);
  16. }
  17. }
  18. int res = f(cost, src, dst, K+1, dp);
  19. return res >= 1000000 ? -1 : res;
  20. }
  21. private int f(int[][] cost, int src, int dst, int K, int[][][] dp){
  22. if(K < 0){
  23. return 1000000;
  24. }
  25. if(dp[src][dst][K] != -1){
  26. return dp[src][dst][K];
  27. }
  28. if(src == dst){
  29. return dp[src][dst][K] = 0;
  30. }
  31. int ans = Integer.MAX_VALUE;
  32. for(int i = 0; i < cost.length; i++){
  33. if(cost[src][i] != 0){
  34. ans = Math.min(ans, cost[src][i] + f(cost, i, dst, K-1, dp));
  35. }
  36. }
  37. return dp[src][dst][K] = (ans == Integer.MAX_VALUE ? 1000000 : ans);
  38. }
  39. }

LeetCode——787. K 站中转内最便宜的航班的更多相关文章

  1. Java实现 LeetCode 787 K 站中转内最便宜的航班(两种DP)

    787. K 站中转内最便宜的航班 有 n 个城市通过 m 个航班连接.每个航班都从城市 u 开始,以价格 w 抵达 v. 现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是 ...

  2. leetcode 787. K 站中转内最便宜的航班

    问题描述 有 n 个城市通过 m 个航班连接.每个航班都从城市 u 开始,以价格 w 抵达 v. 现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 src 到 dst ...

  3. 【力扣leetcode】-787. K站中转内最便宜的航班

    题目描述: 有 n 个城市通过一些航班连接.给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 p ...

  4. [Swift]LeetCode787. K 站中转内最便宜的航班 | Cheapest Flights Within K Stops

    There are n cities connected by m flights. Each fight starts from city u and arrives at v with a pri ...

  5. leetcode_787【K 站中转内最便宜的航班】

    有 n 个城市通过 m 个航班连接.每个航班都从城市 u 开始,以价格 w 抵达 v. 现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 src 到 dst 最多经过 ...

  6. [LeetCode] Cheapest Flights Within K Stops K次转机内的最便宜的航班

    There are n cities connected by m flights. Each fight starts from city u and arrives at v with a pri ...

  7. [链表]LeetCode 25 K组一个翻转链表

    LeetCode 25 k组一个翻转链表 TITLE 示例 1: 输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5] 示例 2: 输入:head = [1,2,3, ...

  8. [LeetCode] 787. Cheapest Flights Within K Stops K次转机内的最便宜航班

    There are n cities connected by m flights. Each fight starts from city u and arrives at v with a pri ...

  9. [LeetCode] 787. Cheapest Flights Within K Stops_Medium tag: Dynamic Programming, BFS, Heap

    There are n cities connected by m flights. Each fight starts from city u and arrives at v with a pri ...

随机推荐

  1. Centos 时间与主机时间不匹配问题解决

    有时候新安装的虚拟机的系统时间会和主机的时间差8个小时,这是因为虚拟机和主机所在的时区不同. 这会导致当你往mysql数据库插入数据时如果设置时间为current_time结果会和虚拟机保持一致. 1 ...

  2. Redis哨兵机制原理

    1.概述 Redis Sentinel是一个分布式系统,为Redis提供高可用性解决方案.可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip p ...

  3. Flink 笔记(一)

    简介 Flink是一个低延迟.高吞吐.统一的大数据计算引擎, Flink的计算平台可以实现毫秒级的延迟情况下,每秒钟处理上亿次的消息或者事件. 同时Flink提供了一个Exactly-once的一致性 ...

  4. P 1020 月饼

    转跳点:

  5. PHP ~ 设置和读取 Cookie

    一,设置 Cookie setcookie("user",$user,time()+3600);     // user 为用户名,$user 为变量的值   二,读取 Cooki ...

  6. 【LeetCode】搜索旋转排序数组

    [问题]假设按照升序排序的数组在预先未知的某个点上进行了旋转. ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] ). 搜索一个给定的目标值,如果数组中存在这个 ...

  7. Charles中windows版本解决response乱码问题

    实际上三种,目前写了两种,加了之后有的不显示乱码,但是有的还是显示,第三种搜索结果是安装证书,但是本人安装后证书后未受到信任,所以暂时不知是否能够成功 1,在charles.ini中,手动增加一个vm ...

  8. Python学习——装饰器/decorator/语法糖

    装饰器 定义:本质是函数,为其他函数添加附加的功能. 原则:1.不能修改原函数的源代码 2.不能修改被原函数的调用方式 重点理解: 1.函数即“变量” 2.高阶函数:返回值中包含函数名 3.嵌套函数 ...

  9. 微服务和SpringCloud入门

    微服务和SpringCloud入门 微服务是什么 微服务的核心是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底去耦合,每个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度看就是一种 ...

  10. IDEA控制台输出中文乱码日志文件正常

    控制台中文输出乱码但输出的日志文件正常 idea.exe.vmoptions与idea64.exe.vmoptions已经配置 -Dfile.encoding=UTF-8 logback.xml中也配 ...