给定一个非负整数的数组,每一个元素表示从当前位置开始跳跃一次的最大长度。
你一开始站在第一个索引的位置。
你的目标是用最少的跳跃次数到达最后一个索引位置。输出跳跃次数。
备注:
假设肯定可以跳到最后一个位置。
示例:
Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
Jump 1 step from index 0 to 1, then 3 steps to the last index.
思路
1、从后向前贪婪法
方法如下:
- 从后往前遍历一遍,找到可以跳到倒数第一位置的最左边的索引,步数+1.
- 然后将这个索引当做跳跃的目标地点,记为end,从end开始向前遍历一遍,找到可以跳到end的最左边的索引,步数+1.
- 重复这个过程,直至end==0为止,整个过程相当于从最后一位跳跃到了第一位。
比如输入数组为 nums=[1,4,2,3,2,1,1],那么:
1、从最末尾的 nums[6] 开始向左,查询可以跳跃到 nums[6] 的最左边的位置,为 nums[3]=3。
2、从 nums[3] 开始向左,查询可以跳跃到 nums[3] 的最左边的位置,为 nums[1]=4。
3、从 nums[1] 开始向左,查询可以跳跃到 nums[1] 的最左边的位置,为 nums[0]。
4、已经到了起始索引,算法结束,解为 3 步。
虽然是贪婪法,但是可以证明能够达到全局最优解(之一)。
反证法:
假设存在一种步数更少解法B,只需要跳 m-1 步即可。(m 为使用贪婪法的解,贪婪法解法记为A)
那么解法B必定至少存在一次跳跃,使得这次跳跃会至少横跨2个解法A的落脚点,否则不可能会比解法A的步数少。
将这次跳跃的起跳和落脚位置记为 b1 和 b2,包含的2个解法A的落脚点记为 a1 和 a2,有 b1 <= a1 < a2 <= b2,且两个等号不会同时成立。不妨假设第一个等号不成立,即 b1 < a1 < a2 <= b2。
那么既然从 b1 可以直接跳到 b2,则自然可以从 b1 跳到 a2,但是根据解法A的规则,a1 是可以跳跃到 a2 的最左边的位置,这与 b1 < a1 矛盾,因此原假设不成立,命题得证。
时间复杂度为O(n^2)。
2、贪婪法改进版
上述方法每次更新end,都要重新遍历一遍end之前的所有元素,越靠前的元素,越容易被多次遍历,浪费时间。
可以提前构造一个数组,里面存放着可以跳跃到当前位置的最左边的元素索引。
这样,再按照上述方法的思想,从后向前寻找,每次寻找可以跳到end的最左边索引时,直接从数组中取即可。
而构造这样一个数组,只需要遍历一遍即可:从后向前遍历,对于每个位置,从当前位置开始、到当前位置可以跳跃到的最远距离结束,这之间的所有的位置,在新数组中都更新成当前位置的索引值。由于是从后向前遍历,因此新数组上的同一索引的数只有可能被更小的数替代。
时间复杂度降为 O(n) + O(n) = O(n)。
3、动态规划
从前向后跳跃,只需要一次扫描即可,从左向右扫描。
维持4个变量:
- i:从左向右扫描的指针,初始化为0
- end:当前位置可以跳跃到的最远距离扫描的指针,初始化为0
- farthest:从end之前的所有位置出发,可以跳跃到的最远距离,初始化为0
- step:步数,初始化为0
原理:
- 从首位置开始,比如nums[0] = 5,那么第一步可以跳到 1~5 的任一位置,记 end = 5。
- 那么跳到哪个位置最好呢?如果 1-5 的某个位置可以保证从这个位置出发再跳跃一次可以达到最远,则这个位置是最好的。
- 因此扫描1-5这5个位置,每扫描到一个,就记录下这个位置可以到达的最远位置,选择最大值,记为 farthest,可以跳跃到 farthest 的位置记为 j。
- 然后当 i 扫描到 end 时,step+1,同时 end=farthest,表示刚才的一步已经从0跳跃到了 j,但是不用记录j的值,只需要步数正确即可。
- 接下来继续扫描,从当前位置到end之间,更新 farthest。
- 循环这个过程,直到 end 超过了倒数第一的位置,算法结束,返回 step 数。
比如输入数组为 nums=[3,2,3,2,1,2,1],那么:
1、从 nums[0] 开始,因为 nums[0]=3,所以可以跳跃到 nums[3],记 farthest=3,end=3,step=1。
2、扫描 nums[1] ~ nums[3],从这3个位置出发可以跳跃到的最远的位置分别为 nums[3]、nums[5]、nums[5],选择最远位置,farthest = 5。更新 end=5,step=2。
3、扫描 nums[4] ~ nums[5],从这2个位置出发可以跳跃到的最远的位置分别为 nums[5]、nums[7],选择最远位置,farthest = 7。更新 end=7,step=3。
4、end已经越界,算法结束,step=3。
由于只扫描一遍,算法复杂度为O(n)。
python实现
def jump(nums):
"""
:type nums: List[int]
:rtype: int
从后往前贪婪法。
"""
# 初始化end
end = len(nums)-1
step = 0
# 开始从后向前跳跃
while(end > 0):
left = end
for i in range(end - 1, -1, -1):
if nums[i] >= end - i:
left = min(left, i)
end = left
step += 1
return step
def jump2(nums):
"""
:type nums: List[int]
:rtype: int
改良版。
"""
# 构造数组
l = len(nums)
left_list = [-1] * l
for i in range(l-2, -1, -1):
# 从当前位置向后长度为nums[i]之中的每个位置,都可以由当前位置直接跳跃到
# 由于是从后向前遍历,因此left_list上的同一索引的数只有可能被更小的数替代
left_list[i+1:i+1+nums[i]] = [i] * nums[i]
# 初始化end
end = len(nums)-1
step = 0
# 开始从后向前跳跃
while(end > 0):
end = left_list[end]
step += 1
return step
def jump3(nums):
"""
:type nums: List[int]
:rtype: int
动态规划。
"""
l = len(nums)
if l <= 1:
return 0
end = 0
farthest = 0
step = 0
for i in range(l):
farthest = max(farthest, i + nums[i])
if i == end:
step += 1
end = farthest
if end >= l-1:
break
return step
if '__main__' == __name__:
nums = [2,3,1,1,4]
print(jump3(nums))