最近学习了背包问题,下面对0-1背包问题和完全背包问题进行总结。
本篇文章适合大致了解背包问题的读者,一些基础知识没有展开讨论。
求解动态规划问题的五个步骤:
1、确定dp数组的含义和范围
2、根据dp数组的含义退出状态转移公式
3、根据状态转移公式对dp数组进行初始化
4、确定遍历顺序
5、举例推导动态规划过程
背包问题:形如给定一个容量有限的背包和若干具有质量和价值的物品并求其最大承载价值的问题即为背包问题。根据可选物品数量的不同分为:0-1背包(每种物品只有一个)、完全背包(每种物品均为无限个)、多重背包(每种物品数量不同)、分组背包(按组打包,每组选择一个)。
滚动数组:背包问题从定义上看需要二维数组作为dp数组,但本质上还是对一个一维数组和重复维护,所以可以使用维护一维数组的方式来节约空间复杂度。
0-1背包的代码思路:
1、确定dp数组的含义和内存范围:dp[i]意为容量为i的背包最大价值为dp[i];
2、根据dp数组的含义退出状态转移公式:对每个物品进行便利,每个物品只有两种选项,放或者不放,所以需要比较两种结果取最大值。dp[i]=max(dp[i],dp[i-nums[j]]+value[j]);
3、数组初始化:可以看出滚动dp数组每个值的确定都依靠上一个物品在此容量下的价值和当前容量空出此容纳物品的空间后的最大价值加上此物品的价值来确定,所以全部初始化为0即可,前提是每个物品的价值均为正,否则需要初始化为最小值,防止初次便利时被0覆盖;
4、确定遍历顺序:运用滚动数组的话必须从最大背包容量开始遍历(这也是0-1背包和完全背包的最大区别),并且必须先遍历物品后遍历容量(物品遍历的for循环在外层,背包容量遍历的循环在内层)。原因是每个物品只能放一次,如果从小容量向大容量遍历的话dp[i-nums[j]]可能已经包含了一次此物品,所以要从后向前遍历,保证每个物品只被放入一次。必须先遍历物品的原因是滚动数组与二维数组不同,每次的值依赖于本行左边的值(将滚动数组想象成二维数组,每次维护的时候变成二维数组的下一行),所以需要一次求出完整的一行。
5、举例推导动态规划过程。
for(int i=0;i<numsSize;i++){
for(int j=bagSize;j>=weight[i];j--){//j>=weight[i]的原因是要保证背包能够容纳当前物品
dp[j]=max(dp[j],dp[j-nums[i]]+value[i]);
}
}
完全背包:完全背包与0-1背包的区别仅仅在于完全背包的内层循环是从最小容量向最大容量遍历。这样遍历每次查询上一次的状态时是已经检查过的状态;
for(int i=0;i<numsSize;i++){
for(int j=weight[i];j<=bagSize;j++){//j=weight[i]的原因是要保证背包能够容纳当前物品
dp[j]=max(dp[j],dp[j-nums[i]]+value[i]);
}
}
下面介绍一种基于背包问题的变式算法:
一、求解装背包的组合数:
dp[i]:容量为i的背包共有dp[i]种装法;
状态转移公式:dp[i]+=dp[i-nums[i]];(求组合数的动态规划基本都用这个公式)
因为每个状态都由前一个状态累加得出,所以将dp[0]初始化为1,其余初始化为0即可;
for(int i=0;i<numsSize;i++){
for(int j=bagSize;j>=weight[i];j--){//j>=weight[i]的原因是要保证背包能够容纳当前物品
dp[j]+=dp[j-nums[i]];
//意思是拿每个物品试一遍,每多一个可选的物品,就多了dp[j-nums[i]]种组合方式
//j-nums[i]可以理解为先腾出当前物品的空间,看看有多少种放法,此时放入该物品的组合就多了这么多次
}
}
以上是本人初学背包问题的一点总结,如有错误还请指正。