背包类型问题

文章目录

前言

跟着labuladong刷题,做一些笔记,原网址链接: link.

1. 经典动态规划:0-1 背包问题

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?

在这里插入图片描述

第一步:明确【状态】和【选择】

状态用于描述子问题
例如在这个例子中,状态由两部分构成, 背包的剩余容量, 以及可以被选择的物品

选择是子问题之间的连接
例如在这个例子,选择就是对某个物品装进背包,或者不装进背包

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

第二步:明确dp数组的定义

首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 dp 数组。

dp[i][w] 的定义如下:对于前 i 个物品,当前背包的容量为 w.
这种情况下可以装的最大价值是 dp[i][w]。

比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。

base case 就是 dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。

int[][] dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            把物品 i 装进背包,
            不把物品 i 装进背包
        )
return dp[N][W]

第三步: 根据「选择」,思考状态转移的逻辑。

上面伪码中「把物品 i 装进背包」和「不把物品 i 装进背包」怎么用代码体现出来呢?
这就要结合对 dp 数组的定义,看看这两种选择会对状态产生什么影响:

如果你没有把这第 i 个物品装入背包,那么很显然,最大价值 dp[i][w] 应该等于 dp[i-1][w],继承之前的结果。
如果你把这第 i 个物品装入了背包,那么 dp[i][w] 应该等于 dp[i-1][w - wt[i-1]] + val[i-1]。

(细节):由于 i 是从 1 开始的,所以 val 和 wt 的索引是 i-1 时表示第 i 个物品的价值和重量。

伪代码:

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            dp[i-1][w],
            dp[i-1][w - wt[i-1]] + val[i-1]
        )
return dp[N][W]

2. 经典动态规划:子集背包问题

416.分割等和子集(中等)

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

可以把问题转化成0,1背包问题
用nums数组加和的一半当成背包容量,看能不能用一个子集正好凑出这个结果
状态:剩余容量, 可选择物品
选择:装还是不装
dp[i][w] = ture 表示可以选择i个物品,剩余w容量的情况下,能够正好装满
basecase: dp[0][…] = false dp[…][0] = true

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        s = sum(nums)
        if s %2 != 0:
            return False
        
        weight = int(s/2)
        n = len(nums)
        dp = [[False] * (weight + 1) for _ in range(n + 1)]
        #dp[n][weight]
        #base case, 剩余容量是0的时候不用装就是满的
        for i in range(n+1):
            dp[i][0] = True
        for i in range(1, n + 1):
            for j in range(1, weight + 1):
                if j >= nums[i-1]:
                    #注意这里两个都是i-1
                    dp[i][j] = dp[i-1][j-nums[i-1]] or dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] #不够大不能装第i个
        return dp[n][weight]

状态压缩

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        s = sum(nums)
        if s %2 != 0:
            return False
        
        weight = int(s/2)
        n = len(nums)
        dp = [False] * (weight + 1) 
        #dp[n][weight]
        #base case, 剩余容量是0的时候不用装就是满的
        dp[0] = True
        for i in range(1, n + 1):
            for j in reversed(range(1, weight + 1)):
                if j >= nums[i-1]:
                    dp[j] = dp[j-nums[i-1]] or dp[j]
                
        return dp[weight]

注意这里for j in reversed(range(1, weight + 1)):
是从后往前遍历,因为 dp[j-nums[i-1]] 其实应改是上一行的数据 dp[i-1][j-nums[i-1]]
通过从后往前遍历,当计算并更改 dp[j] 的时候并没有更改 dp[j-nums[i-1]] ,所以 dp[j-nums[i-1]] 仍然可以当作上一行的数据来使用

3. 经典动态规划:完全背包问题

518.零钱兑换II(中等)

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?

完全背包问题就是每种物品的数量可以是无限的

状态:剩余容量, 可选择物品
选择:当前装几个
dp数组定义:dp[weight][i] = res 表示剩余容量是weight,可选择前 i个 物品时,组合的总数
初始化:dp[weight][i] 都为0
basecase:dp[0][…] = 1 表示weight是0的时候,不论怎么选择都有恰好一种组合, 也就是啥也不选
选择:装几个
装0个:dp[weight][i] = dp[weight][i-1]
装1个:dp[weight][i] = dp[weight - coins[i-1]][i-1]
装2个:dp[weight][i] = dp[weight - 2 * coins[i-1]][i-1]

直到weight - 2 * coins[i-1]<0

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        weight = amount
        dp = [[0] * (n+1) for _ in range(weight + 1)]
        dp[0] = [1] * (n + 1)#basecase: when weight = 0
        for w in range(1, weight + 1):
            for i in range(1, n + 1):
                k = 0
                while w >= k * coins[i-1]:
                    dp[w][i] += dp[w - k * coins[i-1]][i-1]
                    k += 1
        return dp[weight][n]

这种方法非常慢, 原因是我们两个循环里面还有一个while循环,还是计算了不少重复子问题
其实可以稍微改良一下

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        weight = amount
        dp = [[0] * (n+1) for _ in range(weight + 1)]
        dp[0] = [1] * (n + 1)#basecase: when weight = 0
        for w in range(1, weight + 1):
            for i in range(1, n + 1):
                if w >= coins[i-1]:
                    dp[w][i] = dp[w - coins[i-1]][i] + dp[w][i-1]
                else:
                    dp[w][i] =dp[w][i-1]
        return dp[weight][n]

在压缩一下空间

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        weight = amount
        dp = [0] * (weight + 1)
        dp[0] = 1
        for i in range(1, n + 1):
            for w in range(1, weight + 1):
                if w >= coins[i-1]:
                    dp[w] = dp[w - coins[i-1]] + dp[w]

        return dp[weight]

这里注意要把i 和 w的遍历调换一下顺序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值