Leetcode Jump Game 1 & 2

贪心

Posted by Sirin on October 15, 2025
Jump Game

原题

55. Jump Game

45. Jump Game II

这两道题都是比较经典的贪心场景,通过局部最优解得到全局最优解。

第一题要判定能否到达,第二题在保证能够到达的情况下需要最少几步。

T1解答思路

首先对于第一题来说,我们既然要判定能否到达,第一个想法肯定是每一步的时候我都判定一下当前最远能到哪,如果能到最后一个位置,那就成功了。但怎么判定不成功呢?

我们就想到,可以把整个过程中能到达的最远位置记录下来,如果我们遍历到的当前位置比能到达的最远位置还要远的话,就说明这是不可能成功的。

为什么呢?因为我们每遍历到一个新位置,就会更新当前最远可达的位置,这就相当于在染色。位置i的最远可达位置i+nums[i],那么从ii+nums[i]之间的所有位置必然也是可达的。因此,在遍历数据的过程中,如果我们发现当前这个位置没有被染色覆盖,就说明从之前的任何一个位置,都不可能到达这里。既然这个位置不可能到达,后面的位置也必然不可能(染色是区间的,具有连续性)。

T1代码

func canJump(nums []int) bool {
    far := 0
    n := len(nums)

    for i := 0; i < n; i++ {
        if far < i {
            // 当前位置未被染色
            return false
        }

        // 尝试继续向后染色
        far = max(far, i+nums[i])
    }

    // 没有出现过未被染色的情况,必然可达
    return true
}

T2解答思路

对于第二题,则是在保证可达终点的情况下,求最少所需的跳跃次数。假设我们先用动态规划的思想去做,会首先想到用f[i]来记录抵达i位置所需要的最少步数,这样的话,我们只需将所有的f[i]初始化为一个很大的值(math.MaxInt或者是比题目中给出的len(nums)更大的值),然后将f[0]设置为0(初始时天然可达),在进行状态转移即可,状态转移方程可以表示为 \(\large f(i+j) = \min_{j\in[\; 1,\;n[i]\;]}(f(i)+1, f(i+j))\quad i+j<n\) 但我们注意到,如果每一个位置的可行距离很大的话,这样的更新需要较大的开销,如何避免对j的这样的一个循环呢?我们注意到,如果说i+j位置的到达步数是被i更新可达的话,它必然不会被i+1,i+2,...,i+j-1这些位置去更新了(因为那样不符合状态转移的$min$要求)。

因此我们可以推断,当前位置i的步数是由之前首个可达i的位置prev的步数+1来计算的。因此这个步数实际上是分段的多个区间(前闭后开),每个区间内的步数都是相同的。而当前第i个区间的步数,就是第i-1区间的步数+1所得。那么区间是如何分开的呢?那就是i-1区间内的某个位置所能到达的最远位置,就是第i个区间和第i+1个区间之间的分割点!第0个区间天然的就是nums[0]这个位置,虚拟的-1区间所产生的边界,就是在i=0的位置。因此,在遍历整个数组时,只要达到一个新的分割边界,我们就将步数+1。由此我们可以得到不需要内层循环的解法,这也就是我们的贪心解法。

但需要注意的是,遍历过程中,既然我们确保了终点必然可达,那么遍历时就不应该访问i=n-1处。为什么?因为我们步数的更新是设置在新区间的前端,这个更新的步数是用于跳跃到新区间所需的步数,而非抵达当前位置所需的步数(用i=0i=1就很好理解了),所以如果n=1的话就会导致一个错误的答案更新,把步数变成了未来下一次而非本次结算的代价了。

T2代码

func jump(nums []int) int {
    ans := 0
    nowMaxR := 0
    jumpPoint := 0
    n := len(nums)

    for i := 0; i < n-1; i++ {
        nowMaxR = max(nowMaxR, i + nums[i])
		
        //每次抵达新区间时,更新步数作为新区间的代价f(i)
        if i == jumpPoint {
            ans++
            jumpPoint = nowMaxR
        }
    }
    
    return ans
}