一、什么是背包问题
有n件物品,每件物品的体积为 ,价值为 ,求在有限的背包容量 下所能携带的最大价值。背包问题可分为 0 - 1 背包、完全背包与多重背包。
二、0 - 1 背包
每件物品最多选择一次。
dp[0] = 0; //初始化
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) { //逆序
dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
}
}
1、(416)分割等和子集
一个 只包含正整数 的 非空 数组。判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
假设数组和为 t ,当 t 为奇数时,数组不可分割。
当 t 为偶数时,可将 t / 2视为W。
dp[0] = true; //初始化
for (int i = 1; i < n; i++) {
for (int j = t/2; j >= w[i]; j--) { //倒序
dp[j] |= dp[j - w[i]];
}
}
2、(494)目标和
一个 非负整数数组 和一个目标数 S 。对于数组中的任意一个整数,可以从 + 或 - 中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
提示:
- 数组非空,且长度不会超过 20 。
- 初始的数组的和不会超过 1000 。
- 保证返回的最终结果能被 32 位整数存下。
方法一:
当 S > 1000 || S < -1000 时,方法数为0。因此 -1000 ~ 1000 可以看作 W 的范围,开辟 dp[2001] 的数组,其中 0 ~ 1000 代表 -1000 ~ 0,1001 ~ 2000 代表 1 ~ 1000。
dp[w[0] + 1000] = 1;
dp[1000 - w[0]] += 1; //1000 - w[0]与w[0] + 1000可能相同,故加1
for (int i = 1; i < n; i++) {
int[] next=new int[2001]; //记录上一行的数据
for (int j = 0; j <= 2000; j++) {
if (j - nums[i] >= 0) next[j] += dp[j - nums[i]]; // +
if (j + nums[i] <= 2000) next[j] += dp[j + nums[i]]; // -
}
dp = next;
}
return dp[S+1000];
方法二:
原命题等同于:设数组集合为 ,可将 分为两个子集 与 ,即:
命题转化为当 为偶数且 且 时的背包问题。
dp[0] = 1;
for (int i = 0; i < n; i++) {
for (int j = W; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
3、(474)一和零
一个二进制字符串数组 strs 和两个整数 m 和 n 。
求 strs 最大子集的大小,该子集中最多有 m 个 0 和 n 个 1 。
二维 0 - 1 背包,同样先遍历循环池,后遍历target。
int l = strs.length;
int[][] dp = new int[m + 1][n + 1];
int[][] cal = new int[l][2];
for (int i = 0; i < l; i++)
for (int j = 0; j < strs[i].length(); j++) {
if (strs[i].charAt(j) == '0') cal[i][0]++;
else cal[i][1]++;
}
for (int i = 0; i < l; i++)
for (int j = m; j - cal[i][0] >= 0; j--)
for (int k = n; k - cal[i][1] >= 0; k--)
dp[j][k] = Math.max(dp[j][k], dp[j - cal[i][0]][k - cal[i][1]] + 1);
return dp[m][n];
三、完全背包
每件物品可选择多次。
求组合数外层遍历循环池,内层遍历target。
求排列数外层遍历target,内层遍历循环池。
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = w[i]; j <= W; j++) { //顺序
dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
}
}
1、(139)单词拆分
给定一个非空字符串 s 和一个包含非空非重复单词的列表,判定 s 是否可以被拆分为一个或多个在字典中出现的单词。
拆分时可以重复使用字典中的单词。
考虑排列顺序的完全背包问题,外层循环为 target ,内层循环为选择池。
dp[0] = true;
int n = s.length();
for (int i = 1; i <= n; i++)
for (String each : wordDict) {
int l = each.length();
if(i - l >= 0 && s.substring(i - l,i).equals(each))
dp[i] |= dp[i-l];
}
return dp[n];
2、(279)完全平方数
求和为 n 的完全平方数的最少数量。
外层循环为 target ,内层循环为选择池。
for (int i = 1; i <= n; i++) {
dp[i] = i; //最坏情况,都由1组成
for (int j = 2; j * j <= i; j++)
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
3、(322)零钱兑换
给定不同面额且数量不限的硬币 coins 和一个总金额 amount。计算可以凑成总金额所需最少的硬币个数,若无法凑成则返回 -1。
外层循环为 target ,内层循环为选择池。
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
for (int j : coins) {
if(j <= i) dp[i] = Math.min(dp[i], dp[i - j] + 1);
}
return dp[amount] > amount ? -1 : dp[amount];
4、(518)零钱兑换 II
给定不同面额且数量不限的硬币 coins 和一个总金额 amount。计算可以凑成总金额所需的硬币组合数。
外层循环为选择池 ,内层循环为 target。
dp[0] = 1;
for (int i = 0; i < coins.length; i++)
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
return dp[amount];
5、(377)组合总和
给定一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。计算 nums
中总和为 target
的元素组合的个数。
考虑排列顺序的完全背包问题,外层循环为 target ,内层循环为选择池。
dp[0] = 1;
for (int i = 1; i <= target; i++)
for(int j : nums)
if(j <= i) dp[i] += dp[i - j];
return dp[target];