背包问题:
最经典的0-1背包
背包问题是有一个一定容量的背包,然后有一堆物品,这些物品有一定的重量,要求在不超过背包容量的情况下,装最多的重量的问题。
背包问题的变体
背包问题有很多的变体,主要有以下几个:
- 物品不仅仅有重量还会对应不同的价值,求价值最高,而不是重量最高
- 每个物品有无限个而不是只有一个
- 不是求最大重量,而是求达到重量的组合的个数
背包问题的解法:
背包问题是用动态规划的方法解决,可以构造一个二维动规数组解决问题,而二维数组的做法可以进一步优化空间复杂度变为一维的动规数组。
动态规划的问题主要要解决三个问题,1、dp数组的设计,2、边界情况,3、递推公式。
二维动规划数组的解法:
- 动态规划数组设置:设置大小为dp[items.length + 1][backpackSize + 1]的boolean数组,其中数组中每个元素dp[i][j]表示只用前i个物品,能否达到j的负重。+1是用于设置边界条件。
- 边界情况:边界条件是在考虑0件物品 和 背包负重为0 时最大负重为0。
- 递推公式:
- dp[i][j] = dp[i - 1][j] 只要用前i-1个物品能达到 j 重量,那用上物品i,也能达到j重量
- j >= items[i-1] && dp[i-1][j - items[i-1]] 时,dp[i][j] 设置为true
保证背包的负重大于物品i(这里-1是因为0用于设置边界,从1开始),证明当前负重能放下物品i,如果在不用i物品时,可以达到负重j减物品i的重量,那么用上了物品i,就可以达到j的重量。
- 具体的解法看下题 lintcode 92
一维度数组:
也是考虑动规数组的设计、边界情况、递推公式三个问题,二维数组优化为一维,是滚动重复运用了一维数组,达到二维的效果,它们都要两重循环,时间是一样的。
- 动规数组:设置大小为dp[backpackSize + 1]的boolean数组,dp[j]表示:能否达到j负重
- 边界情况:dp[0] = 0
- 递推公式:同样是遍历各个物体i,在只考虑i-1的物品时,如果能达到dp[j - items[i]],那么考虑了物品i,可以达到负重j。
这里注意,因为每个物品只能用一次,所以应该从最大负重往前递推,如果从前往后递推,物品就可能被使用了多次。 - 同样结合lintcode92 看完整的解法
背包变体的解法:
- 带价值的物品:递推数组不要设置为boolean,设置为int数组,表示价值
- 二维:dp[i][j]表示,只考虑前i的物品,负重不超过j的情况下,价值最高的值
- 一维:dp[j] 表示:不超过负重j的情况下,价值最高的值得
- 结合lintcode 125 看完整解法
- 物品不是只有一个,而是有无限个:只需要把递推的方向反转,刚刚提到最大负重往前递推,防止多次使用到物体,只要正向递推,就可以解决无限个物体的问题,具体看lintcode 440 完整解法
- 求组合个数:只要把动规数组的值,设置为达到这个重量的组合个数,边界情况dp[0]=1 即可,具体看 lintcode 563
lintcode 92 背包问题
这是经典的01背包问题:
二维数组的动态规划解法:
public int backPack(int backpackSize, int[] items) {
// 由于要从背包为0,物体数为0的状态开始,所以dp数组要多一行的维度
boolean dp[][] = new boolean[items.length + 1][backpackSize + 1];
for (int i = 0; i <= items.length; i++) {
for (int j = 0; j <= backpackSize; j++) {
dp[i][j] = false;
}
}
// dp[i][j] 表示 前i个背包,随机选若干个,能不能达到j的重量
dp[0][0] = true;
for (int i = 1; i <= items.length; i++) {
for (int j = 0; j <= backpackSize; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= items[i-1] && dp[i-1][j - items[i-1]]) {
dp[i][j] = true;
}
}
}
for (int i = backpackSize; i >= 0; i--) {
if (dp[items.length][i]) {
return i;
}
}
return 0;
}
一维滚动数组解法:
public int backPack(int backpackSize, int[] items) {
// dp[i]代表,容量为 i 的背包,最否能装到负重j
boolean dp[] = new boolean[backpackSize + 1];
dp[0] = true;
for(int i = 0; i < items.length; i++) {
for(int j = backpackSize; j >= items[i]; j--) {
if(dp[j - items[i]]){
dp[j] = true;
}
}
}
for(int j = backpackSize; j >= 0; j--){
if(dp[j]){
return j;
}
}
return 0;
}
lintcode 125 带价值的背包问题
这是01背包中物品带有价值的变体
只要把01背包问题中,dp数组的值设置为当前最大价值,而非boolean能否得到。稍微改动即可
二维数组解法:
public int backPackII(int backpackSize, int[] items, int[] itemsValues) {
// write your code here
int[][] dp = new int[items.length + 1][backpackSize + 1];
for(int i = 0; i <= items.length; i++){
for(int j = 0; j <= backpackSize; j++){
if(i == 0 || j == 0){
dp[i][j] = 0;
}else if(items[i-1] > j){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-items[i-1]] + itemsValues[i-1]);
}
}
}
return dp[items.length][backpackSize];
}
一维滚动数组:
public int backPackII2(int backpackSize, int[] items, int[] itemsValues) {
// write your code here
int[] dp = new int[backpackSize + 1];
for(int i = 0; i < items.length; i++){
for(int j = backpackSize; j >= items[i]; j--){
if(dp[j] < dp[j - items[i]] + itemsValues[i]){
dp[j] = dp[j - items[i]] + itemsValues[i];
}
}
}
return dp[backpackSize];
}
lintcode 440. 背包问题 III
这是01背包问题物体可以用无限次,且带有价值的变体
dp数组为当前最大价值,递推公式从前往后递推,无限次使用物品即可。
一维滚动数组
public int backPackIII(int[] items, int[] itemsValues, int backpackSize) {
int[] dp = new int[backpackSize + 1];
for(int i = 0; i < items.length; i++){
for(int j = items[i]; j <= backpackSize; j++){
if(dp[j] < dp[j - items[i]] + itemsValues[i]){
dp[j] = dp[j - items[i]] + itemsValues[i];
}
}
}
return dp[backpackSize];
}
lintcode 563. 背包问题 V
这是求达到target负重的组合个数,物品只能用一次
只要边界情况设置为1,然后把dp的值,变为当前的组合个数,递推时不是是否能达到的boolean,而是累加上前状态的个数即可。
public int backPackV(int[] nums, int target) {
// Write your code here
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; ++i)
for (int j = target; j >= nums[i]; --j)
dp[j] += dp[j - nums[i]];
return dp[target];
}
lintcode 562. 背包问题 IV
这是求达到target负重的组合个数,物品只能用无限次
只要只能用一次的稍微修改,改为从前向后递推即可。
public int backPackIV(int[] nums, int target) {
// write your code here
int[] dp = new int[target + 1];
dp[0] = 1;
for(int i = 0; i < nums.length; i++){
for(int j = nums[i]; j <= target; j++){
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}