【算法】一文带你搞懂0-1背包问题!(实战篇)

news2024/9/22 13:33:50

在【算法】一文带你搞懂0-1背包问题!(理论篇)中,讲解了纯0-1背包问题及其原理,本篇文章中进入LeetCode中0-1背包问题应用的实战环节,主要难点其实在于看出是0-1背包问题、转换成0-1背包问题。

416. 分割等和子集

题目链接

思路:

这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

要注意题目描述中商品是不是可以重复放入。

即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。

要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。

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

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

以上分析完,我们就可以套用01背包,来解决这个问题了。

动规五部曲分析如下:

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

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

本题中每一个元素的数值既是重量,也是价值。

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

那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了

背包可能会有装不满的时候,拿输入数组 [1, 5, 11, 5]举例, dp[7] 只能等于 6,因为只能放进 1 和 5。

而dp[6] 就可以等于6了,放进1和5,那么dp[6] == 6,说明背包装满了。

2. 确定递推公式

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

本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]

所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

3. dp数组如何初始化

在01背包,一维dp如何初始化,已经讲过,

从dp[j]的定义来看,首先dp[0]一定是0。

如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了

本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。

代码如下:

// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
// 也可以用target为背包大小
vector<int> dp(10001, 0);

4. 确定遍历顺序

在理论篇中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

代码如下:

// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
    for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
        dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

5. 举例推导dp数组

dp[j]的数值一定是小于等于j的。

如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。

用输入[1,5,11,5] 为例,如图:

416.分割等和子集2

最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。

题解:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (vector<int>::iterator it = nums.begin(); it != nums.end(); it++) {
            sum += *it;
        }
        if (sum % 2) return false;
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < nums.size(); i++) {
            for (int j = target; j >= nums[i]; j--) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        if (dp[target] == target) return true;
        return false;
    }
};

时间复杂度:O(n^2)

空间复杂度:O(n)

1049. 最后一块石头的重量Ⅱ

题目链接

思路:

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小这样就化解成01背包问题了

本题物品的重量为stones[i],物品的价值也为stones[i]。

对应着01背包里的物品重量weight[i]和 物品价值value[i]。

动规五步曲:

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

dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

2. 确定递推公式

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

本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

3. dp数组如何初始化

既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。

而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。

当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。

接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

代码为:

vector<int> dp(15001, 0);

4. 确定遍历顺序

在理论篇中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

代码如下:

for (int i = 0; i < stones.size(); i++) { // 遍历物品
    for (int j = target; j >= stones[i]; j--) { // 遍历背包
        dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
    }
}

5. 举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1) / 2 = 4 ,dp数组状态图如下:

1049.最后一块石头的重量II

最后dp[target]里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

