背包题目汇总

这篇博客探讨了四种类型的背包问题:01背包、完全背包、多重背包和求方案总数的无序01背包。通过示例代码展示了如何解决这些问题,包括分割等和子集、零钱兑换和零食选择等场景。同时,提到了动态规划和剪枝策略在优化算法中的应用。
摘要由CSDN通过智能技术生成

一、01背包

416. 分割等和子集

难度中等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];
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值