题目链接
https://leetcode.com/problems/jump-game-ii/
题目描述
给定非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素表示你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。(假设你总是可以到达数组的最后一个位置。)
示例
输入:[2,3,1,1,4]
输出:2
跳到最后一个位置的最小跳跃次数为2。从下标0跳到下标为1的位置,然后从下标为1的位置跳到最后一个位置。
解题思路一
动态规划算法有一部分适用的题目为“求最值”,比如:
最长回文子序列:【leetcode-Python】- Dynamic programming-516. Longest Palindromic Subsequence
编辑距离:【leetcode-Python】-Dynamic Programming-72. Edit Distance
换零钱:【leetcode-Python】-Dynamic Programming-322. Coin Change
买卖股票系列问题:【leetcode-Python】-Dynamic Programming -121. Best Time to Buy and Sell Stock、【leetcode-Python】-Dynamic Programming -122. Best Time to Buy and Sell Stock II、【leetcode-Python】-Dynamic Programming -123. Best Time to Buy and Sell Stock III
最长公共子串:【leetcode-Python】-Dynamic Programming-718. Maximum Length of Repeated Subarray
最长公共子序列:【leetcode-Python】-Dynamic Programming-1143. Longest Common Subsequence(LCS)
最长上升子序列:【leetcode-Python】-Dynamic Programming-300. Longest Increasing Subsequence(LIS)
和绝对值最大子序列:【leetcode-Python】-Dynamic Programming-1749. Maximum Absolute Sum of Any Subarray
最大和子串:【leetcode-Python】-Dynamic Programming -53. Maximum Subarray(最大和连续子数组问题)
这道求最少跳跃次数的问题,也可以用动态规划来做。动态规划的核心也是穷举,但是由于重叠子问题以及最优子结构的存在,我们可以记住子问题的最值,并依据此得到原问题的最值。由于题目要求是从索引0跳到数组最后一个位置的最少跳跃次数,因此我们可以把状态设置为当前所在的索引i,由dp[i]表示从索引i跳到数组最后一个位置的最少跳跃次数(状态可以从题目要求中来猜)。那么在索引i可选的选择为“当前跳几步”。
确定好状态和选择后,我们可以确定状态转移数组。在索引i的位置能够跳跃的最大长度为nums[i],那么从索引i可以跳到i+1,...,i+nums[i]。如果知道从索引i+1,...,i+nums[i]跳到数组最后一个位置的最少跳跃次数,dp[i]应该取这些这些跳跃次数中的最小值再加1。因此状态转移数组为dp[i] = min(dp[i+1],...,dp[i+nums[i])+1(这里需要确保索引不越界)。base case为当前就在数组的最后一格,此时不需要跳跃,即有dp[len(nums)-1] = 0。
Python实现
class Solution:
def jump(self, nums: List[int]) -> int:
dp = [float('inf') for _ in range(len(nums))] #初始化为较大的数
dp[-1] = 0
for i in range(len(nums)-2,-1,-1):#反向更新
for step in range(1,nums[i]+1):#注意step可以取到nums[i],因此range这里右边界应该写为nums[i]+1
if(i+step<len(nums)):
dp[i] = min(dp[i],dp[i+step]+1)
print(dp[i])
return dp[0]
时间复杂度与空间复杂度
时间复杂度为O(N),空间复杂度为O(N)。
解题思路二
一部分动态规划的题目可以由贪心策略来解,这道题也可以由贪心来做。每次选择只选择“当前最优“、”最有潜力的选择“。如何判断是否是当前最优呢?就看做出这个选择后,最远能够到达的位置。
比如对于[2,3,1,1,4],在索引为0的位置能够跳到索引为1的位置和索引为2的位置。在索引为1的位置能够进一步到达的最远位置为4,在索引为2的位置能够进一步到达的最远位置为3,显然如果跳到索引为1的位置,下一步就能跳得更远(跳到4)。那么做出”当前最优“的选择,即为选择”能再进一步跳得最远“的位置。
在具体实现中,我们维护在当前位置i能够跳到的最远位置,并记为边界cur_end。用cur_farthest维护在[i,...,cur_end]范围里的位置跳跃能够到达的最远位置,即在所有选择[i,...,cur_end]中能够跳到的最远位置。从左到右遍历数组,到达边界cur_end时(i==curEnd),表示触发一次跳跃,跳跃到“当前最优的选择”(即[i,...,cur_end]范围里能进一步跳得最远的那个下标),然后我们更新cur_end为cur_farthest,相当于做出了选择。
比如对于[2,3,1,1],初始化cur_end为0,cur_farthest也为0,当前跳跃次数jumps为0。(其实更准确一点来讲,jumps维护的是起跳次数)
从索引为0的位置开始考虑,cur_farthest取2,表示在0这个位置跳跃能够到达的最远距离。由于此时cur_end==i==0(初始化可选位置只有索引0),因此触发一次跳跃,增加起跳次数jumps,并将curEnd更新为cur_farthest.
继续遍历,在索引为1的位置能够到达的最远距离为4,因此更新farthest为4,在索引为2的位置能够到达的最远距离为3,farthest仍为4。此时有i==cur_end,即已经到达边界了(从0开始跳的所有可选跳跃长度都考虑完了),因此可以再触发一次跳跃,更新新的边界cur_end为farthest值,即为4(索引为1的位置能够跳到的最远位置)。同时也要对跳跃次数加1,表示从索引为1的位置起跳并继续跳跃。
不访问最后一个元素的原因:
由于题目给出假设:“一定可以到达最后一个位置”,因此在遍历到最后一个位置之前,边界cur_end一定会大于等于最后一个位置,因此我们不需要访问最后一个元素。同时如果访问最后一个位置,并且边界cur_end也等于最后一个位置,那么会再增加一次不必要的跳跃。
Python实现
class Solution:
def jump(self, nums: List[int]) -> int:
cur_end,cur_farthest = 0,0
jumps = 0
for i in range(0,len(nums)-1):
cur_farthest = max(cur_farthest,nums[i]+i)
if(i == cur_end):#到达右边界
jumps += 1#起跳,增加跳跃次数
cur_end = cur_farthest
return jumps
时间复杂度与空间复杂度
时间复杂度为O(N),空间复杂度为O(1)。
参考
https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/tan-xin-lei-xing-wen-ti/tiao-yue-you-xi