leetcode 动态规划(二)

目录

279. 完全平方数(待完善)

276. 栅栏涂色

152. 乘积最大子数组

740. 删除与获得点数

121. 买卖股票的最佳时机

337. 打家劫舍 III

416. 分割等和子集

322. 零钱兑换

1027. 最长等差数列


279. 完全平方数(待完善)

https://leetcode-cn.com/problems/perfect-squares/

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:输入: n = 12,输出: 3 ,解释: 12 = 4 + 4 + 4.
示例 2:输入: n = 13,输出: 2,解释: 13 = 4 + 9.

题解

 一:动态规划, 时间复杂度O(n^2),用dp[i]表示数字i被拆成的最少的完全平方数的个数。

class Solution(object):
    def numSquares(self, n):
        """
        :type n: int
        :rtype: int
        """
        dp = [i for i in xrange(n + 1)]

        for i in xrange(2, n + 1):
            for j in xrange(i):
                if j * j == i:
                    dp[i] = 1
                    break
                else:
                    dp[i] = min(dp[i], dp[j] + dp[i - j])
        return dp[-1] 

 二:动态规划, 时间复杂度O(n\sqrt{n}),用dp[i]表示数字i被拆成的最少的完全平方数的个数,不过优化了一下,我们内层循环不再遍历i前的所有数字,而是只遍历那些完全平方数,因为只有完全平方数才能减少个数。

class Solution(object):
    def numSquares(self, n):
        res = [i for i in range(n + 1)]

        for i in range(3, n + 1):
            j = 0 
            while i - j * j >= 0:
                res[i] = min(res[i - j * j] + 1, res[i])
                j += 1
        return res[-1]

三:mark一下,官方题解https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode/,等刷一部分递归再来。

276. 栅栏涂色

https://leetcode-cn.com/problems/paint-fence/

有 k 种颜色的涂料和一个包含 n 个栅栏柱的栅栏,每个栅栏柱可以用其中一种颜色进行上色。你需要给所有栅栏柱上色,并且保证其中相邻的栅栏柱 最多连续两个 颜色相同。然后,返回所有有效涂色的方案数。

注意:n 和 k 均为非负的整数。

示例:输入: n = 3,k = 2,输出: 6,解析: 用 c1 表示颜色 1,c2 表示颜色 2,所有可能的涂色方案有:

题解

一:递推公式,考虑i下标的栅栏,那么只有两种情况:

  1. 下标i的栅栏与前面的栅栏不同色,那么前面的栅栏没限制,目前的栅栏有k-1种选择
  2. 下标i的栅栏与前面的栅栏同色,那么前面的栅栏有限制(不能与他前面的同色),因为之前的都定下了,故该下标与前面同色,只有一种选择。

此处用same来记录一个栅栏与前面栅栏同色的方法数,diff来记录一个栅栏与前面栅栏不同色的方法数。

