LeetCode 55

链接:Jump Game

本题可以使用动态规划解决。动态规划主要有四个步骤:

  1. 使用递归回溯方法
  2. 使用备忘录方法优化(自顶向下的动态规划)
  3. 消除递归方法(自底向上的动态规划)
  4. 使用其他方法优化时间/空间复杂度

首先使用递归回溯方法。以数组[2,3,1,1,4]为例,第一个数为2,意味者可能走的步数为1和2,此时做一个循环,针对每一个index,使用回溯法,计算所有情况,到达最后一个index时,返回True。此方法时间复杂度很高,代码在本题不会通过。

    class Solution:
        def canJump(self, nums):
            return self.canJumpFromPosition(0, nums)

        def canJumpFromPosition(self, pos, nums):
            if pos == len(nums) - 1:
                return True
            furthest =  min(pos + nums[pos], len(nums) - 1)
            for next_pos in range(pos + 1, furthest + 1):
                if self.canJumpFromPosition(next_pos, nums):
                    return True
            return False

第二步,使用备忘录法优化。上一种方法遍历所有的index,事实上有些index根本无法到达最后一个index。以数组[2, 4, 2, 1, 0, 2, 0]为例,其中index 2、3和4最终无法到达终点,那么我们可以使用备忘录排除这些index的再次遍历。备忘录初始化时,最后一个index定义为True,因为自身当然能到达自身,其他index定义为None。再修改回溯算法,判断当前index是否已知,直接返回Ture和False。其他方法相同。不过这种方法仍然会超时。

    class Solution:
        def canJump(self, nums):
            memo = [None] * len(nums)
            memo[-1] = True
            return self.canJumpFromPosition(0, nums, memo)

        def canJumpFromPosition(self, pos, nums, memo):
            if memo[pos]:
                return memo[pos]

            furthest =  min(pos + nums[pos], len(nums) - 1)
            for next_pos in range(pos + 1, furthest + 1):
                if self.canJumpFromPosition(next_pos, nums, memo):
                    memo[pos] = True
                    return True

            memo[pos] = False
            return False

第三步我们要消除递归,使用自底向上的动态规划算法,从数组右侧开始遍历。很可惜,这种方法还是会超时。

    class Solution:
    def canJump(self, nums):
        memo = [None] * len(nums)
        memo[-1] = True
        for pos in range(len(nums) - 2, -1, -1):
            furthest =  min(pos + nums[pos], len(nums) - 1)
            for j in range(pos + 1, furthest + 1):
                if memo[j] == True:
                    memo[pos] = True
                    break
        return memo[0] == True

在前三步仍无法解决超时的情况下,需要进行第四步做最后的优化。观察上一步代码,对于给定的pos,实际上我们需要寻找的是后面最左侧可行的位置。如果我们持续追踪这个最左侧位置的话,我们就可以避免二次搜索。一开始设置数组最后一个值为最左侧可到达的index,从右至左遍历,对于每个位置我们检查它能否到这个index。如果可到达,那么当前位置也是可行的,最左侧可到达index会变成当前位置。依次类推,如果遍历完成后最左侧可到达index是数组第一个数时,意味者它可从第一个位置到达最后一个位置。这个思想也是贪心算法的一个体现,代码中的left_index即为局部最优解,循环完毕后产生全局最优解。

    class Solution:
        def canJump(self, nums):
            left_index = len(nums) - 1
            for pos in range(len(nums) - 1, -1, -1):
                if pos + nums[pos] >= left_index:
                    left_index = pos
            return left_index == 0

LeetCode 300

链接:Longest Increasing Subsequence

本题求的是最长递增子序列(LIS),是一道经典问题。一种最简单的求解方式是使用暴力法遍历所有的子序列,并判断它们是否是LIS。我们需要降低时间复杂度,使用动态规划思想分析。观察后发现前i项的求得的LIS不受后面数据的影响。所以,如果我们知道截至第i项的所有LIS,再求前i+1项的LIS时,就可以使用前面数据的LIS,根据第i+1项数据的情况计算LIS。以数组[10,9,2,5,3,7,101]为例,定义dp[i]为前i项LIS,可得:

  • [10],dp[1]=1
  • [10,9],9<10,dp[2]=1
  • [10,9,2],dp[3]=1
  • [10,9,2,5],2<5,dp[4]=2
  • [10,9,2,5,3],2<3,dp[5]=2
  • [10,9,2,5,3,7],2<3<7,dp[6]=3
  • [10,9,2,5,3,7,101],2<3<7<101,dp[6]=4

dp数组的最大值即为给定数组的最长递增子序列。

    class Solution:
        def lengthOfLIS(self, nums):
            if not nums:
                return 0
            dp = [1] * len(nums)
            for i in range(1, len(nums)):
                for j in range(i):
                    if nums[j] < nums[i]:
                        dp[i] = max(dp[i], dp[j] + 1)
            return max(dp)

因为使用了两层循环,所以算法的时间复杂度为O(n^2)。一种降低时间复杂度的方法是使用二分搜索算法。在这种方法中,dp数组用来存储当前的LIS。遍历时,用二分搜索算法查找当前值在数组中的位置,如果当前值小于数组的某个数组,则替换之。否则,将当前值添加进数组中。还是以上文的数组[10,9,2,5,3,7,101]为例,遍历可得:

  • [10],添加10
  • [9],9替换10
  • [2],2替换9
  • [2,5],添加5
  • [2,3],3替换5
  • [2,3,7],添加7
  • [2,3,7,101],添加101

dp数组的最终长度即为最长递增子序列的长度。有时最终的dp数组未必是最长递增子序列,但长度是相同的。在Java中,Arrays类的binarySearch()方法提供了二分搜索法,Python也有这种方法,在bisect模块。具体用法见参考资料。bisect_left方法搜索数据n在数组dp中的位置,如果n已存在,则返回n的index。

class Solution:
    def lengthOfLIS(self, nums):
        import bisect
        dp = []
        for n in nums:
            if not dp or n > dp[-1]:
                dp.append(n)
            else:
                pos = bisect.bisect_left(dp, n)
                if pos == len(dp):
                    dp.append(n)
                else:
                    dp[pos] = n
        return len(dp)

参考资料: LeetCode 55题解 LeetCode 300题解 花花酱 LeetCode 300 最长递增子序列 Python模块:bisect二分算法模块