leetcode55_跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。

示例 1:

输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

示例 2:

输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

本题只需要返回一个能或不能的结果,并不要求给出路径,因此也暗示了动态规划。通常解决并理解一个动态规划问题需要以下 步骤:
1.利用递归回溯解决问题
2.利用记忆表优化(自顶向下的动态规划)
3.移除递归的部分(自底向上的动态规划)

回溯法,其实是一个模板
1, 以当前位置为源流往下摸排所有可以跳到的位置
2, 最终递归返回源流位置
3, 然后再以下面一个位置作为源流位置,重复上述操作
回溯法的效率是很低的,以上已经说过了,本题只需要返回一个能或不能的结果,并不要求给出路径,因此回溯法的作用是让我们理解这个过程,动态规划法和回溯法的方向是相反的!!

class Solution:
    # 回溯法复杂度O(2^n),可以理解成搜索一颗树的各个路径,每个子节点上都有两条路可以走
    def canJump(self, nums: List[int]) -> bool:
        return self.canJumpfromposition(nums, 0)
    def canJumpfromposition(self, nums, position):
        # 边界
        # 当前位置如果为终点,返回true
        if position == len(nums) - 1:
            return True
        # 从当前位置能跳到的最远位置
        furtherestjump = min(nums[position]+position, len(nums)-1)
        # 从当前位置的下一个位置开始摸排
        for i in range(position+1, furtherestjump+1):
           # 以此为源流往下摸排所有可以跳到的位置
           # 最终递归返回当前位置,也就是源流
            if self.canJumpfromposition(nums, i):
                return True
            # 然后再以下面一个位置作为源流位置,重复上述操作
        # 如果当前位置能跳到的范围内都检查过了,都不能到达终点,则说明当前位置不可能到达终点
        return False

自顶向下的动态规划法,其实就是回溯法,只不过用了记忆表保存中间结果,依然递归所以np难

class Solution:
    # 回溯法复杂度O(2^n),可以理解成搜索一颗树的各个路径,每个子节点上都有两条路可以走
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        # 记录对于每个位置,是否能跳到终点
        mem = [None]*n
        mem[n-1] = True
        return self.canJumpfromposition(nums, 0, mem)
    def canJumpfromposition(self, nums, position, mem):
        # 边界
        # 当前位置如果存在记忆,则直接返回记忆的内容
        if mem[position] != None:
            return mem[position]
        # 从当前位置能跳到的最远位置
        furtherestjump = min(nums[position]+position, len(nums)-1)
        # 从当前位置的下一个位置开始摸排,直到它能跳到的最远位置
        for i in range(position+1, furtherestjump+1):
           # 以此为源流往下摸排所有可以跳到的位置
           # 最终递归返回当前位置,也就是源流
            if self.canJumpfromposition(nums, i):
                mem[position] = True
                return True
            # 然后再以下面一个位置作为源流位置,重复上述操作
        # 如果当前位置能跳到的范围内都检查过了,都不能到达终点,则说明当前位置不可能到达终点
        mem[position] = False
        return False

真正的动态规划是和回溯法方向相反的
为了探究状态转移方程,我们先在上面一种解法的基础上,简单反转一下, O(n2)
值得注意的是,我们发现反转后,对于每一个位置,它右边的位置都是有记忆内容存在的

public class Solution {
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        mem = [None]*n
        mem[n-1] = True
        # 从终点左边一位开始,
        for i in range(n-2, -1, -1):
            # 当前位置能跳到的最远位置
            furtherestjump = min(nums[i]+i, n-1)
            # 在当前位置的跳动范围内遍历
            for j in range(i+1, furtherestjump+1):
                # 如果这个跳动范围内存在能达终点的位置
	            if mem[j]:
	                # 则当前位置也是可以到达终点的
	                mem[i] = True
	                break
	        # 如果跳动范围内检查后,没有一个能到终点的位置,则当前位置也无法到终点
	        mem[i] = False
        return mem[0]

现在就容易得到我们的状态转移方程了,时间复杂度O(n)

class Solution:
    # 最右边的位置一定是可以到达终点的位置,它也是“目前最左边的一个可达终点的位置”
    # 从终点左边一位开始往前遍历每个位置,判断其是否可以达到“目前最左边的一个可达终点的位置”
    # 如果可以的话,将这个位置记录为“目前最左边的一个可达终点的位置”
    # 状态转移方程: 
    # i + nums[i] >= leftmost: 对于任意位置 i,判断它是否能到达“目前最左边的一个可达终点的位置”leftmost
    def canJump(self, nums: List[int]) -> bool:
        leftmost = len(nums) - 1
        for i in range(leftmost, -1, -1):
            if i + nums[i] >= leftmost:
                leftmost = i
        return leftmost == 0

作为参考,给出贪心法 O(n)

    # 从左边开始对于每个位置,记录历史上能达到的最远位置,初始为0
    def canJump(self, nums: List[int]) -> bool:
        furtherestindex_inhistory = 0
        n = len(nums)
        for i in range(n):
            if i > furtherestindex_inhistory:
                return False
            furtherestindex_inhistory = max(i + nums[i], furtherestindex_inhistory)
            if furtherestindex_inhistory >= n - 1:
                return True
        return furtherestindex_inhistory >= n - 1
发布了31 篇原创文章 · 获赞 18 · 访问量 5976

猜你喜欢

转载自blog.csdn.net/weixin_40027284/article/details/104803287