蓝桥之01背包和砝码称重

背包问题:
题目描述
小明有一个容量为 V 的背包。

这天他去商场购物,商场一共有 N 件物品,第 i 件物品的体积为 wi​,价值为 vi​。

小明想知道在购买的物品总体积不超过 V 的情况下所能获得的最大价值为多少,请你帮他算算。

输入描述
输入第 1 行包含两个正整数N,V,表示商场物品的数量和小明的背包容量。

第 2∼N+1 行包含 2 个正整数w,v,表示物品的体积和价值。

1≤N≤102,1≤V≤103,1≤wi​,vi​≤10^3。
输入输出样例
示例 1

输入

5 20
1 6
2 5
3 8
5 15
3 3

输出

37

二维数组dp[i][j]表示前i个(第1个到第i个)物品装入容量为j的背包中获得的最大价值。
把每个dp[i][j]都看成一个背包:背包的容量为j,装第1~第i个物品。最后得到dp[N][C]就是问题的答案:把N个物品装进容量为C的背包的最大价值。
在计算dp[i][j]的过程中,分为以下两种情况
1.第i个物品的体积比容量j还大,不能装进容量为j的背包。直接继承前i-1个物品容量为j的背包的情况。也就是说dp[i][j]=dp[i-1][j]。
2.第i个物品的体积比容量小,这个时候能装进背包,又分为两种情况:装或者不装
装第i个物品。从前i-1个物品的情况推测,前i-1个物品是dp[i-1][j]。将第i个物品装进背包后,背包容量减少c[i],价值增加w[i],所以有dp[i][j]=dp[i-1][j-c[i]]+w[i]。(也就是说容量减少为c[i],我们需要找dp[i-1][j-c[i]的最大价值加上当前放入物品的价值)
不装第i个物品,那么最大价值还是dp[i][j]=dp[i-1][j],容量不变找i-1的物品的最大价值。
在两种情况下找最大价值的那一个。
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])

import java.util.Scanner;
public class 背包问题01 {
    
    
    public static void main(String[] args) {
    
    
        Scanner in=new Scanner(System.in);
        int N=in.nextInt();
        int V=in.nextInt();
        int []w=new int[N+1];//物品的体积
        int []v=new int[N+1];//物品的价值
        for(int i=0;i<N;i++) {
    
    
            w[i]=in.nextInt();//容量
            v[i]=in.nextInt();//价值
        }
        //dp[i][j]代表从下标为[0-N]的物品里任意取,放进容量为V的背包,价值总和最大是多少
        int[][] dp=new int[N][V+1];//v+1表示最大取V 0 1 2 3 4
        //初始化
        //i应该从w[0]开始,因为容量w[0]之后在V的容量范围中都是够装的
        for(int i=w[0];i<=V;i++){
    
    
            dp[0][i]=v[0];
        }
        //填充dp数组
        for(int i=1;i<N;i++){
    
    //i代表物品个数
            for(int j=1;j<=V;j++){
    
    //j代表物品容量
                if(w[i]>j){
    
    
                    //如果当前背包容量小于物品的容量,是不放物品
                    dp[i][j]=dp[i-1][j];//那么i-1个物品的价值就是当前情况最大的价值
                }
                else {
    
    
                    //如果当前背包容量大于物品的容量
                    //但是此时分为两种情况
                    //1.不放物品
                    //2.放物品 背包容量减少dp[i][j-w[i]],价值增加v[i]
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
                }
            }
        }
        System.out.println(dp[N-1][V]);

        //打印dp数组
        for(int i=0;i<N;i++){
    
    
            for(int j=0;j<=V;j++){
    
    
                System.out.print(dp[i][j]+"\t");
            }
            System.out.println("\n");
        }
    }
}

滚动数组实现二维数组降一维
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j]。
滚动数组需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
1.确定dp数组的定义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.一维dp数组的递推公式
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。
因此递归公式为:

dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);

3.一维dp数组初始化
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
4.遍历顺序

