Day42|动态规划part04: 01背包问题,你该了解这些!、滚动数组、416. 分割等和子集

01背包理论基础

其他背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。

而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。

01 背包问题描述

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

递归五部曲

示例:

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

对于背包问题,有一种写法, 是使用二维数组,即dpij 表示从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少

  1. 确定递推公式

再回顾一下dpij的含义:从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少。

那么可以有两个方向推出来dpij

  • 不放物品i:由dpi - 1j推出,即背包容量为j,里面不放物品i的最大价值,此时dpij就是dpi - 1j。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dpi - 1j - weight\[i]推出,dpi - 1j - weight\[i] 为背包容量为j - weighti的时候不放物品i的最大价值,那么dpi - 1j - weight\[i] + valuei (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dpij = max(dpi - 1j, dpi - 1j - weight\[i] + valuei);

  1. 初始化dp数组

这一步很重要:

cpp 复制代码
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
  1. 确定遍历顺序

外部循环是物品还是背包?其实都可以,但是放物品比较符合实际情况:

cpp 复制代码
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}
  • 这里要判断j和weighti,看能不能装得下,不然后面dpi - 1j - weight\[i]会报错;
  1. 打印dp数组

最终代码:

cpp 复制代码
public class BagProblem {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight .length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {                    
                  /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }

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

0-1背包滚动数组优化

上面的方法用的是二维的dp数组,其实可以将其优化为一维的dp。例子还是这个:

在使用二维数组的时候,递推公式:dpij = max(dpi - 1j, dpi - 1j - weight\[i] + valuei);

其实可以发现如果把dpi - 1那一层拷贝到dpi上,表达式完全可以是:dpij = max(dpij, dpij - weight\[i] + valuei);

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

注意的点:

  • 递推公式:
other 复制代码
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  • 遍历顺序:外层物品内层背包,背包从后往前倒序

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight0 = 1,价值value0 = 15

如果正序遍历

dp1 = dp1 - weight\[0] + value0 = 15

dp2 = dp2 - weight\[0] + value0 = 30

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

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

倒序就是先算dp2

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

dp1 = dp1 - weight\[0] + value0 = 15

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

416. 分割等和子集

本题其实就是一个变种的0-1背包问题,关键是怎么套入0-1背包:

只有确定了如下四点,才能把01背包问题套到本题上来。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

dp五部曲

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

01背包中,dpj 表示: 容量为j的背包,所背的物品价值最大可以为dpj

套到本题,dpj表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dpj

  1. 确定递推公式

value和weight都是numsi,那么递推公式就从:

dpj = max(dpj, dpj - weight\[i] + valuei);

转换成了

dpj = max(dpj, dpj - nums\[i] + numsi);

  1. 初始化

全部默认初始化为0即可。

  1. 遍历顺序

注意0-1背包一维dp,外层物品内层背包,背包从后往前倒序。

  1. 举例推导。

最终代码:

cpp 复制代码
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = Arrays.stream(nums).sum();
        if(sum % 2 != 0){
            return false;
        }
        int target = sum / 2;
        int n = nums.length;

        //抽象成0-1背包问题
        int dp[] = new int[target + 1];

        // value和weight都是nums[i]
        // dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

        for(int i = 0 ; i < n; 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;
        }

        if(dp[target] == target){
            return true;
        }
        return false;

    }
}
  • Arrays.stream(nums).sum();(好像效率不如for循环累加)
相关推荐
小小杨树17 小时前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2121 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2121 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术2 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像
Darling噜啦啦2 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
用户497863050732 天前
(一)小红的数组操作
算法·编程语言
怕浪猫2 天前
Electron 系列文章封面图
算法·架构·前端框架
徐小夕2 天前
JitWord 3.0 正式发布,高精度Word异构解析+复杂组件兼容,打造web端协同Word编辑器
前端·vue.js·算法
通信小呆呆3 天前
当算法有了“五感”:多模态数据融合如何向人体感官协同学习?
人工智能·学习·算法·机器学习·机器人