【LeetCode动态规划#07】01背包问题一维写法(状态压缩)实战,其二(目标和、零一和)
目标和(放满背包的方法有几种)
难度:中等
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
- 输入:nums: [1, 1, 1, 1, 1], S: 3
- 输出:5
解释:
- -1+1+1+1+1 = 3
- +1-1+1+1+1 = 3
- +1+1-1+1+1 = 3
- +1+1+1-1+1 = 3
- +1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
提示:
- 数组非空,且长度不会超过 20 。
- 初始的数组的和不会超过 1000 。
- 保证返回的最终结果能被 32 位整数存下。
思路
前者是将集合分成两个相等的子集,后者是将集合分成两个尽可能相等的子集
共同点是什么?都是先把问题转换成将当前题目给的数组集合一分为二
因此,本题要往01背包问题上靠,也要先转换为一个将集合划分成两部分的问题
怎么转呢?
题目要在一个非负整数数组nums中的任意一个整数前加正负号,实现所有元素相加后等于目标值target,最后统计一共有多少种相加的方法(即一共有多少种放正负号的方法)
那么我们就可以把数组元素分为两个子集,一个子集中的元素前面都加正号,另一个子集则都加负号
这不就有两个子集了嘛(md这正常人能想到?)
设加负号的子集为 negativeSign, 加正号的子集为 plusSign
注意,此时我们讨论的两个子集都是已经通过dp划分好的,里面不带正负号
那么,两个子集的元素相加应该等于非负整数数组nums的元素之和sum
两个子集的元素相减应该等于目标值target
抽象为公式如下:
① plusSign + negativeSign = sum;
② plusSign - negativeSign = target;
合并一下可以得到: plusSign = (sum + target) / 2;
在01背包问题中,只需用一个子集充当背包即可,因此这里可以选择 加正号的子集plusSign 作为背包
以示例 nums: [1, 1, 1, 1, 1], target: 3 来说
转换为背包问题后,背包的容量为 (5+3)/2 = 4 ,所谓的"物品"就是nums数组中的元素
当然,这里用除法就会涉及不能整除的情况
若不能整除,代表该数组nums找不到能够组合成目标值target的方法,直接return 0
此时,问题就转换成了:使用非负整数数组nums中的元素装满背包有几种方法?
(注意,本题要找的是有几种装满背包的方法)
区分一下之前做的背包问题的目标
单纯的01背包问题:装满某个背包时,物品的最大价值;
分割等和子集:往背包放入物品后,背包的最大重量(换句话说就是能不能用物品把背包装满,能就return true)
最后一块石头:往背包装物品,能装下的最大价值(能装多少装多少)
五步走
1、确定dp数组含义
老规矩,先回顾一下经典01背包问题的dp数组定义
dp[j]: 背包容量为j时,装满背包的最大价值为dp[j]
转换一下,本题的dp数组含义可以定义如下:使用所给的所有物品,装满容量为j的背包有dp[j]种方法
2、确定递推公式
怎么推导出dp[j]呢?
这里要通过"物品"的角度来想,例如,当前如果有一个物品(nums中的一个元素)要放入背包,假设背包容量是5(与示例保持一致, nums元素为5个1)
那么这个物品一定会放入背包中,因此也一定会占用掉背包的一部分容量,占用掉的容量是 j - nums[i]
根据dp数组的含义,在有一个物品确定放入的情况下,dp[5]就会转变为dp[5 - 1],也就是dp[4]
有点乱?那再用直接一点的话描述一下上面发生的事情:
1、最开始,背包容量是5,此时按dp数组的定义,装满该背包会有dp[5]种方法;(因为还有五个容量,你随便怎么装,所有的方法就表示为dp[5])
2、当已经有一个"物品"确定放入容量为5的背包时,背包容量缩减为4(不管你开始没开始往里面放,先给你预留了),还是按定义,此时装满该背包会有dp[5-nums[0]]种方法(即dp[5 - 1] = dp[4])
3、当已经有两个"物品"确定放入容量为5的背包时,背包容量缩减为3(不管你开始没开始往里面放,先给你预留了)
按定义,此时装满该背包会有dp[5-nums[0]-nums[1]]种方法(即dp[5-1-1] = dp[3])
后面的情况以此类推
注意,一定要结合dp数组的定义
这里的
dp[某某]
指的是在"某某"容量下放入物品时,所有方法的集合
简单概括一下,例如:dp[j],j为5,
- 已经有0个的话,有 dp[5]种方法 凑成 容量为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[5]怎么求)
就把所有的 dp[j - nums[i]] 累加起来即可
也就是dp[5] = dp[4] + dp[3] + dp[2] + dp[1] + dp[0]
总结为递推公式就是:
dp[j] += dp[j - nums[i]]
该递推公式很重要,在用背包解决排列组合问题时还能用
3、初始化dp数组
一切结合dp数组的含义:装满容量为j的背包有dp[j]种方法
来看dp[0]的情况
dp[0]即被包容量为0时装满背包的方法数量,这里又可以细分为两种情况:要装的物品重量不为0和重量为0
如果物品重量不为0
那么实际上我们是无法将该物品装入容量为0的背包中的,那么是不是就意味着在该种情况下,dp[0] = 0 了呢?
我认为也不是,因为背包容量为0是一种特殊情况,此时不论你往不往里面放东西(或者放不放得进),背包都已经处于放满状态,因此
dp[0]应该是默认有一种方式装满的,那就是什么也不放
由上述分析可知,dp[0]应该初始化为1,即dp[0] = 1;
如果物品重量为0
接着上面的分析,若背包中物品重量为0, 假设:[0,0,0,0,0], target = 0
那这些0就还是可以往背包里面放的(放不放都一样),并且不同的物品(重量为0)往背包放就算是一种不同的放法
因此,dp[0]就是这五个重量为0的物品不断组合放入背包内的组合方式的种类数量
大概有32种,于是dp[0] = 32
以上分析是建立在认同dp[0]应该初始化为1的情况下成立的(因为其他情况都是基于的dp[0] = 1推导出来的)
说了这么多,无非就是像说明清楚dp[0]初始化为1的可行性,记住本题 dp[0] = 1 就行
4、确定遍历顺序
仍然遵循先遍历物品(nums),后遍历背包容量的顺序,且背包容量的遍历方向是倒序的
这里在逻辑上与之前涉及重量的问题不太一样,下面手动推导一遍
(输入:nums: [1, 1, 1, 1, 1], target: 3)
如图所示为遍历过程
注意dp数组的含义,使用所给的所有物品,装满容量为j的背包有dp[j]种方法
这里有两个关键点:1、需要使用所有的物品;2、装满
- 不论遍历的过程如何,最终我们需要求的是把所有物品放入容量为j的背包的方法,因为遍历物品的过程是一个一个遍历的,所以放入所有物品的方法种类也是由最开始的情况不断累加到最后才能得到的
- 一定要能够装满当前容量才算是一种方法,比如在容量为4的情况下,目前遍历到第一个物品(也就是只有一个物品),无论如何是放不满4个容量的,因此就算能够放入当前的一个物品,也不能算一种方法
说一下"装满方法"是怎样计算的
因为01背包问题中,每个物品只能使用一次,那么在当前物品能够装满当前容量的前提下,使用相同物品以不同顺序放入背包的方法应该视作同一种方法
什么意思呢?就是说假设现在遍历到了nums[2],我们手头上有3个物品,此时容量遍历到2的话,理论上我们有以下放入的方式:
nums[0] nums[1] nums[2]
nums[0] nums[2] nums[1]
nums[1] nums[0] nums[2]
nums[1] nums[2] nums[0]
nums[2] nums[1] nums[0]
nums[2] nums[0] nums[1]
其中,有一半的放入方式重复使用了物品,因此是不计入方法种类
代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//计算数组元素之和
int sum = 0;
for(auto num : nums) sum += num;
//判断两种无解的情况
//1、所给的target已经大于sum
//2、(sum + target) / 2不能整除,即计算背包容量时不能整除
if(abs(target) > sum) return 0;//取绝对值
if((sum + target) % 2 != 0) return 0;
//计算背包容量
int bagSize = (sum + target) / 2;
//定义dp数组
vector<int> dp(bagSize + 1, 0);
//初始化dp数组
dp[0] = 1;
//遍历dp数组
for(int i = 0; i < nums.size(); ++i){//遍历物品num
// 如果当前背包容量小于物品重量,换一个物品继续遍历容量(所以第二层循环的条件是j >= nums[i])
// 每一个元素一定是不可重复放入,所以从大到小遍历
for(int j = bagSize; j >= nums[i]; --j){//遍历背包容量
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
零一和
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
- 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
- 输出:4
- 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
- 输入:strs = ["10", "0", "1"], m = 1, n = 1
- 输出:2
- 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
- 1 <= strs.length <= 600
- 1 <= strs[i].length <= 100
- strs[i] 仅由 '0' 和 '1' 组成
- 1 <= m, n <= 100
思路
这题有点绕的其实,刚上手的话很容易将m、n看成两个容器
其实这样想是错误的,本题实质上还是01背包问题,只不过这个背包有"两个维度"
什么意思呢?我解释一下
先来说题意吧,题目要求是:m代表字符串中0的个数,n代表字符串中1的个数
然后,题目规定一组m、n,要求从字符串数组strs中找到能够满足m、n的最大子集,并返回该子集的大小
拿示例1来看,strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
最多有 5 个 0 和 3 个 1 的strs中的最大子集是 {"10(m=1,n=1)","0001(m=3,n=1)","1","0"}
该子集的大小是4,因此结果值返回的是4
看出来了吗?其实题目规定的"m = 5, n = 3"就是在设置背包的容量
确定了背包就好办了,下面就套五部曲解决问题
五步走
1、确定dp数组的含义
注意,这里题目是要求最大子集个数,也就是背包中物品的个数
那么dp数组可以定义如下
dp[i][j]
: 在背包"容量"(这里的容量指的是strs中一个子字符串中0、1的个数)为i、j时能够装下物品的最大个数
虽然这里需要把dp数组设置成二维的,但其实本质上和之前的一维01背包问题没有区别
如果不好理解的话还是可以把dp数组看成是个一维的,例如dp[G],G是背包的容量,只不过G由两个部分组成,一部分是i,一部分是j
(这里也可以把G想象成strs中一个子字符串,例如"10")
2、确定递推公式
因为本题只是给放入背包的物品多增加了一个维度,所以递推公式可以参考标准01背包问题的递推公式(一维)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
那么对照着本题的递推公式就是
dp[i][j] = max(dp[i][j], dp[i - zeroNums][j - oneNums] + 1(物品数量));
解释一下,
当我们确定放入一个新的子字符串(假设是"10"),那么此时背包容量(i、j)就要对应减少子字符串中0的个数(zeroNums)、子字符串中1的个数(oneNums),(zeroNums, oneNums)即为该子字符串(物品)的重量
当然,此时包内的物品数量要加1(类比之前的递推公式,背包总价增加)
推导dp[i][j]
时,有两种情况:放东西和不放东西
这两者取能够使物品个数最大的那种情况就行,即上面的递推公式
3、初始化dp数组
dp[0][0]
时要初始化为0,即容量为(0,0)时,一个也装不下
然后其余的部分也要初始化为0,为了防止递推值被初始值覆盖(详见)
4、确定遍历顺序
和普通的01背包的一维解法一样,这里也是先遍历物品(子字符串),然后倒序遍历背包容量(i,j)(对应(zeroNums, oneNums))
核心代码如下,结合上面的图来解释
for (string str : strs) { // 遍历物品,即子字符串,例如"10"
int oneNum = 0, zeroNum = 0;
for (char c : str) {//统计字符串中0、1的数量
if (c == '0') zeroNum++;
else oneNum++;
}//以下是遍历背包容量(i,j)
//如果当前背包容量(i,j)小于物品重量(zeroNum,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);
}
}
}
对应到图中就是,我们第一轮遍历是从最下面的一行开始的,从右往左倒序遍历
根据递推公式有:dp[3][3] = max(dp[3][3], dp[3 - 1][3 - 1] + 1);
(此时子字符串是"10")
因为dp[3][3]
的初始值为0,所以dp[2][2]+1
肯定要大一些,故dp[3][3] = dp[3 - 1][3 - 1] + 1;
那dp[2][2]
又是多少?还是用递推公式去算,得到dp[2][2] = max(dp[2][2], dp[2 - 1][2 - 1] + 1);
,取决于dp[1][1]
同理,dp[1][1] = max(dp[1][1], dp[1 - 1][1 - 1] + 1);
,最后可以算出dp[1][1]=dp[0][0]+1=1
所以,
dp[2][2]=1+1=2
dp[3][3]=2+1=3
此时可以得到图中dp[3][3]
处的递推值,同理可以把整个dp数组的递推值计算出来,结果如上图所示
代码
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//定义dp数组,并初始化
//二维数组,行列分别是0、1(可以颠倒)
vector<vector<int>> dp(m + 1, vector(n + 1, 0));
//遍历dp数组
for(string str : strs){//遍历物品(子字符串)
int zeroNums = 0, oneNums = 0;
//统计字符串中的01个数
for(char c : str){
if(c == '0'){
zeroNums++;
}else oneNums++;
}
for(int i = m; i >= zeroNums; --i){//遍历背包容量(i,j),倒序
for(int j = n; j >= oneNums; --j){//i\j遍历顺序可以更换,因为本质上还是容量
dp[i][j] = max(dp[i][j], dp[i - zeroNums][j - oneNums] + 1);
}
}
}
return dp[m][n];
}
};
【LeetCode动态规划#07】01背包问题一维写法(状态压缩)实战,其二(目标和、零一和)的更多相关文章
- 动态规划入门-01背包问题 - poj3624
2017-08-12 18:50:13 writer:pprp 对于最基础的动态规划01背包问题,都花了我好长时间去理解: poj3624是一个最基本的01背包问题: 题意:给你N个物品,给你一个容量 ...
- 动态规划专题 01背包问题详解 HDU 2546 饭卡
我以此题为例,详细分析01背包问题,希望该题能够为大家对01背包问题的理解有所帮助,对这篇博文有什么问题可以向我提问,一同进步^_^ 饭卡 Time Limit: 5000/1000 MS (Java ...
- 动态规划(DP),0-1背包问题
题目链接:http://poj.org/problem?id=3624 1.p[i][j]表示,背包容量为j,从i,i+1,i+2,...,n的最优解. 2.递推公式 p[i][j]=max(p[i+ ...
- nyoj 49-开心的小明(动态规划, 0-1背包问题)
49-开心的小明 内存限制:64MB 时间限制:1000ms Special Judge: No accepted:7 submit:11 题目描述: 小明今天很开心,家里购置的新房就要领钥匙了,新房 ...
- C++动态规划求解0-1背包问题
问题描述: 给定n种物品和一背包.物品i的重量是wi,其价值为vi,背包的容量为C.问:应该如何选择装入背包的物品,是的装入背包中物品的总价值最大? 细节须知: 暂无. 算法原理: a.最优子结构性质 ...
- DP动态规划之01背包问题
目录 问题描述 问题分析 问题求解 Java代码实现 优化方向一:时间方面:因为是j是整数是跳跃式的,可以选择性的填表. 思考二:处理j(背包容量),w(重量)不为整数的时候,因为j不为整数了,它就没 ...
- 动态规划——用二进制表示集合的状态压缩DP
动态规划当中有非常常见的一个分支--状态压缩动态规划,很多人对于状态压缩畏惧如虎,但其实并没有那么难,希望这文章能带你们学到这个经典的应用. 二进制表示状态 在讲解多重背包问题的时候,我们曾经讲过二进 ...
- ACM:动态规划,01背包问题
题目: 有n件物品和一个容量为C的背包.(每种物品均仅仅有一件)第i件物品的体积是v[i],重量是w[i].选一些物品装到这个背包中,使得背包内物品在整体积不超过C的前提下重量尽量大. 解法:两种思路 ...
- 0-1背包问题——动态规划求解【Python】
动态规划求解0-1背包问题: 问题:背包大小 w,物品个数 n,每个物品的重量与价值分别对应 w[i] 与 v[i],求放入背包中物品的总价值最大. 动态规划核心:计算并存储小问题的最优解,并将这些最 ...
- 转 状态压缩DP
引入 首先来说说“状态压缩动态规划”这个名称,顾名思义,状态压缩动态规划这个算法包括两个特点,第一是“状态压缩”,第二是“动态规划”. 状态压缩: 从状态压缩的特点来看,这个算法适用的题目符合以下的条 ...
随机推荐
- 容器数据库(CDB)和传统的非容器数据库的区别
传统的非容器数据库在系统元数据和实例资源没有进行任何共享.容器数据库把 Oracle 提供的元数据.后台进程以及内存结构进行共享,把每个部门数据库的存储结构以 PDB 的形式独立出来,从而实现了系统资 ...
- Javaheima13
Java Stream流 salary 薪水 bonus 奖金 top performer 优秀员工 punish 处罚 1员工信息至少包含了(名称.性别.工资.奖金.处罚记录) 2开发一部有4个员工 ...
- 实验1 C语言初认识
任务1 #include<stdio.h> int main() { printf("My stuno is 202083450002\n"); printf(&quo ...
- StatefulSet 模板,更新,扩缩容,删除
概念: StatefulSet是用来管理有状态应用的工作负载API对象,kubectl 中可以简写sts ,sts每一个pod生成一个唯一的标识符,sts_name-number,number从0开始 ...
- mysql创建函数时提示1418。可选关闭二进制日志或者设置log_bin_trust_function_creators=1
报错详情如下:1418--This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration a ...
- Fiddler之常用的操作
Fiddler操作 一.首次安装 1.设置https Tools → Options → https 第一次选择安装证书,如图 2.无法正常显示https请求 重置所有证书,重置后会重新提示安装证书, ...
- Linux shell $相关的变量
代码示例1 点击查看代码 user="yaya" echo ${user} #在已定义过的变量前加$符号便可使用该变量,{}是可选的 yaya #输出结果 代码示例2 点击查看代码 ...
- 使用web client对 vcenter 进行补丁升级
使用web client对 vcenter 进行补丁升级 背景:最近VMware官网发布了最新的VMware vCenter Server 7.0 iso补丁文件,为了安全起故此对vCenter 进行 ...
- Eclipse安装和配置环境教程(图文详解)
前言 在上一篇文章中,壹哥给大家介绍了Notepad++这个更高级点的记事本,它进行Java开发相比windows自带的记事本要更方便一些.但是即便如此,用这种记事本进行Java开发效率依然很低.如果 ...
- Django笔记四之字段属性
这篇笔记介绍的 field options,也就是 字段的选项属性. 首先,关于 model,是数据库与 python 代码里的一个映射关系,每一个 model 是django.db.models.M ...