leetcode
leetcode 551 ~ 600
金币路径

金币路径

难度:

标签:

题目描述

代码结果

运行时间: 66 ms, 内存: 16.2 MB


/*
 * LeetCode 656: Coin Path (Using Java Streams)
 * Given an array A (index starts at 1) consisting of N integers: A1, A2, ..., AN and an integer B. 
 * The integer B denotes that from Ai, you can jump to Ai+1, Ai+2, ..., Ai+B if 1 <= i + j <= N. 
 * Find the minimum cost path to reach AN from A1, and output the indices of this path. 
 * If there are multiple paths with the same cost, return the lexicographically smallest such path. 
 * If no such path exists, return an empty list.
 */
 
import java.util.*;
import java.util.stream.*;
 
public class CoinPathStream {
    public List<Integer> cheapestJump(int[] A, int B) {
        int N = A.length;
        int[] cost = new int[N];
        int[] path = new int[N];
        Arrays.fill(cost, Integer.MAX_VALUE);
        cost[0] = 0;
        List<Integer>[] res = new List[N];
        for (int i = 0; i < N; i++) res[i] = new ArrayList<>();
        res[0].add(1);
 
        IntStream.range(0, N).forEach(i -> {
            if (A[i] == -1) return;
            IntStream.range(i + 1, Math.min(i + B + 1, N)).forEach(j -> {
                if (A[j] == -1) return;
                int newCost = cost[i] + A[j];
                if (newCost < cost[j]) {
                    cost[j] = newCost;
                    path[j] = i;
                    res[j] = new ArrayList<>(res[i]);
                    res[j].add(j + 1);
                } else if (newCost == cost[j]) {
                    List<Integer> newPath = new ArrayList<>(res[i]);
                    newPath.add(j + 1);
                    if (comparePaths(newPath, res[j]) < 0) {
                        res[j] = newPath;
                    }
                }
            });
        });
 
        return res[N - 1];
    }
 
    private int comparePaths(List<Integer> path1, List<Integer> path2) {
        for (int i = 0; i < Math.min(path1.size(), path2.size()); i++) {
            if (!path1.get(i).equals(path2.get(i))) {
                return path1.get(i) - path2.get(i);
            }
        }
        return path1.size() - path2.size();
    }
}
 

解释

方法:

这个题解使用了动态规划的思路。从终点开始倒序遍历数组,对于每个位置,计算从该位置跳到终点的最小花费。状态转移方程为:dp[i] = min(dp[j] + coins[i]),其中 i + 1 <= j <= min(n, i + maxJump + 1)。同时使用 nxt 数组记录每个位置的最优下一跳位置,用于最后重构最小花费路径。

时间复杂度:

O(n * maxJump),最坏情况下为 O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在动态规划中,为什么选择从终点开始倒序遍历数组,而不是从起点开始正序遍历?
倒序遍历数组从终点开始,这样可以确保在计算某个位置 i 的最小花费时,所有可能跳跃到的后续位置 j 的最小花费已经被计算过了。这种方式符合动态规划中的‘无后效性’,即后续的状态不会影响先前的状态。如果从起点开始,我们需要在每次计算时预先知道未来的状态,这在动态规划中通常是不可行的。
🦆
在处理`coins[i] == -1`的情况时,直接跳过该位置是否会影响到最终的路径重构?
直接跳过`coins[i] == -1`的位置是必要的,因为这表示从该位置无法进行任何有效跳跃(视为无法通行的阻碍)。在路径重构时,跳过这些位置不会影响路径的正确性,因为这些位置本身就不应该出现在任何最小花费路径上。
🦆
状态转移方程`dp[i] = min(dp[j] + coins[i])`里为什么是加上`coins[i]`而不是`coins[j]`?
在状态转移方程中,`dp[i]`表示从位置i跳到终点的最小总花费。当考虑从位置i跳到位置j的花费时,我们必须支付从i位置开始的代价`coins[i]`,而`dp[j]`已经包含了从j到终点的最小花费。因此,适合的转移方程是将当前位置的花费与目标位置的已知最小花费相加。
🦆
当`maxJump`的值非常大时,存在优化内层循环的算法吗?
当`maxJump`非常大时,内层循环可能会变得效率低下。一种可能的优化方法是使用数据结构,如线段树或最小堆,这些能够快速检索和更新区间最小值。例如,使用线段树可以在对数时间内进行查询和更新操作,从而减少每次更新`dp[i]`时的时间复杂度。

相关问题

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

 

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

 

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

 

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

 

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000