DP(3) | 0-1背包 | Java | 卡码 46 & LeetCode 416 做题总结

代码随想录笔记
AcWing-背包九讲专题
一道例题
dd大牛背包9讲

背包笔记

对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。

01背包:n种物品,每种物品只有 1 个,每个物品只有选OR不选两种情况
完全背包:n种物品,每种物品有 无限 个,每个物品可选无限次
多重物品:n种物品,每种物品的个数各不相同,每个物品有限度
混合背包问题:
二位费用的背包问题:
分组背包

01 背包 :每种物品仅有一件,可以选择放或不放。

二维数组

题目:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

例子:背包最大重量为4
在这里插入图片描述

  • 暴力法
    每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 o ( 2 n ) o(2^n) o(2n),这里的n表示物品数量。

  • 二维dp数组01背包(dp五步)

  • 首先对于整个问题:m个物品,背包容量最大为n。
    初步将问题分解为:在已经知道了前m-1个物品的所有最优解的情况下(即无论背包容量多少),再加上第m个物品的情况。
    此时有三种情况:
    1、第m个的重量超过了背包容量n,不可能放下第m个,所以此时的最优解,就是m-1个物品,背包容量为n的最优解。
    2、第m个的重量小于n,可以放但是不放,此时的最优解仍然是m-1个物品,背包容量为n的最优解。
    3、第m个的重量小于n,可以放而且真的放了,此时最优解就是第m个物品的价值,加上,m-1个物品时背包容量为(n-第m个物品的重量)的最优解。(第一点保证了第三点 j-weight[i]一定大于零)

根据递推关系发现,构造问题的最优解,不仅需要一个i来标识选择物品的范围,还需要一个j,来求出在0到i选择物品且背包容量为j时的最优解。从而将dp数组确定为dpij

(1)确定dp数组以及下标的含义
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

(2)递推公式
不放物品i:dp[i-1][j]
放物品i : dp[i-1][j-weight[i]] + value[i]
(不放物品 i 时,背包重量 dp[i-1][j-weight[i]]
递推公式: dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

j-weight[i]在干什么?在保证装入 物品 i 之前有充分的背包空间。
在这里插入图片描述

(3)初始化

  • 一个例题
    在这里插入图片描述
    初始化,
    首列:全为零,因为背包容量为0的时候,什么都装不了,价值为0
    首行:如果表示物品,只放该物品时的价值为value[物品](前提是背包的容量要够);如果不表示物品,初始化全为0
    其余全部初始化为0

(4)遍历顺序
对于二维数组实现01背包,双层for循环的条件哪个在外都可以

一维数组(滚动)

  • 以下面为例
    在这里插入图片描述
  • 代码
//代码随想录
    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWight = 4;
        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] + " ");
        }
    }
  • debug打断点+手写
    在这里插入图片描述
    第二个for循环的条件j >= weight[i];直接保证当前i可以放进去
    逆序,为了之前数组的值不被污染。

dp[j] 容量为 j 的背包,拥有的最大价值为 dp[j]

  • 递推公式:dp[j] = max(dp[j]) ,dp[j-weight[i]] + value[i] )
    不放物品 i :dp[j] 【对比】二维数组中不放物品i是 dp[i-1][j]
    放物品i:dp[j-weight[i]] + value[i]
  • 初始化:dp[0] = 0
  • 遍历顺序:先物品后背包;且要倒序遍历背包。倒序遍历才能保证物品只添加一次

卡码 46. 携带研究材料

题目链接: https://kamacoder.com/problempage.php?pid=1046

  • AC-二维数组版
import java.util.*;

public class Main {
    
    public static void main(String[] args) {
        // 背包容量 N
        // 物品种类 M
        Scanner sc = new Scanner(System.in);
        
        int M = sc.nextInt();
        int N = sc.nextInt();
        
        int[] values = new int[M];
        int[] weights = new int[M];
        
        for(int i = 0; i < M;i++) {
            weights[i] = sc.nextInt();
        }
        
        
        for(int i = 0; i < M;i++) {
            values[i] = sc.nextInt();
        }
        
        int[][] dp = new int[M][N+1];
        
        // 初始化
        for(int i = weights[0]; i <= N; i++) {
            dp[0][i] = values[0];
        }
        
        // 先物品
        for(int i = 1; i < M; i++) {
            // 后背包
            for(int j = 0; j <= N; j++) {
                if(j-weights[i] < 0) {
                    dp[i][j] = dp[i-1][j];
                } else {
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weights[i]] + values[i]);
                }
            }
        }
        System.out.println(dp[M-1][N]);
    }
}
  • 出错点
    初始化错误:背包初始化是 int[][] dp = new int[M][N+1]; 而不是 int[][] dp = new int[M][N]; ,因为 列代表 背包的容积是 0-N。
    所以返回值、初始化也有错
  • 这里的双层for循环哪个在外都可以。for物品 for背包容量,对于二维数组实现的0-1背包,是可以颠倒的。如下图,他的值是由左上方或者正上方得出来的

在这里插入图片描述

  • ac-滚动数组版
import java.util.*;

public class Main {
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int M = sc.nextInt();
        int N = sc.nextInt();
        int[]weight = new int[M];
        int[]value = new int[M];
        int[]dp = new int[N+1];
        
        for(int i=0; i<M; i++) {
            weight[i] = sc.nextInt();
        }
        
        for(int i=0; i<M; i++) {
            value[i] = sc.nextInt();
        }
        
        for(int i=0; i<M; i++) {
            for(int j=N; j>=weight[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j-weight[i]]+value[i]);
            }
        }
        System.out.println(dp[N]);
    }
}

416. 分割等和子集

请问这道题和动态规划有啥关系呢?

思路(卡哥的思路):
① 回溯,每个元素取或者不取,时间复杂度是指数级别的
②动态规划
在这里插入图片描述
以示例1为例,数组求总和22,和的一半11就是两个子集分别的和。
问容量为11的背包,能不能被[1,5,11,5]装满,如果装满了,就说明可以组成11,如果装不满,就说明不能组成11。如果能凑成11,那么剩下的元素肯定也能凑成另一个11。

背包问题的关键在元素可不可以重复使用,这里不可以,所以是0-1背包。每一个元素相当于一个物品,物品的重量和价值都是它的值。

  • 看了思路之后自己通过了
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int i=0; i<nums.length; i++) {
            sum += nums[i];
        }
        if(sum % 2 != 0) return false;  //不可能是奇数,因为要划分成两个数组。

        // M = nums.length
        // N = sum/2
        // weight[i] = nums
        // value[i] = nums
        int M = nums.length;
        int N = sum/2;

        int[]dp = new int[N+1];
        for(int i=0; i<M; i++) {
            for(int j=N; j>=nums[i]; j--) {
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }

        if(dp[N] == N) {
            return true;
        } else {
            return false;
        }
    }
}
  • 补充 特判:如果是奇数,就不符合要求

Java

  • 导入
import java.util.*; 把util这个包下的全部类导入到程序中
  • 输入
Scanner sc = new Scanner(System.in);
        
int M = sc.nextInt();
int N = sc.nextInt();

int[] values = new int[M];
int[] weights = new int[M];

for(int i = 0; i < M;i++) {
    weights[i] = sc.nextInt();
}


for(int i = 0; i < M;i++) {
    values[i] = sc.nextInt();
}
  • 输出
System.out.println();
  • main函数为入口
public class Main {
    
    public static void main(String[] args) {
        // 
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值