Skip to content

Day 30


正式开始背包问题,背包问题还是挺难的,虽然大家可能看了很多背包问题模板代码,感觉挺简单,但基本理解的都不够深入。

如果是直接从来没听过背包问题,可以先看文字讲解慢慢了解 这是干什么的。

如果做过背包类问题,可以先看视频,很多内容,是自己平时没有考虑到位的。

背包问题,力扣上没有原题,大家先了解理论,今天就安排一道具体题目。

01 背包问题(二维)

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

文章讲解:https://programmercarl.com/背包理论基础01背包-1.html

视频讲解:https://www.bilibili.com/video/BV1cg411g7Y6

01 背包问题场景

有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次,求解背包能背的物品最大价值是多少?

思路分析

(1)确定 dp 数组以及下标的含义

i 来表示物品、j 表示背包容量,dp[i][j] 表示从下标为 [ 0 - i ] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少

(2)确定递推公式

不放物品 i( j < weight [ i ] ):背包容量为 j,里面不放物品 i 的最大价值是 dp[ i - 1 ] [ j ]

放物品 i:背包空出物品 i 的容量后,背包容量为 j - weight[ i ],dp[i - 1] [ j - weight [ i ] ] 为背包容量为 j - weight [ i ] 且不放物品 i 的最大价值,那么 dp [ i - 1 ] [ j - weight [ i ] ] + value [ i ] (物品 i 的价值),就是背包放物品 i 得到的最大价值

递归公式: dp [ i ] [ j ] = max ( dp [ i - 1 ] [ j ], dp [ i - 1 ] [ j - weight [ i ] ] + value [ i ] ),因为要求最大价值,所以取最大

(3) dp 数组如何初始化

物品重量价值
物品 0115
物品 1320
物品 2430

初始化左边列(0,0,0)

首先从 dp [ i ] [ j ] 的定义出发,如果背包容量 j 为 0 的话,即 dp [ i ] [ 0 ],无论是选取哪些物品,背包价值总和一定为 0

初始化上边行(0,15,15,15,15)

(1)dp [ 0 ] [ j ] :i 为 0,存放编号 0 的物品的时候,各个容量的背包所能存放的最大价值

(2)j < weight [ 0 ] 的时候,dp[ 0 ][ j ] 应该是 0,因为背包容量比编号 0 的物品重量还小

(3)j >= weight [ 0 ] 时,dp [ 0 ] [ j ] 应该是 value [ 0 ],因为背包容量放足够放编号 0 物品

其余位置初始化

(1)递推公式:dp [ i ] [ j ] = max ( dp [ i - 1 ] [ j ], dp [ i - 1 ] [ j - weight [ i ] ] + value [ i ] );

(2)从递推公式可以看出,dp [ i ] [ j ] 是由左上方数值推导出来了,那么其他下标初始为什么数值都可以,因为都会被覆盖

(3)一开始就统一把 dp 数组统一初始为 0,更方便一些

(4)确定遍历顺序

根据递推公式和表格可以看出,当前的值是由正上方和左上方的的值推导而来

先遍历物品,后遍历背包;先遍历背包,后遍历物品,两种方式皆可

(5)举例推导 dp 数组

题解

java
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 材料的种类
        int n = scanner.nextInt();
        // 背包的空间
        int bageweight = scanner.nextInt();
        // 存储材料的重量
        int[] weight = new int[n];
        // 存储材料的价值
        int[] value = new int[n];

        for (int i = 0; i < n; i++) {
            weight[i] = scanner.nextInt();
        }
        for (int i = 0; i < n; i++) {
            value[i] = scanner.nextInt();
        }

        // 初始化 dp 数组
        int[][] dp = new int[n][bageweight + 1];

        // 左边列(可以省略,因为二维数组的默认初始化是0)
        // for (int i = 0; i < n; i++) {
        //     dp[i][0] = 0;
        // }

        // 上边行
        // 把物品 0 放入不同容量的背包,只有当背包容量大于等于 weight[0] 才能放入
        for (int i = weight[0]; i <= bageweight; i++) {
            // dp[0][i] 表示存放编号0的物品的时候,各个容量的背包所能存放的最大价值
            dp[0][i] = value[0];
        }

        // 遍历物品(从第二行开始遍历,递归公式需要由正上方和左上方的数值推导而来)
        for (int i = 1; i < n; i++) {
            // 遍历背包容量
            for (int j = 0; j <= bageweight; j++) {
                // 不放物品 i(背包容量不够)
                if (j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                }
                // 放物品 i(i 是索引下标)
                else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }

        }
        System.out.println(dp[n-1][bageweight]);
    }
}

01 背包问题(一维)

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

文章讲解:https://programmercarl.com/背包理论基础01背包-2.html

视频讲解:https://www.bilibili.com/video/BV1BU4y177kY

思路分析

