45. 跳跃游戏 II (Medium)

专题归类: 11-贪心 LeetCode 链接: https://leetcode.cn/problems/jump-game-ii/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给定一个长度为 n非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度

你的目标是使用最少的跳跃次数到达数组的最后一个下标。假设你总是可以到达数组的最后一个位置。

返回最少的跳跃次数。

示例 1:

输入:nums = [2,3,1,1,4]
输出:2
解释:跳到最后一个下标的最小跳跃次数是 2。
     从下标 0 跳到下标 1(跳 1 步),然后从下标 1 跳 3 步到达最后一个下标。

示例 2:

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

题目详细分析

数据范围: 1 <= nums.length <= 10^40 <= nums[i] <= 10^5

核心约束:

  • 题目保证一定可以到达最后一个位置(与前一题 “55. 跳跃游戏” 不同)
  • 每个位置可以跳 [1, nums[i]] 步之间的任意长度
  • 目标是最少步数,而非”能否到达”

关键洞察:

  • 这是一个最少步数问题,等价于 BFS 求最短路径:每条边从位置 i 指向 [i+1, i+nums[i]] 的所有位置
  • 但 BFS 需要显式建图或 O(n^2) 遍历,对于 n=10^4 可行但不优
  • 贪心思想:在当前步数能到达的范围内,选择能跳得最远的那一个位置作为下一步的起点
  • 核心是区间分层的思想——第 k 步能到达的范围是一个连续区间,BFS 的每一层就是一步能到达的区间

小白版直白理解

就像玩跳格子游戏,你站在起点,每个格子上写着”从这里最远能跳几格”。你想用最少的跳跃次数到终点。

策略很简单:在当前这一步能跳到的范围内,选一个能让你下一步跳得最远的位置。 就像你要过一条河,先看看脚底下这块区域(当前步数可达范围),从这些石头里选一块能让你下一次跳得最远的那块。

具体点:你的第一步能跳的范围是 [1, nums[0]],在这个范围内,你要找到哪个位置能让你再跳得更远(即 i + nums[i] 最大),选它作为下一步的起点。两步能到的范围就变成了 [原范围, 最远位置],以此类推。


解题思路

思路一:贪心——区间分层 BFS(推荐)

思路讲解: 维护三个变量:

  • end:当前步数能到达的最远边界(当前 BFS 层的右边界)
  • max_reach:遍历过程中能到达的最远位置(下一层能到的最远位置)
  • steps:已使用的步数

遍历数组(不访问最后一个元素,因为不需要从最后一个位置起跳):

  1. 更新 max_reach = max(max_reach, i + nums[i])
  2. 如果 i == end,说明当前层遍历完毕,步数 +1,将 end 更新为 max_reach(进入下一层)

每步操作相当于:在当前层内探索,找到下一层能到达的最远距离,当走完当前层时,步数增加,进入下一层。

def jump(nums):
    n = len(nums)
    if n == 1:
        return 0
    
    end = 0        # 当前步数能到达的最远边界
    max_reach = 0  # 遍历过程中能到达的最远位置
    steps = 0
    
    # 不需要遍历最后一个元素(n-1),因为到达 n-1 时已经完成
    for i in range(n - 1):
        max_reach = max(max_reach, i + nums[i])
        
        # 到达当前步数的边界,步数 +1,更新边界
        if i == end:
            steps += 1
            end = max_reach
    
    return steps

时间复杂度: O(n) | 空间复杂度: O(1)

思路二:逆向贪心(从后往前)

思路讲解: 从终点出发,每次找能跳到当前位置的最左边的位置(贪心地让每一步尽量靠左)。虽然直观,但最坏时间复杂度可能退化到 O(n^2)

def jump(nums):
    n = len(nums)
    position = n - 1
    steps = 0
    while position > 0:
        for i in range(position):
            if i + nums[i] >= position:
                position = i
                steps += 1
                break
    return steps

时间复杂度: O(n^2)(最坏) | 空间复杂度: O(1)

思路三:动态规划

思路讲解: dp[i] 表示到达位置 i 的最小步数。对于每个可达位置 j,更新 dp[j] = min(dp[j], dp[i] + 1)。复杂度较高,仅作思路参考。

def jump(nums):
    n = len(nums)
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(n):
        for j in range(1, nums[i] + 1):
            if i + j < n:
                dp[i + j] = min(dp[i + j], dp[i] + 1)
    return dp[n - 1]

时间复杂度: O(n^2)(最坏) | 空间复杂度: O(n)


易错点

  • 数组长度为 1:已经在终点,不需要跳跃,返回 0。正向贪心的循环中由于 range(n - 1)n=1 时为空,但在代码开头直接处理更安全
  • 不需要遍历最后一个元素range(n - 1) 而非 range(n),因为到达 n-1 时已经完成任务,不需要再从最后一个位置起跳
  • end 的更新时机:必须在 i == end 时更新步数,然后将 end 设为 max_reach。如果提前更新步数会导致计数错误
  • max_reach 可能小于 end 的情况:如果某次边界内所有位置的跳跃能力都很差,max_reach 可能等于 end(即无法前进),但题目保证可达所以不会死循环
  • 边界条件 end = 0:第一步时 i == 0 == end,会触发步数 +1,这是正确的——第一次起跳就计为一步

框架提炼

贪心模板:最少跳跃 / BFS 分层

def minSteps(nums):
    end = 0          # 当前层的右边界
    max_reach = 0    # 下一层能达到的最远位置
    steps = 0
    
    for i in range(len(nums) - 1):  # 不遍历最后一个元素
        max_reach = max(max_reach, i + nums[i])
        if i == end:
            steps += 1
            end = max_reach
    
    return steps

适用场景: 这个模板适用于给定跳跃范围,求最少到达终点的步数的问题。本质上是 BFS 在一维区间上的优化实现——将 BFS 的”显式队列”优化为”区间端点”。

核心思想可推广到:

  • 区间覆盖问题:给定多个区间,用最少数量的区间覆盖目标范围
  • 最少跳跃次数:求”最少步数到终点”的通用解法

关联题目