一.01背包
1.问题描述
对于01背包问题题干,移步至AcWing 👇👇👇
2. 01背包问题 - AcWing题库https://www.acwing.com/problem/content/2/
代码按照下题干编写:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 c [ i ],质量是 w [ i ]
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总质量最大
输出最大质量
2.问题分析
对于动态规划问题,我们主要从两个方面分析:
状态表示 和 状态计算
对于状态表示,要考虑怎么用dp数组,用几维的dp数组,用dp数组来表示什么状态
一般来说 题目的限制条件个数 ,决定了dp数组的维度
例如01背包问题中的限制条件有体积 (对应背包容积)和物品总个数
按照y总的闫式DP分析法 图解如下
3.二维dp代码
根据图解 可以写出代码 👇👇👇
//二维01背包
//状态转移方程 :dp[i][j]=max(dp[i-1][j-volume[i]]+weight[i] , dp[i-1][j])
//前i个物品 容量为j 选取当前物品(物品-1,背包容量-当前物品体积)
// 不选取当前物品 (最大值必定是前i-1个物品,容量为j的最大值)
//选与不选取最大值
//二维01背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001][1001],w[1000],c[1000];
int main()
{
int n,V,i,j;
//n件物品 背包容量V
cin>>n>>V;
for(i=0;i<n;i++) cin>>w[i];
for(i=0;i<n;i++) cin>>c[i];
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++){
for(j=c[i];j<=V;j++){
//状态转移方程
//capacity weight
dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
}
cout<<dp[n][V];
}
}
4.代码优化过程
👆👆👆
通过观察上述代码,我们可以发现,那么对于我们的程序,时间上 01背包问题的时间复杂度为O(N*W),其中N为物品的数量,W为背包的最大容量,时间复杂度无需优化
但是当求 dp[i][j]时,每次用的是dp[i-1][…],也就是用的第i-1层所求出来的结果,所以从空间上,我们可以对我们的程序进行优化,用一维的dp数组就可以解决该问题
当把我们的状态转移方程
里含i的那一维删除后 成了
所以压缩后的状态转移方程的括号中的dp[ j ]代表的是dp[ i-1 ][ j ]也就是上一层的dp[ j ]的结果(这个的问题不大,因为现在要求第i层的dp[ j ],那么dp[ j ]里面存放的自然就是上一层所求出的dp[ j ]的结果,只有当max函数比较完毕后,dp[ j ]才从第i-1层的结果更新成了第i层,也就是当前层的结果)
关于 dp[ j - c[ i ] ] 代表的也是上一层的结果,也就是dp[ i - 1 ][ j - c[ i ] ]
(那么这个就要考虑 j 在循环中是递增还是递减的了,如果要保证代表的是就应该让dp[ j ]先于dp[ j - c[ i ] ]求出来
这样的话,求dp [ j ]时,在状态转移方程中
括号中的dp [ j ]还没求出,因为正在求,dp [ j ] 还保留着上一层的结果,也就是dp [ i - 1 ][ j ]
并且由于dp[ j - c [ i ] ]还没求出,它代表的也是上一层的值,也就是dp[ i - 1 ] [ j - c[ i ] ]
此时刚好和我们的二维数组的dp相吻合,所以如果想进行空间优化,只要让dp[ j ] 比 dp[ j - c[ i ] ]先求出来就好,也就是让循环中的 j 由从小到大改为从大到小循环就好,此时因为 ,所以大的先求出,也就是dp [ j ] 先求出,dp [ j - c[ i ] ]后求出,与我们的预期相同)
5.一维dp代码
上述文字表述的代码表示如下
(为方便读者观看,代码中仍有注释用来解释,与上面的文字表述意思相同)👇👇👇
//二维01背包
//状态转移方程 :dp[i][j]=max(dp[i-1][j-volume[i]]+weight[i] , dp[i-1][j])
//前i个物品 容量为j 选取当前物品(物品-1,背包容量-当前物品体积)
// 不选取当前物品 (最大值必定是前i-1个物品,容量为j的最大值)
//选与不选取最大值
//优化的心路历程:
//看二维背包发现.对于dp[i][j]都需要用dp[i-1][…]这一层,也就是说都要用dp[i][…]前面那一层
//所以这一层可以省略,只要i从小到大,dp[i]在操纵前存放的永远都是dp[i-1]的值
//然后对于 j 选择从大到小也避免了重复的计算
//因为dp[i][j]的意义本身就是前i个物品 容量为j
//所以如果是从小到大计算的话 dp[j]=max(dp[j],dp[j-c[i]]+w[i]); 中的dp[j-c[i]]会有重复
//(从小到大的时候,j-c[i]<j,所以dp[j-c[i]]一定是比dp[j]先算出来的,这样就会有重复)
//但是从大到小的话dp[j]和dp[j-c[i]]比较时候一定是没有重复的
//一维01背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001],w[1000],c[1000];
int main()
{
int n,V,i,j;
//n件物品 背包容量V
cin>>n>>V;
for(i=0;i<n;i++) cin>>w[i];
for(i=0;i<n;i++) cin>>c[i];
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++){
//倒着循环 防止背包重复装取
for(j=V;j>=c[i];j--){
//状态转移方程
//capacity weight
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
// dp[i][j]=max(dp[i-1][j],dp[?][j-c[i]]+w[i])
// 如果是二维的时候,那么应该?处是i-1,那么如何保证呢
// 假如j是从小到大的,那么由于dp[j-c[i]]一定比dp[j]先算出来
// 并且就是在这一层算出来的(即i这一层)
// 所以这里的dp[j-c[i]]+w[i]实际上是与dp[i][j-c[i]]+w[i]等价的
// 如果让这里的dp[j-c[i]]+w[i]和dp[i-1][j-c[i]]+w[i]等价
// 就应该使得dp[j-c[i]]比dp[j]后算出来(此时dp[j-c[i]]中仍然是上一层i-1时的数据)
// 也就是dp[j-c[i]]与dp[i-1][j-c[i]]等效
// 所以使j从大到小循环,即可保证在第i层时,先算dp[j]再算dp[j-c[i]]
// 那么dp[j-c[i]]中保存的时第 i 层的数据
// 此时再做max操作即与dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])等价
}
cout<<dp[V]<<endl;
}
}
二.完全背包
1.问题描述
请移步至AcWing 👇👇👇
3. 完全背包问题 - AcWing题库https://www.acwing.com/problem/content/3/
代码按照以下题干编写
有 N 件物品和一个容量是 V 的背包。每件物品可使用任意次。
第 i 件物品的体积是 c [ i ],质量是 w [ i ]
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总质量最大
输出最大质量
2.问题分析
先说结论:
0-1背包 dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w)
完全背包 dp[i][j]=max(dp[i-1][j],dp[i] [j-v]+w)⭐
可知0-1背包和完全背包的状态转移方程大致相同,但是有个小小的不同,其实分析起来差距很大
正如 01 背包一样我们仍要从 状态表示 和 状态计算 两方面进行分析,完全背包也如此
完全背包的限制条件也是物品个数 和 背包容量 所以也要 先由二维dp数组表示
对于完全背包问题每个物品可以取任意个
那么对于第 i 个物品,就有取0,1,2,3,4…n 个的情况
(直到总体积大于等于j)(第i 个物品体积为 v 质量为w)
那么也就是
①dp[ i ][ j ] =max(dp[ i-1 ][ j ],dp[ i-1 ][ j-v ]+w , dp[ i-1 ][ j-2v ]+2w,…,dp[ i-1 ][ j-n*v ]+n*w)
对应第 i 个物品取 0 1 2 … n 个的情况
如果背包容积不是j 而是j-v,那么相当于
②dp[ i ][ j-v ]=max(dp[ i-1 ][ j-v ],dp[ i-1 ][ j-2v ]+w,…,dp[ i-1 ][ j-n*v ]+(n-1)*w
①②对比会发现①②大部分相同,①除去首项后相当于整体比②多w
也就是dp[ i ][ j ]=max(dp[ i-1 ][ j] ,②+w)
即dp[ i ][ j ] =max(dp[ i-1 ][ j ],dp[ i ][ j-v ]+w) ⭐
得出最后的状态转移方程,与0-1背包的像,但是得出结论的历程不一样
对比下
0-1背包 dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i-1 ][ j-v ]+w)
完全背包 dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i ][ j-v ]+w)⭐
用 y总的 闫式dp分析法 的分析图解如下
3.二维dp代码
根据图解 可以写出代码 👇👇👇
//二维完全背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001][1001],w[1000],c[1000];
int main()
{
int n,V,i,j;
//n件物品 背包容量V
cin>>n>>V;
for(i=0;i<n;i++) cin>>w[i];
for(i=0;i<n;i++) cin>>c[i];
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++){
for(j=c[i];j<=V;j++){
//状态转移方程
//capacity weight
dp[i][j]=max(dp[i-1][j],dp[i][j-c[i]]+w[i]);
}
cout<<dp[n][V];
}
}
4.代码优化过程
与01背包类似 完全背包也可以用一维数组进行空间优化,降低空间复杂度
0-1背包 dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w)
完全背包 dp[i][j]=max(dp[i-1][j],dp[i] [j-v]+w)⭐
通过比较可以得知,若想使用一维dp关于 dp[i-1][j] 与01背包的一致即可
但是对于dp[i][j-v]可知与01背包的不同 需要优化
回顾下上面所说的01背包的优化过程:
为了保证 dp[j-v]代表的是dp[i-1][j-v]只要使得dp[j-v]后于dp[j] 也就是 j 从大到小循环即可,此时在
中,dp[j]代表dp[i-1][j] dp[j-v]代表dp[i-1][j-v]
所以 此时我们想让 dp[j-v]代表dp[i][j-v] 只需要再让 j 从小到大循环即可
5.一维dp代码
完全背包一维dp代码如下 👇👇👇
//一维完全背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001],w[1000],c[1000];
int main()
{
int n,V,i,j;
//n件物品 背包容量V
cin>>n>>V;
for(i=0;i<n;i++) cin>>w[i];
for(i=0;i<n;i++) cin>>c[i];
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++){
for(j=c[i];j<=V;j++){
//状态转移方程
//capacity weight
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
//j 正着循环 由于 j > j-c[i]
//所以在第i层求dp[j]时,dp[j-c[i]]已经在此层(第i层)被求出
//也就是说dp[j-c[i]]在此时实际是dp[i][j-c[i]]
//dp[j]在此时是dp[i-1][j]
//符合二维完全背包的逻辑
}
cout<<dp[V]<<endl;
}
}
三.总结
经过优化空间复杂度的过程,我们发现 01背包和完全背包问题优化的主要点是搞清楚
一维的dp数组对应的值是在哪一层所求出来的,然后再对应其二维dp数组的状态转移方程即可
所以在做题时 先根据闫式dp分析法按照 状态表示 和 状态计算 分析
根据题目中的限制写出状态转移方程 再根据状态转移方程判断是否在第 i 层用的都是 第 i 层或第i-1层的数据,进而根据实际情况选择正确的循环方向,进而优化空间复杂度