leetcode(4)背包系列题目


背包递推公式

  1. 问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

    如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

    class Solution:
        def canPartition(self, nums: List[int]) -> bool:
            target = sum(nums)
            if target % 2 == 1:return False
            target //= 2
            # 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
            # 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
            dp = [0] * 10001
            for i in range(len(nums)):
                for j in range(target, nums[i] - 1, -1):
                    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
            return dp[target] == target
    

    dp[target]里是容量为target的背包所能背的最大重量。
    那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
    在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
    那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

    class Solution:
        def lastStoneWeightII(self, stones: List[int]) -> int:
            all_weight = sum(stones)
            target = all_weight // 2
            dp = [0] * 1501
            for i in range(len(stones)):
                for j in range(target, stones[i] - 1, -1):
                    dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
            return all_weight - dp[target] * 2
    
  2. 问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

    假设加法的总和为x,那么减法对应的总和就是sum - x。
    所以我们要求的是 x - (sum - x) = target
    x = (target + sum) / 2
    此时问题就转化为,装满容量为x背包,有几种方法。
    对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

    class Solution:
        def findTargetSumWays(self, nums: List[int], target: int) -> int:
            sumValue = sum(nums)
            if abs(target) > sumValue or (sumValue + target) % 2 == 1: return 0
            bagSize = (sumValue + target) // 2
            dp = [0] * (bagSize + 1)
            dp[0] = 1
            for i in range(len(nums)):
                for j in range(bagSize, nums[i] - 1, -1):
                    dp[j] += dp[j - nums[i]]
            return dp[bagSize]
    

    求组合数:外层for循环遍历物品,内层for遍历背包。

    class Solution:
        def change(self, amount: int, coins: List[int]) -> int:
            dp = [0] * (amount + 1)
            dp[0] = 1
            for c in coins:
                for j in range(c, amount + 1):
                    dp[j] += dp[j - c]
            return dp[-1]
    

    求排列数:外层for遍历背包,内层for循环遍历物品。

    class Solution:
        def combinationSum4(self, nums: List[int], target: int) -> int:
            dp = [0] * (target + 1)
            dp[0] = 1
            for j in range(1, target + 1):
                for c in nums:
                    if j - c >= 0:
                        dp[j] += dp[j - c]
    
            return dp[-1]
    

    看成完全背包的排列问题

    class Solution:
        def climbStairs(self, n: int) -> int:
            dp = [0] * (n + 1)
            dp[0] = dp[1] = 1
            for j in range(2, n + 1):
                for i in range(1, 3):  # 物品只在【1,2】里面取
                    dp[j] += dp[j - i]
            return dp[-1]
    
  3. 问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 对应题目如下:

    其实是01背包问题!
    不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

     class Solution:
         def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
             dp = [[0] * (n + 1) for _ in range(m + 1)]  # 默认初始化0
             for cur_str in strs:  # 遍历物品
                 zeros = cur_str.count('0')
                 ones = cur_str.count('1')
                 # 遍历背包容量,且从后向前遍历
                 for i in range(m, zeros - 1, -1):
                     for j in range(n, ones - 1, -1):
                         dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
             return dp[m][n]
    
  4. 问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); 对应题目如下:

    考过没做出来
    初始化凑成每个金额都使用面额为1的硬币,则最多需要amount个硬币,因此初始化为amount + 1
    与完全平方数的区别是coins里面可能没有1,所以dp要初始化为amount + 1

    class Solution:
        def coinChange(self, coins: List[int], amount: int) -> int:
            dp = [amount + 1] * (amount + 1) # 都是amount + 1
            dp[0] = 0
            for c in coins:
                for j in range(c, amount + 1):
                    dp[j] = min(dp[j], dp[j - c] + 1)
            return dp[-1] if dp[-1] < amount + 1 else -1 # 如果<就说明是由coins里面组成的
    

    考过没做出来
    就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

    class Solution:
        def numSquares(self, n: int) -> int:
            nums = [i**2 for i in range(1, n + 1) if i**2 <= n]
            dp = [n + 1] * (n + 1)
            dp[0] = 0
            for num in nums:
                for j in range(num, n + 1):
                    dp[j] = min(dp[j], dp[j - num] + 1)
            return dp[-1]
    
    

    外层遍历背包内层遍历物品时要注意判断if j >= len(word):
    能不能凑到第i个位置就看:
    ①不使用当前的word,dp[j]已经用其他方式凑好没有
    ②使用当前单词,j - len(word)的位置已经凑好没有and 当前的word 跟s在这个位置所需要的单词是不是一样的 s[j - len(word) : j]

    class Solution:
        def wordBreak(self, s: str, wordDict: List[str]) -> bool:
            n = len(s)
            dp = [False] * (n + 1)
            dp[0] = True
            for j in range(1, n + 1):
                for word in wordDict:
                    if j >= len(word):
                        dp[j] = dp[j] or dp[j - len(word)] and word == s[j - len(word) : j]
            return dp[-1]
    

总结:动态规划-我的一生之敌...(不是)转移方程一定要想清楚!!!

一、01背包问题

如果使用二维dp数组,先遍历物品还是先遍历背包重量都可以,且两层循环都是正序遍历!
如果使用一维dp数组,只能是物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

a.内层循环遍历顺序

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

因为倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组历的时候不用倒序呢?

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

b.两个嵌套for循环的顺序

只能是物品遍历的for循环放在外层,遍历背包的for循环放在内层。

因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品

二、完全背包问题

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

a.内层循环遍历顺序

01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大正序遍历

b.两个嵌套for循环的顺序

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

三、求装满背包有几种方法

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。(518.零钱兑换II)
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。(377. 组合总和 Ⅳ 、70. 爬楼梯进阶版(完全背包)
初始化为:
dp = [0] * (amount + 1)
dp[0] = 1
递推公式:dp[j] += dp[j - nums[i]]
  • 如果求最小数,那么两层for循环的先后顺序就无所谓了。(322. 零钱兑换 、279.完全平方数 )
初始化为:
dp = [amount + 1] * (amount + 1)
dp[0] = 0
递推公式为:dp[j] = min(dp[j], dp[j - nums[i]] + 1)

四、多重背包问题

多重背包介于完全背包和01背包问题之间,即每种物品的个数是指定的。

参考资料:
背包问题总结篇
一篇文章吃透背包问题!
一文搞懂完全背包问题

posted @ 2022-05-08 11:11  YTT77  阅读(276)  评论(0编辑  收藏  举报