LeetCode 198

链接:House Robber

本题使用动态规划解决。首先需要分析题目的递归关系:当强盗到达第i间房屋时,他可以选择抢,也可以选择不抢。如果抢劫这间房屋,根据题目要求,他必定没有抢劫第i-1间房屋,但他可能抢劫第i-2间房屋。如果不抢劫第i间房屋,那么他可能抢劫第i-1间房屋。这两个选择造成抢劫得来金钱的不同。设rob(i)为前i间房屋抢劫得来金钱的最大值,第一种需要计算前i-2间房屋抢劫金钱的最大值加上第i间房屋抢劫的金钱。第二种只需要计算前i-1间房屋抢劫金钱的最大值。两者需取其中最大值,由此得出递归关系:

rob(i) = max(rob(i - 2) + curr, rob(i - 1)) # curr代表当前第i间房屋抢劫的金钱

得出递归关系后可直接使用递归方法,不过普通的递归方法用在本题会超时:

    class Solution:
        def rob(self, nums):
            return self.rob_house(nums, len(nums) - 1)

        def rob_house(self, nums, i):
            if i < 0:
                return 0
            return max(self.rob_house(nums, i - 2) + nums[i], self.rob_house(nums, i - 1))

继续优化算法,采用备忘录法,备忘录法是自顶向下的算法,将求出的值保存在备忘录中,避免普通递归方法中的重复计算。定义一个值均为-1的数组,每次计算得出的结果存入数组中,如果某次递归发现memo[i]的值不为-1,说明已经保存之前计算的结果,直接返回memo[i],无需再次计算。

    class Solution:
        def rob(self, nums):
            memo = [-1 for _ in range(len(nums))]
            return self.rob_house(nums, len(nums) - 1, memo)

        def rob_house(self, nums, i, memo):
            if i < 0:
                return 0
            if memo[i] != -1:
                return memo[i]
            memo[i] = max(self.rob_house(nums, i - 2, memo) + nums[i], self.rob_house(nums, i - 1, memo))
            return memo[i]

自底向上的动态规划,使用迭代方法。此方法和上一种方法均击败了94.83%的python3提交代码。

    class Solution:
        def rob(self, nums):
            if not nums:
                return 0
            memo = [-1 for _ in range(len(nums) + 1)]
            memo[0] = 0
            memo[1] = nums[0]
            for i in range(1, len(nums)):
                memo[i + 1] = max(memo[i - 1] + nums[i], memo[i])
            return memo[-1]

在上一种方法中,实际参与循环的只有memo[i]和memo[i-1]两项,所以该方法的空间可以进一步的压缩,使用两个变量来代替。其中prev2代表递归式中的nums[i-2],prev1代表nums[i-1],每次循环将prev2的值修改为上一次循环prev1的值,更新后prev1的值则类似于递归关系中的rob(i)。

    class Solution:
        def rob(self, nums):
            prev2 = prev1 = 0
            for n in nums:
                prev2, prev1 = prev1, max(n + prev2, prev1)
            return prev1

LeetCode 494

链接:Target Sum

本题可使用搜索或者动态规划。前者即使用DFS暴力搜索,计算所有可能的和差情况。另外使用备忘录方法可以减少时间复杂度。这里主要介绍动态规划。

我们定义Sum(i)为前i项数字所有可能的和差情况,则Sum(i-1)为前i-1项的和差情况,那么可以推出Sum(i-1)加nums[i]和减nums[i]分别是两种情况,即可得出递推关系

Sum(i) = (Sum(i-1) + nums[i]) + (Sum(i-1) - nums[i])

这里的Sum(i)可以用二维数组dp[i][j]来表示,i表示nums中的第i项数字,i等于0时不对应nums中的数字。j表示和差的结果,数组的值代表对于前i个数所能达到各个结果的次数。那么二维数组所需的空间又是多少呢?i的空间经上文分析可得为nums中数字的长度加1,j的空间我们来举例说明,假如数组为[1,1,1,1,1],那么它的最大和为5,最小和为-5,空间为11,即是两倍的最大和加一。下面的视频推荐观看,作者解释的比较清晰。

以下为AC代码:

    class Solution:
        def findTargetSumWays(self, nums, S):
            from functools import reduce
            sum = reduce(lambda x, y: x+y, nums)
            if sum < S:
                return 0
            dp = [[0] * (sum * 2 + 1) for _ in range(len(nums) + 1)]
            dp[0][sum] = 1
            for i in range(len(nums)):
                for j in range(nums[i], 2 * sum + 1 - nums[i]):
                    if dp[i][j]:
                        dp[i + 1][j + nums[i]] += dp[i][j]
                        dp[i + 1][j - nums[i]] += dp[i][j]
            return dp[len(nums)][S + sum]

由于数组遍历中i+1只和i有关系,所以我们可以将二维数组降为一维。在i循环时建立一个新数组next,j循环时用next数组记录结果,结束后将next的值复制给dp。

class Solution:
    def findTargetSumWays(self, nums, S):
        from functools import reduce
        sum = reduce(lambda x, y: x+y, nums)
        if sum < S:
            return 0
        dp = [0 for _ in range(sum * 2 + 1)]
        dp[sum] = 1
        for i in range(len(nums)):
            next = [0 for _ in range(sum * 2 + 1)]
            for j in range(nums[i], 2 * sum + 1 - nums[i]):
                if dp[j]:
                    next[j + nums[i]] += dp[j]
                    next[j - nums[i]] += dp[j]
            dp = next
        return dp[S + sum]

参考资料: 算法-动态规划 Dynamic Programming–从菜鸟到老鸟 LeetCode 494题解 花花酱 LeetCode 494. Target Sum