Leetcode算法——45、跳跃游戏II

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/HappyRocking/article/details/84582937

给定一个非负整数的数组,每一个元素表示从当前位置开始跳跃一次的最大长度。

你一开始站在第一个索引的位置。

你的目标是用最少的跳跃次数到达最后一个索引位置。输出跳跃次数。

备注:
假设肯定可以跳到最后一个位置。

示例:

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))

猜你喜欢

转载自blog.csdn.net/HappyRocking/article/details/84582937