class Solution(object):
    def numWays(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: int
        """
        if n <= 0 or k <= 0 or ( k == 1 and n > 2):
            return 0
        if n == 1:
            return k
        diff, same = [0] * (n + 1), [0] * (n + 1)
        diff[1], same[1] = k, 0

        for i in range(2, n + 1):
            same[i] = diff[i - 1]
            diff[i] = (diff[i - 1] + same[i - 1]) * (k - 1)
        return same[-1] + diff[-1]

152. 乘积最大子数组

https://leetcode-cn.com/problems/maximum-product-subarray/

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

示例 1:输入: [2,3,-2,4],输出: 6,解释: 子数组 [2,3] 有最大乘积 6。
示例 2:输入: [-2,0,-1],输出: 0,解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

题解

一:这一题乍一看与最大子序和差不多,不过有一点不一样。求和的时候只要前面的求和大于等于0,该值加上去,就是对该值的加强,但是乘积不一样,当该值是正值时,自然前面是正值乘上会最大,如果该值是负值,最大的反而不是和前面最大的一起得到的,此时期待前面是负值,这样才会最大,当然0要单独讨论,因为任何子序只要有0,值必为0。start记录的是非0元素的下标,即一段开始的位置,product记录的是这段的乘积和,若product>0,看情况更新res,若是负值,我们则证明这一段有一个多处的负值,我们把这一段的第一个负值和他前面的均剔除掉,则必定乘积是正值,看情况更新res。

class Solution(object):
    def maxProduct(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0
        res = max(nums)
        start, neg, product = 0, 1, 1
        for i in xrange(len(nums)):
            if nums[i] == 0:
                start = i + 1
                product = 1
            else:
                product *= nums[i]
                if product > 0:
                    res = max(res, product)
                if product < 0:
                    j, pos_product = start, product 
                    while j < i and nums[j] > 0:
                        pos_product /= nums[j]
                        j += 1
                    if j != i:
                        pos_product /= nums[j]
                    re

二:动态规划,转自leetcode官方题解,https://leetcode-cn.com/problems/maximum-product-subarray/solution/cheng-ji-zui-da-zi-shu-zu-by-leetcode-solution/

class Solution(object):
    def maxProduct(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0
        
        local_min, local_max, res = nums[0], nums[0], nums[0]

        for i in xrange(1, len(nums)):
            tmp_min = min([local_min * nums[i], local_max * nums[i], nums[i]])
            local_max = max([local_max * nums[i], local_min * nums[i], nums[i]])
            local_min = tmp_min
            res = max(local_max, res)

        return res

740. 删除与获得点数

https://leetcode-cn.com/problems/delete-and-earn/

给定一个整数数组 nums ,你可以对它进行一些操作。每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除每个等于 nums[i] - 1 或 nums[i] + 1 的元素。开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:输入: nums = [3, 4, 2],输出: 6,解释: 删除 4 来获得 4 个点数,因此 3 也被删除。之后,删除 2 来获得 2 个点数。总共获得 6 个点数。
示例 2:输入: nums = [2, 2, 3, 3, 3, 4],输出: 9,解释: 删除 3 来获得 3 个点数,接着要删除两个 2 和 4 。之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。总共获得 9 个点数。
注意:nums的长度最大为20000。每个整数nums[i]的大小都在[1, 10000]范围内。

题解

一:rec[i]记录每个值i出现了几次,dp[i]表示截止i,目前能获得的最大的点数,那么对rec进行动态规划,其实就是打家劫舍的一种变种。考虑是否选择添加点数i时,综合比较添加i-1,和i-2以及i的值。

class Solution(object):
    def deleteAndEarn(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        rec = [0] * 10001
        dp = [0] * 10001
        for num in nums:
            rec[num] += 1

        dp[1] = rec[1] * 1
        for i in xrange(2, len(rec)):
            dp[i] = max(dp[i - 2] + rec[i] * i, dp[i - 1])   
        return dp[-1]

二:其实和思路一一样,只不过借用了python的结构,其中avoid表示不添加当前点数,using表示添加当前的点数。

class Solution(object):
    def deleteAndEarn(self, nums):
        count = collections.Counter(nums)
        prev = None
        avoid = using = 0
        for k in sorted(count):
            if k - 1 != prev:
                avoid, using = max(avoid, using), k * count[k] + max(avoid, using)
            else:
                avoid, using = max(avoid, using), k * count[k] + avoid
            prev = k
        return max(avoid, using)

三:把pythonic改写一下,其实用的是未变化的avoid

avoid, using = max(avoid, using), k * count[k] + max(avoid, using)

相当于,如下:

class Solution(object):
    def deleteAndEarn(self, nums):
        count = collections.Counter(nums)
        prev = None
        avoid = using = 0
        for k in sorted(count):
            if k - 1 != prev:
                tmp = max(avoid, using)
                using = k * count[k] + max(avoid, using)
                avoid = tmp
            else:
                tmp = max(avoid, using)
                using = k * count[k] + avoid
                avoid = tmp
            prev = k
        return max(avoid, using)

121. 买卖股票的最佳时机

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

题解

一:其实目前的最大值cur_max减去他前面的最小值cur_min中的最大就是解。因为必须先买再卖,所以如果当前值比最小值还小,就得看他后面的峰值,此时cur_max必须更新成现在的值,因为卖出必须先买入。

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        if not prices:
            return 0
        res, cur_min, cur_max = 0, prices[0], prices[0]

        for i in xrange(len(prices)):
            if prices[i] > cur_max:
                cur_max = prices[i]
                res = max(cur_max - cur_min, res)
            elif prices[i] < cur_min:
                cur_min, cur_max = prices[i], prices[i]
        return res

337. 打家劫舍 III

https://leetcode-cn.com/problems/house-robber-iii/

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

题解

一:打家劫舍的1和2,可以用一个列表来表示截止目前房子可盗取的最高金额。

  • 当不盗取当前屋子可以直接用截止上一个屋子盗取的最高金额。由于前两题房子可以用下标表示,而树结构不便于用下标表示,这边可以用字典表示。这一题在这种情况下也很好解决,因为可以直接拿到截止子节点盗取的最高金额。
  • 当盗取当前屋子,那么就是当前屋子加上截止上上一个屋子能盗取的最高金额,这时候用列表很好找到。但是本题不好找,因为找到孙子节点还是要费点事的。我们可以将截止孙节点的最高金额记到其父节点(当前节点的子节点)上。那么每个节点及两种状态,不取当前节点,则最高金额是其子节点(子节点可取可不取)的的最高金额。取当前节点,则最高金额是不取子节点的金额加上当前节点的金额。

字典rec的记当前节点的两种状态,下标0表示不取当前节点,则对子节点无限制,可取子节点两种状态的最大值。下标1表示取当前节点,则对子节点有限制(不能取子节点)。

class Solution(object):
    def __init__(self):
        self.rec = {}

    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        if not root:
            return 0
        self.helper(root)
        return max(self.rec[root])

    def helper(self, root):
        if not root:
            return [0, 0]
        if root in self.rec:
            return self.rec[root]

        left_child = self.helper(root.left)
        right_child = self.helper(root.right)

        self.rec[root] = [0, 0]
        self.rec[root][0] = (max(left_child[0], left_child[1]) 
                           + max(right_child[0], right_child[1]))
        self.rec[root][1] = left_child[0] + right_child[0] + root.val
        return self.rec[root]

416. 分割等和子集

https://leetcode-cn.com/problems/partition-equal-subset-sum/

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:每个数组中的元素不会超过 100。数组的大小不会超过 200
示例 1:输入: [1, 5, 11, 5],输出: true。解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:输入: [1, 2, 3, 5],输出: false。解释: 数组不能分割成两个元素和相等的子集.

题解

一:首先列表元素小于2的一定不符合,列表元素和不为偶数的也是False。剩下的我们可以完全当作0-1背包的问题,即是容量为target的背包,用nums中的元素(体积),能达到的最大价值(每个元素的价值是其本身)是多少。最后我们只要看这个最大价值是不是target。

class Solution(object):
    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        n = len(nums)
        if n == 0:
            return True
        if n == 1:
            return False
        target = sum(nums)
        if target % 2:
            return False 
        target //= 2

        dp = [0] * (target + 1)

        for i in range(n):
            for j in range(target, -1, -1):
                if j >= nums[i]:
                    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
            if dp[-1] == target:
                return True
        return False

法二:典型的背包问题,在n个物品中选出一定物品,填满target的背包,F(n,C)考虑将n个物品填满容量为C的背包。F(i,c) = F(i - 1, c) || F(i-1, c-w(i))。第一种情况,我们先看i-1个物品能否填满c的空间,如果能,那么多一种选择也能;第二种情况,我们考虑前i-1个物品能否填满c-w(i)的空间,若能,则放入第i个物品即可填满c的空间。

class Solution(object):
    def __init__(self):
        self.rec = None

    def helper(self, capacity, idx, nums):
        if capacity == 0:
            return True
        if idx < 0 or capacity < 0:
            return False  
        if self.rec[idx][capacity] != -1:
            return self.rec[idx][capacity]

        res = (self.helper(capacity, idx - 1, nums) 
               or self.helper(capacity - nums[idx], idx - 1, nums))
        self.rec[idx][capacity] = res 
        return res

    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        n = len(nums)
        if n == 0:
            return True
        if n == 1:
            return False
        target = sum(nums)
        if target % 2:
            return False 
        target //= 2

        self.rec = [[-1] * (target + 1) for _ in range(n)]
        
        return self.helper(target, n - 1, nums) == 1

法三:法二的动态规划版本。

class Solution(object):
    def canPartition(self, nums):
        n = len(nums)
        if n == 0:
            return True
        if n == 1:
            return False
        target = sum(nums)
        if target % 2:
            return False 
        target //= 2

        rec = [False] * (target + 1) 
        
        for c in range(target + 1):
            rec[c] = (c == nums[0])

        for i in range(1, n):
            for c in range(target, nums[i] - 1, -1):
                rec[c] = (rec[c] or rec[c - nums[i]])
        return rec[-1]

322. 零钱兑换

https://leetcode-cn.com/problems/coin-change/submissions/

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:输入: coins = [1, 2, 5], amount = 11。输出: 3 。解释: 11 = 5 + 5 + 1
示例 2:输入: coins = [2], amount = 3。输出: -1
说明:你可以认为每种硬币的数量是无限的。

题解

一:动态规划。这种做法和硬币只能用一次并没有太多的区别。

class Solution(object):
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """
        n = len(coins)
        if amount == 0:
            return 0
        if amount < 0 or n == 0:
            return -1 
        dp = [[amount + 1] * (amount + 1) for _ in range(n)]
        dp[0][0] = 0

        for j in range(amount + 1):
            if j % coins[0] == 0:
                dp[0][j] = j // coins[0]
        
        for i in range(1, n):
            dp[i][0] = 0
            for j in range(amount + 1):
                dp[i][j] = dp[i - 1][j]
                cnt = j // coins[i]
                for k in range(cnt + 1):
                    dp[i][j] = min(min(dp[i - 1][j],dp[i][j]),
                                   dp[i - 1][j - k * coins[i]] + k)                
        return dp[-1][-1] if dp[-1][-1] != (amount + 1) else -1

二:对一的空间优化,顺带也稍微优化了一下时间。注意这种做法与只能取一次的区别,即0-1背包问题。区别在于,这边的内层循环是从左往右的,即由小到大(而不是0-1背包的由大到小)

0-1背包内层循环从右往左是因为,真正的递推公式是dp[i][j] = min(dp[i - 1][j], dp[i-1][j-coins[i] + 1),即每次用到的都是截止上一个硬币的值,当作空间优化后,只有一行,我们从后面往前更新,可以理解为每次用到的都是未更新前的值,也即截止上一个硬币的相关值。参见https://blog.csdn.net/qq_xuanshuang/article/details/104025707

本题:硬币可以无限次使用,当作了空间优化后,同样只有一行,当时我们希望每次的值都是用的截止当前硬币的值,因为在本次用该硬币时,之前可能用过相同值的硬币,即我们期待拿到的是更新后的值,这种适合从左往右更新。

class Solution(object):
    def coinChange(self, coins, amount):
        n = len(coins)
        if amount == 0:
            return 0
        if amount < 0 or n == 0:
            return -1 

        dp = [amount + 1] * (amount + 1)
        dp[0] = 0

        for j in range(amount + 1):
            if j % coins[0] == 0:
                dp[j] = j // coins[0]

        for i in range(1, n):
            for j in range(coins[i], amount + 1):
                dp[j] = min(dp[j], dp[j - coins[i]] + 1)

        return -1 if dp[-1] == (amount + 1) else dp[-1]

1027. 最长等差数列

给定一个整数数组 A,返回 A 中最长等差子序列的长度。回想一下,A 的子序列是列表 A[i_1], A[i_2], ..., A[i_k] 其中 0 <= i_1 < i_2 < ... < i_k <= A.length - 1。并且如果 B[i+1] - B[i]( 0 <= i < B.length - 1) 的值都相同,那么序列 B 是等差的。

提示:2 <= A.length <= 2000。0 <= A[i] <= 10000

题解

一:这题和873. 最长的斐波那契子序列的长度https://leetcode-cn.com/problems/length-of-longest-fibonacci-subsequence/),https://blog.csdn.net/qq_xuanshuang/article/details/105023133一样。

from collections import defaultdict
class Solution(object):
    def longestArithSeqLength(self, A):
        """
        :type A: List[int]
        :rtype: int
        """
        index = {}
        for i in range(len(A)):
            if A[i] not in index:
                index[A[i]] = set()
            index[A[i]].add(i)
        longest = defaultdict(lambda: 2)
        res = max(collections.Counter(A).values())

        for i in range(len(A)):
            for j in range(i):
                delta = A[i] - A[j]
                idx_set = index.get(A[j] - delta)
                if idx_set is None:
                    continue
                for idx in idx_set:
                    if idx is not None and idx < j:
                        longest[j, i] = max(longest[j, i], longest[idx, j] + 1)
                        res = max(res, longest[j, i])
        return max(res, min(2, len(A)))

 二:也可以先确定,等差数列的最后两个数字,这样他前面的数字也出现了,类似斐波那契题目一样,这边也涉及到两个数之间的关系,所以用二维数组,字典表示longest[i,j]表示以i和j结尾的最长的等差数列的长度。

from collections import defaultdict
class Solution(object):
    def longestArithSeqLength(self, A):
        res = 2
        rec = defaultdict(int)
        longest = defaultdict(lambda: 2)

        for i in range(len(A)):
            for j in range(i + 1, len(A)):
                target = 2 * A[i] - A[j]
                if target in rec:
                    longest[i, j] = longest[rec[target], i] + 1
                    res = max(res, longest[i,j])
            rec[A[i]] = i
        return max(res, min(2, len(A)))

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值