题目描述

✅ 656. 成本最小路径

题意分析

从数组第一个位置出发,每次向右前进 1..B 个位置,不能落到值为 -1 的位置,跳跃时可以跨过这些位置。每个落脚位置都要支付对应费用,目标是到达最后一个位置的最小费用路径;费用相同则选择下标序列中字典序最小的一条。

跳跃方向只能向右,不会形成环。可以从终点向前计算每个位置的最优后缀,再记录下一步来恢复路径,最终输出从 1 开始的位置编号。

解法:从后往前 DP + next 恢复路径

核心思路

[!blue]

定义 dp[i] 为从位置 i 到终点的最小费用,包含 A[i] 本身;无法到达终点时记为 INF。终点合法时,dp[n-1] = A[n-1],因为已经不需要继续跳跃。若终点禁止落脚,整题直接无解。

从右往左处理位置 i,它的下一步只能是 i+1..min(i+B,n-1) 中的某个 j,而这些 dp[j] 都已经计算完成。跳过 dp[j] == INF 的后继,在可达后继中寻找最小 dp[j],再加上固定的 A[i]。若没有可达后继,或者当前位置本身禁止落脚,就保持不可达。

用 next[i] 记录所选后继。费用相同时应选择下标最小的 j:所有候选路径都以 i 开头,下一步不同就是它们第一个不同的位置,较小的 j 一定带来较小字典序。若下一步相同,则比较的是该后继的路径,而它已在更早的反向计算中保留了最小费用、最小字典序的后缀。因此局部的平局选择能递推成完整路径的字典序最优。

计算完成后,dp[0] == INF 就返回空结果;否则从 0 不断沿 next 前进。每次下标严格增大,合法的非终点状态又一定记录了后继,所以最终会到达终点,并由终点的 next = -1 结束。

解题步骤

  1. 若终点禁止落脚,返回空结果;否则将所有费用初始化为 INF、所有后继初始化为 -1,再设置终点费用。
  2. 从 n-2 向 0 枚举位置,跳过禁止落脚的位置。
  3. 枚举最多 B 步内的可达后继,优先最小费用,同费用选择更小后继下标。
  4. 有合法后继时更新 dp[i] 和 next[i],没有则保持不可达。
  5. 起点可达时沿 next 恢复路径,每个零基下标加一后加入答案。只有一个合法位置时,路径自然只包含起点本身。

代码实现

class Solution {
    public List<Integer> cheapestJump(int[] A, int B) {
        int n = A.length;

        if (A[n - 1] == -1) {
            return new ArrayList<>();
        }

        long INF = (long) 1e18;
        long[] dp = new long[n];
        int[] next = new int[n];

        Arrays.fill(dp, INF);
        Arrays.fill(next, -1);

        // 状态包含当前格子的费用,因此终点初始化为自身费用。
        dp[n - 1] = A[n - 1];

        for (int i = n - 2; i >= 0; i--) {
            if (A[i] == -1) {
                continue;
            }

            long best = INF;
            int bestJ = -1;
            int upper = Math.min(n - 1, i + B);

            for (int j = i + 1; j <= upper; j++) {
                if (dp[j] == INF) {
                    continue;
                }

                // 先比较费用,同费用优先选择下标更小的下一步。
                if (dp[j] < best || (dp[j] == best && j < bestJ)) {
                    best = dp[j];
                    bestJ = j;
                }
            }

            if (bestJ != -1) {
                dp[i] = best + A[i];
                next[i] = bestJ;
            }
        }

        if (dp[0] == INF) {
            return new ArrayList<>();
        }

        List<Integer> path = new ArrayList<>();
        int cur = 0;

        while (cur != -1) {
            path.add(cur + 1);
            cur = next[cur];
        }

        return path;
    }
}
func cheapestJump(A []int, B int) []int {
    n := len(A)
    if A[n-1] == -1 {
        return []int{}
    }

    const INF int64 = 1 << 62
    dp := make([]int64, n)
    next := make([]int, n)
    for i := range dp {
        dp[i] = INF
        next[i] = -1
    }

    // 状态包含当前格子的费用,因此终点初始化为自身费用。
    dp[n-1] = int64(A[n-1])
    for i := n - 2; i >= 0; i-- {
        if A[i] == -1 {
            continue
        }

        best := INF
        bestJ := -1
        upper := i + B
        if upper > n-1 {
            upper = n - 1
        }
        for j := i + 1; j <= upper; j++ {
            if dp[j] == INF {
                continue
            }
            // 先比较费用,同费用优先选择下标更小的下一步。
            if dp[j] < best || (dp[j] == best && j < bestJ) {
                best = dp[j]
                bestJ = j
            }
        }
        if bestJ != -1 {
            dp[i] = best + int64(A[i])
            next[i] = bestJ
        }
    }

    if dp[0] == INF {
        return []int{}
    }

    path := make([]int, 0)
    cur := 0
    for cur != -1 {
        path = append(path, cur+1)
        cur = next[cur]
    }
    return path
}

复杂度分析

  • 时间复杂度:$O(nB)$。每个位置最多检查 B 个后继,路径恢复最多再经过 n 个位置。
  • 空间复杂度:$O(n)$。保存最小费用和下一步位置,输出路径也不超过 n 个位置。

关键点总结

[!green]

  • 状态费用包含当前位置,终点也要计费,转移时只加一次 A[i]。
  • 只向右跳跃使状态图无环,从右向左处理即可先得到全部后继状态。
  • 先比较费用,同费用再比较下一步下标;后继已经保留最优后缀,无需复制整条路径。
  • 保存后继即可恢复答案,禁止位置和无法到达终点的位置都保持不可达。

易错点总结

[!yellow]

  • 把 -1 当作负费用参与求最小值,会选中禁止落脚的位置。
  • 初始化终点费用为零,会漏掉路径中最后一个位置的费用。
  • 字典序最小不等于跳跃次数最少;零费用位置可能让更长路径的字典序更小。
  • 费用更大时仍优先选较小下标,颠倒了费用与字典序的优先级。
  • 起点不可达时仍沿 next 输出,会把不完整路径当作答案;有效路径也要记得将下标加一。

相似题目

题目 难度 关联与区别
45. 跳跃游戏 II 中等 同样在数组中向前跳有限距离,原题最小化跳数,本题有位置费用、障碍和字典序并列规则。
64. 最小路径和 中等 同样在无环状态图上求最小路径和,本题转移来自后续一段下标而非两个网格邻居。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/47681401
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!