文章目录
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
举个简单的例子,输入如下:
N=3, W=4;
Wt=[2, 1,3]
val=[4, 2, 3]
可以结合代码随想录背包问题总结一起复习
https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC
1.明确dp[i][j]数组及下标的含义
2.确定递推公式
3.dp数组如何初始化
4.确定遍历顺序(先物品,后背包)
5.举例推导dp数组(表格法)
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后再动手写代码;
0-1背包问题
1. 明确状态和选择
只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题
所以状态有两个,就是「背包的容量」和「可选择的物品」
再说选择,也很容易想到啊,对于每件物品,你能选择什么?选择就是「装进背包」或者「不装进背包」嘛
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1, 选择2...)
2.明确dp数组的定义
dp数组是什么?其实就是描述问题局面的一个数组。换句话说,我们刚才明确问题有什么「状态」,现在需要用dp数组把状态表示出来
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。
dp的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]
eg:如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6
根据这个定义,我们想求的最终答案就是dp[N][W]。
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]
3.根据选择, 思考状态转移的逻辑
简单说就是,上面伪码中「把物品i装进背包」和「不把物品i装进背包」怎么用代码体现出来呢?
这一步要结合对dp数组的定义和我们的算法逻辑来分析:
先重申一下刚才我们的dp数组的定义:
dp[i][w]表示:对于前i个物品,当前背包的容量为w时,这种情况下可以装下的最大价值是dp[i][w]。
- 如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。
- 如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。
而dp[i-1][w-wt[i-1]]也很好理解:你如果想装第i个物品,你怎么计算这时候的最大价值?换句话说,在装第i个物品的前提下,背包能装的最大价值是多少?
综上就是两种选择,结合dp的定义对两种选择用程序的语言表达(表达的时候选择就用了一个max函数,此外要结合最优子问题,把选择与dp[i-1]结合起来)
最后一步,把伪码翻译成代码,处理一些边界情况。
我用 C++ 写的代码,把上面的思路完全翻译了一遍,并且处理了w - wt[i-1]可能小于 0 导致数组索引越界的问题:
int knapsack(int W, int N, vector<int>& wt, vector<int>& val){
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N+1, vector<int>(W+1, 0));
for(int i=1;i<=N; i++){
for(int w =1;w<=W;w++){
if(w-wt[i-1]<0){
// 当前背包容量装不下,只能选择不装入背包
dp[i][w]=dp[i-1][w];
}
else{
// 装入或者不装入背包,择优
// 第i个物品的重量为 wt[i-1]
dp[i][w]=max(dp[i-1][w-wt[i-1]]+val[i-1], dp[i-1][w]);
}
}
}
return dp[N][W];
}
子集背包问题

算法的函数签名如下:
// 输入一个集合,返回是否能够分割成和相等的两个子集
bool canPartition(vector<int>& nums);
对于这个问题,看起来和背包没有任何关系,为什么说它是背包问题呢?
首先回忆一下背包问题大致的描述是什么:
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
那么对于这个问题,我们可以先对集合求和,得出 sum,把问题转化为背包问题:
给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
1. 明确状态和选择
这个前文 经典动态规划:背包问题 已经详细解释过了,状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
2.明确dp数组的定义
按照背包问题的套路,可以给出如下定义:
dp[i][j] = x 表示,对于前 i 个物品,当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。
比如说,如果 dp[4][9] = true,其含义为:对于容量为 9 的背包,若只是用前 4 个物品,可以有一种方法把背包恰好装满。
或者说对于本题,含义是对于给定的集合中,若只对前 4 个数字进行选择,存在一个子集的和可以恰好凑出 9。
根据这个定义,我们想求的最终答案就是 dp[N][sum/2],base case 就是 dp[…][0] = true 和 dp[0][…] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。
3.根据选择,思考状态转移的逻辑
回想刚才的 dp 数组含义,可以根据「选择」对 dp[i][j] 得到以下状态转移:
不选:如果不把 nums[i] 算入子集,或者说你不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态 dp[i-1][j],继承之前的结果。
选择:如果把 nums[i] 算入子集,或者说你把这第 i 个物品装入了背包,那么是否能够恰好装满背包,取决于状态 dp[i-1][j-nums[i-1]]。
首先,由于 i 是从 1 开始的,而数组索引是从 0 开始的,所以第 i 个物品的重量应该是 nums[i-1],这一点不要搞混。
dp[i - 1][j-nums[i-1]] 也很好理解:你如果装了第 i 个物品,就要看背包的剩余重量 j - nums[i-1] 限制下是否能够被恰好装满。换句话说,如果 j - nums[i-1] 的重量可以被恰好装满,那么只要把第 i 个物品装进去,也可恰好装满 j 的重量;否则的话,重量 j 肯定是装不满的。
最后一步,把伪码翻译成代码,处理一些边界情况。