题解:

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for (int i = 0; i < stones.size(); i++) {
            sum += stones[i];
        }
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < stones.size(); i++) {
            for (int j = target; j >= stones[i]; j--) {
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
};

494. 目标和

题目链接

思路:

这道题目乍一看和动态规划背包之类的没啥关系。

本题要如何使表达式结果为target,既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的,所以right = sum - left。

所以由left - (sum - left) = target 推导出 left = (target + sum)/2 。

target是固定的,sum是固定的,left就可以求出来。

此时问题就转化为,装满容量为left的背包,有几种方法

这里的left,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

(C++代码中,输入的S 就是题目描述的 target)
if ((S + sum) % 2 == 1) return 0; // 此时没有方案

同时如果 S的绝对值已经大于sum,那么也是没有方案的。

(C++代码中,输入的S 就是题目描述的 target)
if (abs(S) > sum) return 0; // 此时没有方案

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

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

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

2. 确定递推公式

有哪些来源可以推出dp[j]呢?

只要知道nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种

dp[j] += dp[j - nums[i]]

这个公式在后面在讲解背包解决排列组合问题的时候还会用到。

3. dp数组如何初始化

从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看应该等于多少。

如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。

如果是数组[0,0,0,0,0] target = 0 呢。

其实此时最终的dp[0] = 32,也就是这五个零 子集的所有组合情况,但此dp[0]非彼dp[0],dp[0]能算出32,其基础是因为dp[0] = 1累加起来的。

dp[j]其他下标对应的数值也应该初始化为0,从递推公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

4. 确定遍历顺序

在理论篇中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

5. 举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:

题解:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (auto i : nums) {
            sum += i;
        }
        if (abs(target) > sum) return 0;
        if ((sum + target) % 2) return 0;
        int bagSize = (sum + target) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量
  • 空间复杂度:O(m),m为背包容量

474. 一和零

题目链接​​​​​​​

思路:

本题有点绕,是多重背包问题,还是0-1背包问题呢?

多重背包是每个物品,数量不同的情况。

但本题中strs 数组里的元素就是物品,每个物品都是一个!

而m和n相当于是一个背包,两个维度的背包。

理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。

但本题其实是01背包问题!

只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

动规五部曲:

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

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

2. 确定递推公式

dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。

所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

3. dp数组如何初始化

在理论篇中已经讲解了,01背包的dp数组初始化为0就可以。

因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

4. 确定遍历顺序

在理论篇中,讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。

代码如下:

for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
        for (int j = n; j >= oneNum; j--) {
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}

那遍历背包容量的两层for循环先后循序有没有什么讲究?

没讲究,都是物品重量的一个维度,先遍历哪个都行。

5. 举例推导dp数组

以输入:["10","0001","111001","1","0"],m = 3,n = 3为例

最后dp数组的状态如下所示:

474.一和零

题解:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
        for (string str : strs) {
            int oneNum = 0;
            int zeroNum = 0;
            for (char num : str) {
                if (num == '1') oneNum++;
                else zeroNum++;
            }

            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};
  • 时间复杂度: O(kmn),k 为strs的长度
  • 空间复杂度: O(mn)

总结

此时我们讲解了0-1背包的多种应用,

  • 纯0-1背包是求 给定背包容量,装满背包的最大价值是多少。
  • 416. 分割等和子集是求 给定背包容量,能不能装满这个背包。
  • 1049. 最后一块石头的重量Ⅱ是求 给定背包容量,尽可能装,最多能装多少。
  • 494. 目标和是求 给定背包容量,装满背包有多少种方法。
  • 474. 一和零是求 给定背包容量,装满背包最多有多少个物品。

这些题目都是 0-1背包不同维度上的应用,可以细心体会。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1964665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

动态卷积(轻量级卷积)替代多头自注意力

动态卷积&#xff0c;它比自注意力更简单、更有效。我们仅基于当前时间步长预测单独的卷积核&#xff0c;以确定上下文元素的重要性。这种方法所需的操作数量随输入长度呈线性增长&#xff0c;而自注意力是二次的。在大规模机器翻译、语言建模和抽象摘要上的实验表明&#xff0…

【论文阅读笔记 + 思考 + 总结】MoMask: Generative Masked Modeling of 3D Human Motions

创新点&#xff1a; VQ-VAE &#x1f449; Residual VQ-VAE&#xff0c;对每个 motion sequence 输出一组 base motion tokens 和 v 组 residual motion tokensbidirectional 的 Masked transformer 用来生成 base motion tokensResidual Transformer 对 residual motion toke…

机器学习 | 分类算法原理——似然函数

Hi&#xff0c;大家好&#xff0c;我是半亩花海。接着上次的逻辑回归继续更新《白话机器学习的数学》这本书的学习笔记&#xff0c;在此分享似然函数这一分类算法原理。本章的分类算法原理基于《基于图像大小进行分类》项目&#xff0c;欢迎大家交流学习&#xff01; 目录 一、…

个性化你的生产力工具:待办事项App定制指南

国内外主流的10款待办事项软件对比&#xff1a;PingCode、Worktile、滴答清单、番茄ToDo、Teambition、Todoist、Microsoft To Do、TickTick、Any.do、Trello。 在寻找合适的待办事项软件时&#xff0c;你是否感到选择众多、难以决断&#xff1f;一个好的待办事项工具可以大大提…

stl-algorithm【1】

#include《algorithm》 交换两数swap&#xff08;x&#xff0c;y&#xff09; 不只可以交换两个“数”&#xff08;数据类型&#xff09; 翻转【借助迭代器】reverse(it1,it2) 仍是左闭右开

国产开源夜莺部署

使用二进制方式部署夜莺 - 快猫星云 (flashcat.cloud) # install mysql yum -y install mariadb* systemctl enable mariadb systemctl restart mariadb mysql -e "SET PASSWORD FOR rootlocalhost PASSWORD(1234);"# install redis yum install -y redis systemctl…

navicat 17 下载安装

百度网盘 通过网盘分享的文件&#xff1a;Navicat17 链接: https://pan.baidu.com/s/1nFFQzWhjxRUM_X6bVlWNGw?pwd8888 提取码: 8888 1.双击运行安装包 2.点击下一步 2.勾选我同意&#xff0c;点击下一步 3.自定义安装路径&#xff0c;点击下一步 4.注意勾选桌面快捷方式&a…

编程新手指南:从入门到精通

编程小白如何成为大神&#xff1f;大学新生的最佳入门攻略 编程已成为当代大学生的必备技能&#xff0c;但面对众多编程语言和学习资源&#xff0c;新生们常常感到迷茫。如何选择适合自己的编程语言&#xff1f;如何制定有效的学习计划&#xff1f;如何避免常见的学习陷阱&…

基于YOLOv8的高压输电线路异物检测系统

基于YOLOv8的高压输电线路异物检测系统 (价格88) 包含 【“鸟窝”&#xff0c;“风筝”&#xff0c;“气球”&#xff0c;“垃圾”】 4个类 通过PYQT构建UI界面&#xff0c;包含图片检测&#xff0c;视频检测&#xff0c;摄像头实时检测。 &#xff08;该系统可以根据数…

众人帮蚂蚁帮任务平台修复版源码,含搭建教程。

全修复运营版本的任务平台&#xff0c;支持垂直领域细分&#xff0c;定向导流&#xff0c;带有排行榜功能&#xff0c;任务发布上传审核&#xff0c;用户信用等级&#xff0c;充值接口等等均完美可用。支付对接Z支付免签接口&#xff0c;环境配置及安装教程都已经打包。 搭建环…

ARM学习(31)编译器对overlay方式的支持

ARM学习&#xff08;31&#xff09;编译器对overlay方式的支持 1、overlay介绍 overlay&#xff1a;重叠得意思&#xff0c;就是可以重复利用得空间&#xff0c;一般在内存上使用这种空间。比如以Windows操作系统为例&#xff0c;其存储空间&#xff08;ROM/FLASH&#xff09;…

springboot垂钓服务系统-计算机毕业设计源码17434

摘要 本文旨在针对垂钓爱好者的需求&#xff0c;基于微信小程序平台&#xff0c;设计并实现一套垂钓服务系统。首先&#xff0c;通过对用户需求进行调研和分析&#xff0c;确定了系统的基本功能模块&#xff0c;包括垂钓点信息展示、用户预约和支付、钓具租赁信息等。接着&…

WebView加载数据的几种方式

之前客户端加载H5时遇到了一些问题&#xff0c;我为了方便解决问题&#xff0c;所以将对应场景复刻到了Demo中&#xff0c;从之前的网络加载模拟为了本地加载Html的方式&#xff0c;但是没想到无意被一个基础知识点卡了一些时间&#xff0c;翻看往昔笔记发现未曾记录这种基础场…

【MATLAB源码】机器视觉与图像识别技术(7)续---BP神经网络

系列文章目录在最后面&#xff0c;各位同仁感兴趣可以看看&#xff01; BP神经网络 第一节、BP网络定义第二节、BP网络结构及其特点第三节、信息传播方式 信息的正向传播&#xff1a;实质是计算网络的输出误差的反向传播&#xff1a;实质是学习过程第四节、 BP网络的算法流程…

python:plotly 网页交互式数据可视化工具

pip install plotly plotly-5.22.0-py3-none-any.whl pip install plotly_express 包含&#xff1a;GDP数据、餐厅的订单流水数据、鸢尾花 Iris数据集 等等 pip show plotly Name: plotly Version: 5.22.0 Summary: An open-source, interactive data visualization librar…

每日OJ_牛客HJ60 查找组成一个偶数最接近的两个素数

目录 牛客HJ60 查找组成一个偶数最接近的两个素数 解析代码 牛客HJ60 查找组成一个偶数最接近的两个素数 查找组成一个偶数最接近的两个素数_牛客题霸_牛客网 解析代码 首先需要判断素数&#xff0c;素数表示除过1和本身&#xff0c;不能被其它数整除。通过循环遍历来判断一…

飞致云开源社区月度动态报告(2024年7月)

自2023年6月起&#xff0c;中国领先的开源软件公司FIT2CLOUD飞致云以月度为单位发布《飞致云开源社区月度动态报告》&#xff0c;旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况&#xff0c;以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源大屏…

pycharm怎么使用Anaconda和配置

打开Anaconda Prompt 要删除 Conda 环境 yolov5sconda&#xff0c;你可以使用以下命令&#xff1a; conda remove --name yolov5sconda --all这个命令会删除名为 yolov5sconda 的整个环境&#xff0c;包括其中安装的所有包和依赖项。请在命令提示符或终端中运行此命令。执行此…

Java线程池的设计与使用

Java线程池的设计与使用 多线程情景引入 情景分析 请求积压的情况 系统资源受限: 当大量用户请求同时到来时&#xff0c;服务器受限于内存、CPU、和网络带宽等资源&#xff0c;导致用户长时间等待。后端处理能力限制: 如频率限制措施&#xff08;每秒或每几秒的访问限制&…

嵌入式day15

数组指针 能够指向整个数组 一维数组&#xff1a; &a&#xff0c;考察a的数据类型 int&#xff08;*p&#xff09;[10]&#xff1a;表示一个指向长度为10的一维整型数组的指针 二维数组&#xff1a; 指向函数的指针 函数的函数名&#xff0c;即为函数的入口地址&#x…