暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【每日一题】787. K 站中转内最便宜的航班:一题四解:BFS & DFS & DP & 记忆化搜索 & 优化,慢慢来!

彤哥来刷题啦 2021-09-01
472

题目描述(Medium)

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

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

提示:









链接:https://leetcode-cn.com/problems/cheapest-flights-within-k-stops

方法一、记忆化搜索

本题显然是可以使用DFS来做的,只需要把从 src 到 dst 的所有路径搜索一遍即可,但是,当用例非常大时,有可能超时,所以,我们需要加上记忆化,对于搜索过的路径,从缓存中拿过来使用即可。

加记忆化的过程也比较简单,就是找到你的 dfs 函数参数中变的部分即可,比如,我们下面的代码中,变的部分就是 i 和 k,所以,声明一个 memo 数组记录已经遍历过的节点。

代码如下,比较简单,注意边界及返回值:

class Solution {

    int INF = 1000007;

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        // k表示经过的节点,我们转成边数(步数),这样好计算一些
        int[][] memo = new int[n][k+2];
        int ans = dfs(flights, src, dst, k + 1, memo);
        return ans >= INF ? -1 : ans;
    }

    // 表示从 i 到 dst 的走 k 步的最小价格
    private int dfs(int[][] flights, int i, int dst, int k, int[][] memo) {
        if (k < 0) {
            return INF;
        }

        if (i == dst) {
            return 0;
        }

        if (memo[i][k] != 0) {
            return memo[i][k];
        }

        int min = INF;
        for (int[] flight : flights) {
            // 遍历 i 的下一个节点
            if (flight[0] == i) {
                min = Math.min(min, dfs(flights, flight[1], dst, k - 1, memo) + flight[2]);
            }
        }

        memo[i][k] = min;

        return min;
    }
}

  • 时间复杂度:,题目给定的 的范围为,与 成正比,我们这里要遍历 层,每层都需要遍历一遍 数组。
  • 空间复杂度: 数组占用 的额外空间,递归栈占用 的额外空间。

运行结果如下:

image-20210824145227340

方法二、动态规划

有了记忆化搜索,转成动态规划就比较简单了,我们可以这样定义动态规划:

  • 状态定义:dp[i][k]
    表示从 i
    点到 dst
    k
    步的最少价格
  • 状态转移:,其中 的下一个节点
  • 初始值:初始时,dst 到 dst 走 0 步的最少价格为 0,其它为无穷大
  • 返回值:取 中的最小值即可

好了,请看代码:

class Solution {

    int INF = 1000007;

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        return dp(n, flights, src, dst, k);
    }

    private int dp(int n, int[][] flights, int src, int dst, int K) {
        // dp[i][k]表示从i点到dst走k步的最少价格
        // dp[i][k]=min(dp[i_next][k-1] + g[i][j])
        int[][] dp = new int[n][K+2];
        for (int i = 0; i < n; i++) {
            Arrays.fill(dp[i], INF);
        }
        dp[dst][0] = 0;
        for (int k = 1; k <= K + 1; k++) {
            for (int[] flight : flights) {
                dp[flight[0]][k] = Math.min(dp[flight[0]][k], dp[flight[1]][k - 1] + flight[2]);
            }
        }

        int ans = IntStream.of(dp[src]).min().getAsInt();

        return ans >= INF ? -1 : ans;
    }
}

  • 时间复杂度:,题目给定的 的范围为,与 成正比,我们这里要遍历 次,每次都需要遍历一遍 数组。
  • 空间复杂度: 数组占用 的额外空间。

运行结果如下:

image-20210824145403017

方法三、动态数组 + 优化

可以看到,方法二中, 只与 有关,所以,我们可以使用滚动数组来优化空间。

所谓滚动数组,即申请一个两行的数组,轮动使用,计算当前行为 x & 1,上一行就是 (x-1) & 1 根据你的变量变化。

请看代码:

class Solution {

    int INF = 1000007;

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        return dp(n, flights, src, dst, k);
    }

    private int dp(int n, int[][] flights, int src, int dst, int K) {
        // dp[i][k]表示从i点到dst走k步的最少价格
        // dp[i][k]=min(dp[i_next][k-1] + g[i][j])
        int ans = INF;

        int[][] dp = new int[2][n];
        Arrays.fill(dp[0], INF);
        dp[0][dst] = 0;
        for (int k = 1; k <= K + 1; k++) {
            // 防止之前的值干扰,每次都要初始化
            Arrays.fill(dp[k & 1], INF);
            for (int[] flight : flights) {
                dp[k & 1][flight[0]] = Math.min(dp[k & 1][flight[0]], dp[(k - 1) & 1][flight[1]] + flight[2]);
            }
            ans = Math.min(ans, dp[k & 1][src]);
        }

        return ans >= INF ? -1 : ans;
    }
}

  • 时间复杂度:,题目给定的 的范围为,与 成正比,我们这里要遍历 次,每次都需要遍历一遍 数组。
  • 空间复杂度: 数组占用 的额外空间。

运行结果如下:

image-20210824151429056

方法四、BFS + 剪枝

既然是求最短路径,我们使用 BFS 也是可以的,不过,考虑到用例非常大的情况,我们需要认真的思考剪枝的方案,本题我们申请一个 数组记录从 src 到 i 的最小价格,下次再遍历到 i 时,如果价格比记录的值小才计算,否则直接丢弃。

请看代码:

class Solution {

    int INF = 1000007;

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        return bfs(n, flights, src, dst, k);
    }

    private int bfs(int n, int[][] flights, int src, int dst, int k) {
        // 整理题目给定的flights,转换成每个节点的子节点有哪些
        List<int[]>[] g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }

        for (int[] flight : flights) {
            g[flight[0]].add(new int[] {flight[1], flight[2]});
        }

        // 表示src到i到最小价格
        int[] ans = new int[n];
        Arrays.fill(ans, INF);
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[] {src, 0});
        // 退出条件加上 k 的限制
        while (!queue.isEmpty() && k + 1 > 0) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int[] poll = queue.poll();
                for (int[] path : g[poll[0]]) {
                    int distance = poll[1] + path[1];
                    // 剪枝1,小于 i 之前记录的最小值,且小于 dst 之前记录的最小值
                    if (distance < ans[path[0]] && distance < ans[dst]) {
                        ans[path[0]] = distance;
                        // 剪枝2,到 dst 了就不用继续往下了
                        if (path[0] != dst) {
                            queue.offer(new int[] {path[0], distance});
                        }
                    }
                }
            }
            k--;
        }

        return ans[dst] >= INF ? -1 : ans[dst];
    }
}

  • 时间复杂度:,最多遍历 K 层,每层最多有 n-1 个元素,每个元素最多有 n-1 个子节点。
  • 空间复杂度: 数组最多只占用 的额外空间。

运行结果如下:

image-20210824155024151

最后

如果对你有帮助,请点个赞吧,谢谢^^

也可以关注我的公号【彤哥来刷题啦】,每日分享题解,一起刷题,一起拿全家桶。


文章转载自彤哥来刷题啦,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论