目录
55. 跳跃游戏
问题描述
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 3 * 10^4
0 <= nums[i] <= 10^5
解法1:动态规划(递归+memoization)
首先求状态转移方程。
站在当前位置k,所能做的选择有nums[k]中,分别为向前跳跃1,2,...,nums[k]步。如果其中任何一种选择能够导致到达最后一个下标则表示从当前位置k可以到达最后一个下标。
这个问题用深度优先搜索同样可以解决,但是用动态规划方法(为了方便对比,考虑和深度优先搜索两者都采用递归的方式实现)的话可以利用重叠子结构的特性,即可以利用memoization技巧来提高效率。
其次确定baseline case。如果到达了某个位置其值为0的话就陷入了坑里,到达不了终点了。如果当前位置就是终点,就表示可以到达。当然如果位置值越界了也表示到达不了。
class Solution:
def canJump(self, nums: List[int]) -> bool:
if len(nums)==1:
return True
memo = set() # Only record False
def dp(k):
if k in memo:
return False
# k: start position
if k==len(nums)-1:
return True
if nums[k]==0:
return False
for j in range(1,nums[k]+1):
if k+j<=len(nums)-1:
if dp(k+j):
return True
memo.add(k)
return False
return dp(0)
居然超出时间限制。。。好没面子。
解法2:动态规划(迭代)
考虑从后向前以迭代的方式进行遍历。如果一个节点能到达最终节点就标记为True,否则就标记为False。
一个节点它所能跳跃范围内的节点全部为False,它就也是False;否则就是True。
class Solution:
def canJump(self, nums: List[int]) -> bool:
if len(nums)==1:
return True
flags = len(nums)*[False]
flags[-1] = True
for k in range(len(nums)-2,-1,-1):
for j in range(1,nums[k]+1):
if flags[j+k]:
flags[k] = True
break
return flags[0]
执行用时:4196 ms, 在所有 Python3 提交中击败了5.02%的用户
内存消耗:16 MB, 在所有 Python3 提交中击败了23.92%的用户
勉强过关。问题出在哪儿?
解法3:贪心算法
设想一下,对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x,它本身可以到达,并且它跳跃的最大长度为 ,这个值大于等于 y,即
,那么位置 y 也可以到达。换句话说,对于每一个可以到达的位置 x,它使得
这些连续的位置都可以到达。
从左向右遍历数组中的每一个位置,并实时维护最远可以到达的位置。对于当前遍历到的位置 x,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 更新 最远可以到达的位置。
在遍历的过程中(不一定要等到遍历完!),如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True 作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False 作为答案。
class Solution:
def canJump(self, nums) :
if len(nums)==1:
return True
farthest = 0 # Represent the reachable greatest distance
for i, value in enumerate(nums):
if farthest>=i and i+value>farthest: # If the current pos is reachable, and can reach more distant place
farthest = i+value # Update the reachable greatest distance
if farthest>=len(nums)-1:
return True
return False
执行用时:124 ms, 在所有 Python3 提交中击败了23.01%的用户
内存消耗:16.2 MB, 在所有 Python3 提交中击败了5.09%的用户
还是很差。参考了一个评论区的题解,不在中途进行判断,总是到遍历完才进行判断,性能反而大幅度提升。感觉不是很理解了。。。
class Solution:
def canJump(self, nums) :
#if len(nums)==1:
# return True
farthest = 0 # Represent the reachable greatest distance
for i, value in enumerate(nums):
if farthest>=i and i+value>farthest: # If the current pos is reachable, and can reach more distant place
farthest = i+value # Update the reachable greatest distance
#if farthest>=len(nums)-1:
#return True
return farthest>=len(nums)-1
执行用时:64 ms, 在所有 Python3 提交中击败了98.40%的用户
内存消耗:16 MB, 在所有 Python3 提交中击败了17.47%的用户
45. 跳跃游戏 II
问题描述
给你一个非负整数数组 nums
,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是2。从下标为 0 跳到下标为 1 的位置,跳1步,然后跳3步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4] 输出: 2
提示:
1 <= nums.length <= 10^4
0 <= nums[i] <= 1000
方法一:广度优先搜索
思路与算法1
上一题是考虑可达性,本题是在保证可达性的前提下,求解最小跳跃次数。
既然是最短路径问题,第一感是广度优先搜索。
从某点出发,它所能到达的点都属于它的邻节点。由此展开广度优先搜索。
代码
class Solution:
def jump(self, nums: List[int]) -> int:
visited = set([0])
q = deque([(0,0)])
while len(q)>0:
node,layer = q.popleft()
if node == len(nums)-1:
return layer
for k in range(1,nums[node]+1):
if k+node not in visited and k+node <= len(nums)-1:
q.append((k+node,layer+1))
visited.add(k+node)
执行用时:1824 ms, 在所有 Python3 提交中击败了14.85%的用户
内存消耗:16.7 MB, 在所有 Python3 提交中击败了5.16%的用户
方法二:动态规划(递归+memoization)
思路与算法
记从节点k出发到达终点所需要的最少次数为,从该点出发跳一次能覆盖的范围由nums[k]决定,可以得到状态转移方程如下所示:
基线情况(baseline case):k>=nums.length-1时,表示已经到达,返回0
代码
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums)==1:
return 0
memo = dict()
def dp(k):
if k in memo:
return memo[k]
if k>=len(nums)-1:
memo[k] = 0
return 0
minjump = float('inf')
for j in range(1,nums[k]+1):
minjump = min(minjump,dp(k+j))
memo[k] = minjump+1
return minjump+1
return dp(0)
执行用时:8056 ms, 在所有 Python3 提交中击败了5.43%的用户
内存消耗:33.8 MB, 在所有 Python3 提交中击败了5.16%的用户
惨胜如败。。。
方法三:动态规划(反向迭代)
思路与算法
考虑反向的遍历。
终点的距离为0;从终点出发,凡是一次跳跃的覆盖范围涵盖终点的就将距离标记为1。
从某个节点出发它的覆盖范围包含的节点的最短距离加一记为该节点的距离。
由此倒推直到节点0。
其实这里面也有广度优先搜索的影子。
代码
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums)==1:
return 0
dist = len(nums)*[len(nums)]
dist[-1] = 0
for k in range(len(nums)-1,-1,-1):
for j in range(1,nums[k]+1):
if k+j<len(nums):
dist[k] = min(dist[k], 1+dist[k+j])
#print(dist)
return dist[0]
执行用时:8156 ms, 在所有 Python3 提交中击败了5.31%的用户
内存消耗:16.1 MB, 在所有 Python3 提交中击败了5.16%的用户
依然惨淡。。。
方法四:动态规划(正向迭代)
思路与算法
从节点0出发,它能够到达的范围内的节点都将距离标记为1。
在某个节点,将其覆盖范围内的所有节点都标记为它自己的距离值加1,但是与它已经被标记的值做比较决定是否更新。
同样,这里面也有广度优先搜索的影子。
代码
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums)==1:
return 0
dist = len(nums)*[len(nums)]
dist[0] = 0
for k in range(0,len(nums)):
for j in range(1,nums[k]+1):
if k+j<len(nums):
dist[k+j] = min(dist[k+j], 1+dist[k])
#print(dist)
return dist[-1]
执行用时:8388 ms, 在所有 Python3 提交中击败了5.04%的用户
内存消耗:15.9 MB, 在所有 Python3 提交中击败了20.68%的用户
方法五:贪心算法(官解)
这道题是典型的贪心算法,通过局部最优解得到全局最优解。以下两种方法都是使用贪心算法实现,只是贪心的策略不同。
如果我们「贪心」地进行正向查找,每次找到可到达的最远位置,就可以在线性时间内得到最少的跳跃次数。思路描述略(疲了^-^),直接看代码。
class Solution:
def jump(self, nums: List[int]) -> int:
n = len(nums)
maxPos, end, step = 0, 0, 0
for i in range(n - 1):
if maxPos >= i:
maxPos = max(maxPos, i + nums[i])
if i == end:
end = maxPos
step += 1
return step
执行用时:52 ms, 在所有 Python3 提交中击败了68.70%的用户
内存消耗:15.8 MB, 在所有 Python3 提交中击败了47.07%的用户
没有理解的是,如何证明在这里贪心算法能够确保最优解呢?什么情况下可以用贪心算法,什么情况下贪心算法不能确保最优解呢?