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循环累加)
相关推荐
QiLinkOS2 小时前
从技术到资产的跃迁:企业专利布局的深层逻辑
c语言·数据结构·c++·单片机·嵌入式硬件·算法·开源
aini_lovee2 小时前
FMCW雷达测速测距系统(锯齿波 + CFAR检测)
算法
qq_297574672 小时前
设计模式系列文章(基础篇第 11 篇):模板方法模式——定义算法骨架,实现代码复用与流程统一
算法·设计模式·模板方法模式
lqqjuly2 小时前
知识蒸馏:理论、算法与可运行实现
人工智能·深度学习·算法
水上冰石3 小时前
comfui的sd1.5模型,有多少采样算法,详解每一个采样算法
人工智能·算法
黎阳之光3 小时前
视频孪生+空天地水工融合,黎阳之光构建智慧水利监测新范式
大数据·人工智能·物联网·算法·安全
cheems95273 小时前
[算法手记] 贪心 爬楼梯问题
算法·贪心算法
KaMeidebaby3 小时前
卡梅德生物技术快报|酵母双杂交 cDNA 文库构建与蛋白互作筛选流程
服务器·前端·数据库·人工智能·算法
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2026.05.27 题目:3121. 统计特殊字母的数量 II
笔记·算法·leetcode
ST——Jess4 小时前
年度行业趋势研究报告:泛心理数字化赛道“流日推演”的算法困境与高保真交互范式重构
人工智能·算法·架构