力扣787. K 站中转内最便宜的航班

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

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

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

难度中等430

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

现在给定所有的城市和航班,以及出发城市 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,如图中蓝色所示。

在这里插入图片描述

解析:

求最短路径,肯定可以用 BFS 算法来解决。因为 BFS 算法相当于从起始点开始,一步一步向外扩散,那当然是离起点越近的节点越先被遍历到,如果 BFS 遍历的过程中遇到终点,那么走的肯定是最短路径。

不过呢,我们在 BFS 算法框架详解 用的是普通的队列Queue来遍历多叉树,而对于加权图的最短路径来说,普通的队列不管用了,得用优先级队列PriorityQueue

因为:

​ 在多叉树(或者扩展到无权图)的遍历中,与其说边没有权重,不如说每条边的权重都是 1,起点与终点之间的路径权重就是它们之间「边」的条数。按照 BFS 算法一步步向四周扩散的逻辑,先遍历到的节点和起点之间的「边」更少,累计的权重当然少。

​ 换言之,先进入Queue的节点就是离起点近的,路径权重小的节点。

但对于加权图,路径中边的条数和路径的权重并不是正相关的关系了,有的路径可能边的条数很少,但每条边的权重都很大,那显然这条路径权重也会很大,很难成为最短路径。

所以,对于加权图的场景,我们需要优先级队列「自动排序」的特性,将路径权重较小的节点排在队列前面,以此为基础施展 BFS 算法。

采用动态规划思想

  • 若先不管k的限制:
    在这里插入图片描述

s1, s2是指向dst的相邻节点,它们之间的权重我是知道的,分别是w1, w2

只要我知道了从srcs1, s2的最短路径,我不就知道srcdst的最短路径了吗!

minPath(src, dst) = min(
    minPath(src, s1) + w1, 
    minPath(src, s2) + w2
)

这其实就是递归关系了

  • 考虑路径条数k+1条边(k个站点即k+1条边)的限制
  1. dp函数定义:

从起点src出发,k步之内(一步就是一条边)到达节点s的最小路径权重为dp(s, k)

  1. basecase
// 定义:从 src 出发,k 步之内到达 s 的最小成本
    int dp(int s, int k) {
        // 从 src 到 src,一步都不用走
        if (s == src) {
            return 0;
        }
        // 如果步数用尽,就无解了
        if (k == 0) {
            return -1;
        }

        // ...
    }

题目想求的最小机票开销就可以用dp(dst, K+1)来表示:

int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
    // 将中转站个数转化成边的条数
    K++;
    //...
    return dp(dst, K);
  1. 状态转移

在这里插入图片描述

s1, s2是指向dst的相邻节点,我只要能够在K - 1步之内从src到达s1, s2,那我就可以在K步之内从src到达dst

也就是如下关系式:

dp(dst, k) = min(
    dp(s1, k - 1) + w1, 
    dp(s2, k - 1) + w2
)
  • 实现细节:
  1. 怎么知道s1, s2是指向dst的相邻节点,他们之间的权重是w1, w2HashMap

给一个节点,就能知道有谁指向这个节点,还知道它们之间的权重。得用一个数据结构记录每个节点的「入度」indegree:

代码

class Solution {
    int src,dst;
    HashMap<Integer, List<int[]>> indegree;
    int[][] memo;
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        k++;// 将中转站个数转化成边的条数
        this.src=src;
        this.dst=dst;
        
        memo=new int[n][k+1];
        for(int[] row:memo){
            Arrays.fill(row,-888);
        }
        indegree=new HashMap<>();
        for(int[] f:flights){
            int from=f[0];
            int to=f[1];
            int price=f[2];
            // 记录谁指向该节点,以及之间的权重
            indegree.putIfAbsent(to,new LinkedList<>());
            indegree.get(to).add(new int[]{from,price});
        }
        return dp(dst,k);
    }
    int dp(int s,int k){
        // base case
        if(s==src) return 0;
        if(k==0) return -1;
        // 查备忘录,防止冗余计算
        if(memo[s][k]!=-888) return memo[s][k];
        int res=Integer.MAX_VALUE;
        if(indegree.containsKey(s)){
            // 当 s 有入度节点时,分解为子问题
            for(int[] v:indegree.get(s)){
                int from=v[0];
                int price=v[1];
                int subProblem=dp(from,k-1);
                if(subProblem!=-1){
                    res=Math.min(res,subProblem+price);
                }
            }
        }
        memo[s][k]= res == Integer.MAX_VALUE ? -1 : res;
        return memo[s][k];
  }
}
   
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值