for(int i = 0; i < weight.size(); i++) {
    
     // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) {
    
     // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

第二个for循环倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次。
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
二维数组不需要倒序遍历的原因是因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。

   public static void main(String[] args) {
    
    
        int[] weight = {
    
    1, 2, 3,5,3};
        int[] value = {
    
    6, 5, 8,15,3};
        int bagWight = 20;
        testWeightBagProblem(weight, value, bagWight);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
    
    
        int wLen = weight.length;
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagWeight + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < wLen; i++){
    
    
            for (int j = bagWeight; j >= weight[i]; j--){
    
    
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //打印dp数组
        for (int j = 0; j <= bagWeight; j++){
    
    
            System.out.print(dp[j] + " ");
        }
    }

砝码称重
在这里插入图片描述
样例说明
能称出的 10 种重量是:1、2、3、4、5、6、7、9、10、11​。
1 = 1;
2 = 6 − 4 (天平一边放6,另一边放 4);​
3 = 4 − 1;
4 = 4;
5 = 6 − 1;
6 = 6;
7 = 1 + 6;
9 = 4 + 6 − 1;
10 = 4 + 6;
11 = 1 + 4 + 6。

1.dp数组的定义
dp[i][j]表示的是前i个砝码是否能够称出重量j。
2.初始化dp数组
dp[0][j]=0,因为没砝码肯定无法进行称重
3.确定dp数组的递推公式
1.当砝码重量等于需要称出的重量时直接称dp[i][j]=true
2.当不等时,此时分为3种情况
1.不加砝码的情况下,则dp[i][j]=dp[i-1][j]
2.放左边:dp[i-1][j-w[i]]表示上一次称砝码放天平左边能不能配出这个重量,如果dp[i-1][j-w[i]]=true,则表示左边能够配出这个j-w[i]的重量,这样再有当前的砝码w[i]放左边自然能够配出重量j。可能会出现j-w[i]为负数的情况(比如容量为3,当砝码重量为4,这个时候的话j-w[i]=3-4=-1),需要加绝对值左边变为1,这样我们将重量为4的砝码放到右边时,右边重量减左边重量能够称出容量3。
3.放右边:dp[i-1][j+w[i]]表示上一次称砝码放天平右边能不能配出这个重量。这个时候如果重量为j + w[i]为true的话,说明上一次找得到j+w[i]的重量,而此时我们又知道当前砝码w[i]的重量,将当前砝码放在左边,这个时候右边重量减左边重量,也能判断容量为j的是否为配成功。
dp数组
在这里插入图片描述

import java.util.Scanner;

public class Main {
    
    

    public static void main(String[] args) {
    
    
        // TODO Auto-generated method stub
        int count = 0;// 统计能配出的个数
        int[] w = new int[101];
        boolean[][] dp = new boolean[101][100001];//表示前i个砝码能否称出j重量
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long sum = 0L;
        for (int i = 1; i <= n; i++) {
    
    
            w[i] = sc.nextInt();// 每个砝码的重量
            sum += w[i];// 砝码最大重量
        }
        for (int i = 1; i <= n; i++) {
    
    // 有多少个砝码
            for (int j = 1; j <= sum; j++) {
    
    // 砝码最大重量
                if (j == w[i]) {
    
    // 能直接称出砝码的质量(不用放左边或放右边或不放)就直接赋值为1(true)说明可以称
                    dp[i][j] = true;
                } else {
    
    
                    // 分为三种情况1.dp[i-1][j]如果上一次的砝码是1说明可以称出这个重量,我们直接复制下来就行了,表示不放砝码
                    // 2.dp[i-1][abs(j-w[i])]表示上一次称砝码放天平左边能不能配出这个重量(如果上一次能够称出,后面我们在进行配重的时候就可以直接用上一次的,小问题推大问题)
                    // 3.dp[i-1][j+w[i]]表示上一次称砝码放天平右边能不能配出这个重量
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][Math.abs(j - w[i])] || dp[i - 1][j + w[i]];//||一个为true则为true,都为false则为false
                }
            }
        }
        for (int i = 1; i <= sum; i++) {
    
    
            // System.out.print(dp[n][i] + " ");
            if (dp[n][i]) {
    
    //前n个砝码能否称出重量i
                count++;
            }
        }
        System.out.println(count);
        // for (int i = 1; i <= n; i++) {//打印dp数组
        //     for (int j = 1; j <= sum; j++) {
    
    
        //        System.out.print(dp[i][j]+"\t");
        //     }
        //     System.out.println();
        // }
    sc.close();
    }
}

猜你喜欢

转载自blog.csdn.net/ChenYiRan123456/article/details/127838232
今日推荐