一、01背包
难度中等912收藏分享切换为英文接收动态反馈
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
(由于是恰好装满,所以应该将dp[0]初始化为0而将其他全部初始化为INT_MIN)
初始化为INT_MIN
理由:dp[i]>0时,表示此时背包恰好能装的最多量,如果初始化为0,dp[i]代表当下可装最多而不是恰好。
举例 nums = {1, 2, 4, 9}
0 1 2 3 4 5 6 7 8
1 0 1 - - - - - - -
2 1 2 - - - - -
4 1 2 2 3 -
9
package zsh;
import java.util.Arrays;
public class Knapsack01 {
public static void main(String[] args) {
int[] nums = {1,2,3,4}; //{1,5,11,5};{2, 6, 8, 10}
int sum = 0;
for (int num : nums) {
sum += num;
}
int remainder = sum % 2;
if(remainder == 1){
System.out.println(false);
}
else{
int capacity = sum / 2;
int[] dp = new int[capacity + 1];
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = capacity; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-nums[i]] + 1);
}
}
if(dp[capacity] > 0){
System.out.println(true);
}
else{
System.out.println(false);
}
}
}
}
二、完全背包
leecode322 零钱兑换
package zsh;
import java.util.Arrays;
public class UnboundedKnapsack {
public static void main(String[] args) {
int[] coins = {2, 5};
int amount = 11;
int[] dp = new int[amount+1];
Arrays.fill(dp, 0x3f3f3f3f); //设成Integer.MAX_VALUE会使下面dp[j - coins[i]] + 1溢出
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
// for (int j = coins[i]; j <= amount ; j += coins[i]) { //注意此处不能为j += coins[i],会略去很多中间态,使结果错误
for (int j = coins[i]; j <= amount ; j++) {
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
System.out.println(dp[amount] == 0x3f3f3f3f ? -1 : dp[amount]);
}
}
三、多重背包
第一行 可使用钱总额N 零食种类N
第2~N+1行 零食价格 数量 喜爱程度
问:选取X元零食可达到最大喜爱度
6 7
3 1 8
4 1 2
3 1 1
9 1 7
4 1 1
5 1 18
4 1 4
输出 18
6 7
3 1 8
4 1 2
3 1 1
9 1 7
4 1 1
4 1 8
4 1 4
输出 9
状态方程:
# k为装入第i种物品的件数, k <= min(n[i], j/w[i])
dp[i][j] = max{(dp[i-1][j − k*w[i]] + k*v[i]) for every k}
同理也可以进行空间优化,而且 j 也必须逆向枚举,优化后伪代码为
// 完全背包问题思路二伪代码(空间优化版)
dp[0,...,W] = 0
for i = 1,...,N
for j = W,...,w[i] // 必须逆向枚举!!!
for k = [0, 1,..., min(n[i], j/w[i])]
dp[j] = max(dp[j], dp[j−k*w[i]]+k*v[i])
总的时间复杂度约为O(NWn-) = O(Wsigma(ni)) 级别.n-表示n平均
package zsh;
import java.util.Scanner;
public class BoundedKnapsack { //多重背包
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int amount = scanner.nextInt(); //总金额 //背包重量
int snackTypes = scanner.nextInt(); //零食种类 //物品种类
int[] price = new int[snackTypes]; //零食价格 //物品重量
int[] num = new int[snackTypes]; //零食数量 //物品数量
int[] degree = new int[snackTypes]; //喜爱程度 //物品价格
for (int i = 0; i < snackTypes; i++) {
price[i] = scanner.nextInt();
num[i] = scanner.nextInt();
degree[i] = scanner.nextInt();
}
int[] dp = new int[amount+1];
for (int i = 0; i < snackTypes; i++) {
// for (int j = amount; j > 0; j -= price[i]) { //注意此处不能为j -= price[i],会略去很多中间态,使结果错误
for (int j = amount; j >= price[i]; j--) {
int min = Math.min(j/price[i], num[i]);
for (int k = 1; k <= min; k++) {
dp[j] = Math.max(dp[j], dp[j - k * price[i]] + k * degree[i]);
}
}
}
System.out.println(dp[amount]);
}
}
//反例
//5 2
//2 3 4
//3 4 5
四、求方案总数
4.1 无序01背包
满足总价等于X且商品不允许重复,进行夜宵组合,计算对应的组合个数
输入:
第一行:总价X和商品件数M
第二行:M件商品的价格
输出:
返回不同夜宵的种类组合个数
如果输入无法达成目标,返回-1
输入
//25 5 5 4
//3 5 10 7 5 1 2 3 2
输出 2
输入
//2 5
//3 5 10 7 5
输出 -1
方法一:恰好装满01背包,求方案总数问题 dp[0] = 1 初始化巧妙,时间复杂度O(NW)
装满背包或将背包装至某一指定容量的方案总数。对于这类问题,需要将状态转移方程中的 max 改成 sum ,大体思路是不变的。初始化dp[0] = 1
例如若每件物品均是完全背包中的物品,转移方程即为
dp[i][j] = sum(dp[i−1][j], dp[i-1][j−w[i]]) // j >= w[i]
状态方程:
if(dp[i - price] != 0){
dp[i] += dp[i - price]; //等同上面sum()
}
举例
0 1 2 3 4 5
1 1 1 0 0 0 0
2 1 1 0 0
3 2 1 1
2 2 3 2 3
package zsh;
import java.util.Scanner;
public class CombinationSum {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int target = scanner.nextInt();
int num = scanner.nextInt();
int[] arr = new int[num];
for (int i = 0; i < num; i++) {
arr[i] = scanner.nextInt();
}
int[] dp = new int[target+1];
dp[0] = 1;
for(int price : arr){
for(int i = target; i >= price; i--){
if(dp[i - price] != 0){
dp[i] += dp[i - price];
}
}
}
if(dp[target] > 0) {
System.out.println(dp[target]);
}
else{
System.out.println(-1);
}
}
}
//25 5 5 4
//3 5 10 7 5 1 2 3 2
方法二:排序 dfs 剪枝 回溯
package zsh;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
public class Solution {
static int count = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// 关键步骤
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
}
/**
* @param candidates 候选数组
* @param len 冗余变量
* @param begin 从候选数组的 begin 位置开始搜索
* @param target 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
* @param path 从根结点到叶子结点的路径
* @param res
*/
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
count++;
return;
}
for (int i = begin; i < len; i++) {
// 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
if (target - candidates[i] < 0) {
break;
}
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
// if (i > begin && candidates[i] == candidates[i - 1]) {
// continue;
// }
path.addLast(candidates[i]);
// 调试语句 ①
// System.out.println("递归之前 => " + path + ",剩余 = " + (target - candidates[i]));
// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);
path.removeLast();
// 调试语句 ②
// System.out.println("递归之后 => " + path + ",剩余 = " + (target - candidates[i]));
}
}
public static void main(String[] args) {
// int[] candidates = new int[]{10, 1, 2, 7, 6, 1, 5};
// int target = 8;
int[] candidates = new int[]{1, 1, 1};
int target = 2;
Solution solution = new Solution();
List<List<Integer>> res = solution.combinationSum2(candidates, target);
System.out.println("输出 => " + res);
System.out.println(count);
}
}
原代码,没有排序剪枝,时间复杂度高,O(2^N)
package zsh;
import java.util.Arrays;
import java.util.Scanner;
public class Main1 {
static int res = 0;
static int totalPrice;
public static void dfs( int index, int[] priceArr, int sum){
sum += priceArr[index];
if(totalPrice < sum){
return;
}
if(totalPrice == sum){
res++;
return;
}
for(int i = index+1; i < priceArr.length; i++){
dfs(i, priceArr, sum);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
totalPrice = scanner.nextInt();
int num = scanner.nextInt();
int[] priceArr = new int[num];
for (int i = 0; i < num; i++) {
priceArr[i] = scanner.nextInt();
}
int min = Arrays.stream(priceArr).min().getAsInt();
if(totalPrice < min){
res = -1;
}
else {
for (int i = 0; i < num; i++) {
dfs( i, priceArr, 0);
}
if (res == 0) {
res = -1;
}
}
System.out.println(res);
}
}
//2 5
//3 5 10 7 5
4.2 有序完全背包
三个物品分别重1,2,4,有顺序的放入容量为n的背包,求方案总数。
输入 0 输出 0
输入 3 输出 3 解释:[[1,1,1],[2,1],[1,2]]
输入 5 输出 10 解释:[1,1,1,1,1],[]....
dp 0 1 2 3 4 5
1 1 1+1 2+1 3+2+1 6+3+1
不同于前面的无序方案,这里,[2,1],[1,2]是两种方案,因此循环体先目标值,后物品个数
public static int workSchedule(int n) {
if(n <=0) {
return 0;
}
int[] coins = {1,2,4};
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1;i<=n;i++){
for (int j = 0; j < coins.length; j++) {
if(coins[j]<=i) {
dp[i] += dp[i-coins[j]];
}
}
}
return dp[n];
}