bool canPartition(vector<int>& nums){
int sum=0;
for(int num:nums) sum+=num;
// 和为奇数时,不可能划分成两个和相等的集合 base case
if(sum %2 !=0) return false;
// 开始子集背包
int n = nums.size();
sum=sum/2;
// base case dp[0][...]=false # 可选择物品为0则无论背包容量多大,都无法装满
// dp[...][0]=true # 背包容量为0,无论多少物品,都能装满
vector<vector<bool>> dp(n+1, vector<bool>(sum+1, false));
for(int i=0; i<=n; i++){
dp[i][0]=true;
}
// 状态转移方程 dp[i][j] 处理选与不选
for(int i =1; i<=n;i++){
for(int j=1; j<=sum; j++){
if(j-nums[i]<0){
// 背包容量不足,不能装入第 i 个物品
dp[i][j]=dp[i-1][j];
}
else{
//dp[i][j]=max(
//dp[i-1][j], dp[i-1][j-nums[i-1]]);
dp[i][j]=
dp[i-1][j]||dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
完全背包问题

int change(int amount, int[] coins);
我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
这个问题和我们前面讲过的两个背包问题,有一个最大的区别就是,每个物品的数量是无限的,这也就是传说中的「完全背包问题」,没啥高大上的,无非就是状态转移方程有一点变化而已。
下面就以背包问题的描述形式,继续按照流程来分析。
1. 明确状态和选择
状态有两个,就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」嘛,背包问题的套路都是这样。
2. 明确dp数组的定义
dp[i][j] 的定义如下:
若只使用前 i 个物品,当背包容量为 j 时,有 dp[i][j] 种方法可以装满背包。
换句话说,翻译回我们题目的意思就是:
若只使用 coins 中的前 i 个硬币的面值,若想凑出金额 j,有 dp[i][j] 种凑法。
经过以上的定义,可以得到:
base case 为 dp[0][…] = 0, dp[…][0] = 1。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。// 无为而治竟然被提出来了
我们最终想得到的答案就是 dp[N][amount],其中 N 为 coins 数组的大小。
大致的伪代码思路如下:
int dp[N+1][amount+1]
dp[0][..]=0
dp[..][0]=1
for i in [1..N]:
for j in [1..amount]:
把物品i装进背包
不把物品i装进背包
return dp[N][amount]
3. 根据选择,思考状态转移的逻辑
注意,我们这个问题的特殊点在于物品的数量是无限的,所以这里和之前写的背包问题文章有所不同。
如果你不把这第 i 个物品装入背包,也就是说你不使用 coins[i] 这个面值的硬币,那么凑出面额 j 的方法数 dp[i][j] 应该等于 dp[i-1][j],继承之前的结果。
如果你把这第 i 个物品装入了背包,也就是说你使用 coins[i] 这个面值的硬币,那么 dp[i][j] 应该等于 dp[i][j-coins[i-1]]。
首先由于 i 是从 1 开始的,所以 coins 的索引是 i-1 时表示第 i 个硬币的面值。
dp[i][j-coins[i-1]] 也不难理解,如果你决定使用这个面值的硬币,那么就应该关注如何凑出金额 j - coins[i-1]。(我的理解还是在i个物品可选择下,但是当前硬币coins[i-1]不知道用多少个,所以j - coins[i-1]减一个,看是否还需要再减coins[i-1])
比如说,你想用面值为 2 的硬币凑出金额 5,那么如果你知道了凑出金额 3 的方法,再加上一枚面额为 2 的硬币,不就可以凑出 5 了嘛。
综上就是两种选择,而我们想求的 dp[i][j] 是「共有多少种凑法」,所以 dp[i][j] 的值应该是以上两种选择的结果之和:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j-coins[i-1]];
return dp[N][W]
最后一步,把伪码翻译成代码,处理一些边界情况。
int change(int amount, vector<int>& coins){
int n = coints.size()
vector<vector<int>> dp(n+1, vector<int>(amount+1, 0));
// base case
// dp[0][..]=0 #物品为0,凑法为0
// dp[..][0]=1 #背包容量为0, 无为而治,1种凑发
for(int i=0; i<=n;i++){
dp[i][0]=1;
}
// dp[i][j]结合选择
for(int i=1;i<=n;i++){
for(int j=1; j<=amount; j++){
if(j-amount[i-1]>0)
// 选与不选
dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
else
// 背包容量不足,不能装入第 i 个物品
dp[i][j]=dp[i-1][j];
}
}
return dp[n][amount];
}


被折叠的 条评论
为什么被折叠?