对于背包问题其实状态都是可以压缩的(状压 DP),使用一维数组(滚动数组

一维数组实现思想

dp [ i ] [ j ] 表示从下标为 [ 0 - i ] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少

(1)二维 dp 数组递推公式:dp [ i ] [ j ] = max ( dp [ i - 1 ] [ j ], dp [ i - 1 ] [ j - weight [ i ] ] + value [ i ] );

(2)当前层的结果就是由上一层推出来的,那不如直接把上一层( dp [ i - 1 ]那一层 )的结果拷贝到当前层( dp [ i ] ),表达式完全可以是:dp [ i ] [ j ] = max ( dp [ i ] [ j ], dp [ i ] [ j - weight [ i ] ] + value [ i ] );

(3)与其把 dp[i - 1]这一层拷贝到 dp [ i ] 上,不如只用一个一维数组了,只用 dp [ j ] (一维数组,也可以理解是一个滚动数组)。

(4)这就是滚动数组的由来,需要满足的条件上一层可以重复利用,直接拷贝到当前层

(1)确定 dp 数组的定义

在一维 dp 数组中,dp [ j ] 表示:容量为 j 的背包,所背的物品价值可以最大为 dp [ j ]

(2)一维 dp 数组的递推公式

一维 dp 数组的思想由来就是把上一层( dp [ i - 1 ] )的结果拷贝到当前层( dp [ i ] ),所以在 上面递推公式的基础上,去掉 i 这个维度就好

递推公式为:dp [ j ] = max( dp [ j ], dp [ j - weight [ i ] ] + value [ i ] )

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

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

(3)一维 dp 数组如何初始化

dp 数组在推导的时候一定是取最大值,如果题目给的价值都是正整数,那么非 0 下标都初始化为 0,因为这样才能让 dp 数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

(4)一维 dp 数组遍历顺序

倒序遍历背包;先遍历物品,后遍历背包

物品重量价值
物品 0115
物品 1320
物品 2430

倒序遍历背包是为了保证物品 i 只被放入一次

正序遍历

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

从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了

② 先遍历物品,后遍历背包

如果先遍历背包,上面提到了背包必须是倒序遍历,这样才能保证物品只会被添加一次,正因为这个原因,就会导致所有背包只会有一个物品,无法达到背包价值最大

(5)举例推导 dp 数组

题解

java
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 材料的数量
        int M = scanner.nextInt();
        // 材料的大小
        int N = scanner.nextInt();

        // 材料占用的空间
        int[] costs = new int[M];
        // 材料的价值
        int[] values = new int[M];

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

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

        int[] dp = new int[N + 1];

        // 先遍历物品
        for (int i = 0; i < M; i++) {
            // 后遍历背包
            for (int j = N; j >= costs[i]; j--) {
                // 包含了选 物品 i 和不选 物品 i 的情况,取最大值
                dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]);
            }
        }
        System.out.println(dp[N]);
    }
}

416. 分割等和子集

本题是 01 背包的应用类题目

题目链接:https://leetcode.cn/problems/partition-equal-subset-sum

文章讲解:https://programmercarl.com/0416.分割等和子集.html

视频讲解:https://www.bilibili.com/video/BV1rt4y1N7jE

思路分析

本题要求集合里能否出现总和为 sum / 2 的子集。

01 背包场景迁移

(1)既有一个只能装重量为 sum / 2 的背包,商品为数字,这些数字能不能把这个背包装满

(2)一个数字只有一个维度,即表示重量,也表示价值,且重量等于价值

(3)商品是数字,重量和对应的价值是相同的,如果最大价值是 sum / 2,说明正好被商品装满了

⚠️ 具体的动规五部曲分析可以参考上面两题,这里只是 01 背包场景迁移的应用,不再赘述

一维 DP

java
class Solution {
    public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0) {
            return false;
        }
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 总和应该平均分
        if (sum % 2 != 0) {
            return false;
        }
        int target = sum / 2;
        int[] dp = new int[target + 1];
        // 先遍历物品(此题场景中,物品就是数字)
        for (int i = 0; i < nums.length; i++) {
            // 后遍历背包(必须倒叙遍历),此题场景中,总和就是背包
            for (int j = target; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        // 剪枝优化
        if (dp[target] == target) {
            return true;
        }
        return dp[target] == target;
    }
}

二维 DP

java
class Solution {
    public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0) {
            return false;
        }
        int n = nums.length;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 总和应该平均分
        if (sum % 2 != 0) {
            return false;
        }
        int target = sum / 2;
        int[][] dp = new int[nums.length][target + 1];
        // 初始化上边行
        for (int j = nums[0]; j <= target; j++) {
            dp[0][j] = nums[0];
        }

        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= target; j++) {
                if (j < nums[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
                }
            }
        }

        // for(int x : dp){
        //     System.out.print(x + ",");
        // }
        // System.out.print("    "+i+" row"+"\n");
        return dp[n - 1][target] == target;

        /*
            0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 6,
            0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 11,
            0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11
        */
    }
}