16. 目标和
例题494:
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
难点:
target是固定的,那么一定会有left组合-right组合=target。
而sum也是固定的,left+right=sum
所以推出:left-(sum-left)=target——>left=(sum+target)/2
因此,后续问题就是怎么在nums中求出和为固定left的组合。
动态规划思路
- 确定dp数组和下标含义
dp[j]表示容量为j的背包可装组合数。 - 确定递推公式
有哪些来源可以推出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]]
这个公式在后面在讲解背包解决排列组合问题的时候还会用到!
- 初始化
dp[0]=1,从递推结果反向推导 - 确定遍历顺序
一维滚动数组只有一个方向:先物品后背包,并且背包是倒序遍历 - 举例验证递推公式
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int left,sum=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
if((target+sum)<0 || target>sum) return 0;//此时无解
if((target+sum)%2==1) return 0;//此时无解
left=(sum+target)/2;
int[] dp=new int[left+1];
dp[0]=1;
for(int i=0;i<nums.length;i++){
for(int j=left;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[left];
}
}
需要注意:
①当sum+target为奇数的时候,直接返回0。
因为left+right=sum,left-right=target。所以,sum和target应该是同奇同偶的,否则矛盾。
②dp数组的含义发生了变化,不再是以前的最大容量,而是最大可能组合数。
③对应的递推公式也发生变化,同不同的二叉搜索树题目类似,dp[j]应该由dp[j-weight[i]]的和相加。
17. 一和零
例题474:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
动态规划
- 确定dp数组和下标含义
dp[i][j]表示有i个0,j个1时的子集长度。 - 确定递推公式
dp[i][j]由当前背包0、1个数减去当前str的0、1个数得到的dp数组子集个数+1
dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1) - 初始化
dp[0][0]=0 - 遍历方向
从物品再背包从左到右依次遍历 - 举例验证递推公式
代码如下:
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp=new int[m+1][n+1];
dp[0][0]=0;
for(String c:strs)
{
int zeroNum=findZeroNum(c);
int oneNum=findOneNum(c);
for(int i=m;i>=zeroNum;i--){
for(int j=n;j>=oneNum;j--){
dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
}
}
}
return dp[m][n];
}
public int findZeroNum(String str){
int count=0;
for(int i=0;i<str.length();i++){
if(str.charAt(i)=='0')
count++;
}
return count;
}
public int findOneNum(String str){
int count=0;
for(int i=0;i<str.length();i++){
if(str.charAt(i)=='1')
count++;
}
return count;
}
}
注意:遍历背包和物品时是倒序。防止重复装入。
18. 完全背包理论基础
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。
在下面的讲解中,我依然举这个例子:
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品都有无限个!
问背包能背的物品最大价值是多少?
01背包和完全背包唯一不同就是体现在遍历顺序上,所以本文就不去做动规五部曲了,我们直接针对遍历顺序经行分析!
01背包的遍历代码如下:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。
先遍历背包在遍历物品,代码如下:
// 先遍历背包,再遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
cout << endl;
}
例题:卡码网52题
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
输入:
第一行包含两个整数,N,V,分别表示研究材料的种类和行李空间
接下来包含 N 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
输出:
输出一个整数,表示最大价值。
import java.util.Scanner;
class Main{
public static void main(String[] args){
int res=comBb();
System.out.println(res);
}
public static int comBb(){
int N,V;
//System.out.println("input N and V:");
Scanner scan=new Scanner(System.in);
N=scan.nextInt();
V=scan.nextInt();
//System.out.println("input weight and values:");
int[] weight=new int[N];
int[] values=new int[N];
for(int i=0;i<N;i++){
weight[i]=scan.nextInt();
values[i]=scan.nextInt();
}
int[] dp=new int[V+1];
for(int i=0;i<N;i++){
for(int j=weight[i];j<=V;j++){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+values[i]);
}
}
return dp[V];
}
}
19. 零钱兑换||
例题518:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
因为每一种面额的硬币有无数个,对应完全背包问题。
- 确定dp数组和下标定义
dp[j]为总金额j的最多组合数。 - 确定递推公式
如果有1块钱,那么dp[j]应该由dp[j-1]种组合决定;
如果有2块钱,那么dp[j]应该由dp[j-2]种组合决定;
…
如果有j块钱,那么dp[j]应该由dp[0]种组合决定。
所以dp[j]是其前dp的和,这是一个组合问题,组合问题中dp[j]的公式如下:
dp[j]+=dp[j-weight[i]] - 初始化
dp[0]=0,由结果反推 - 确定遍历方向
完全背包的一维dp可以有两种方向:先物品后背包或者先背包后物品。只不过背包是正向遍历,保证可以取多个物品。
但在这里不行!
先看先物品后背包:
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
假设:coins[0] = 1,coins[1] = 5。
那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序,代码如下:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时dp[j]里算出来的就是排列数!
5. 举例验证递推公式
代码如下:
时间复杂度O(mn),n是coins长度,m是amount。
空间复杂度O(m)
class Solution {
public int change(int amount, int[] coins) {
int[] dp=new int[amount+1];
dp[0]=1;
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
}
注意:
- 这个题的难点不是递推公式,因为组合问题中dp[j]+=dp[j-weight[i]]碰见过。难点是遍历顺序的确定。
如果是组合问题就是先物品后背包。
如果是排列问题就是先背包后物品。
20. 动态规划第4节总结
- 目标和
难点是想到背包的容量是left,因为sum和target固定,所以left=(sum+target)/2。并且还要想到sum和target同奇同偶可以剪枝。
所以,题目就转换为了在容量为left的背包中,和为left的组合数。
在这里,碰到了组合问题的背包递推公式:dp[j]+=dp[j-weight[i]] - 一和零
一个字符串数组中的每个字符串的1、0个数,找到满足m个0,n个1的最长子集数。
dp[i][j]表示满足(小于等于)i个0和j个1的字符串个数。
dp[i][j]应该由去掉当前字符串的01个数后的个数+1,与本身的最大值决定。
所以,dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1) - 完全背包
遍历方向两个都可以,需要注意的是为了保证可以重复取物品,背包是正向遍历。
与01背包的二维数组不同就是背包的遍历方向是正序而不是倒序。
与01背包的一维数组不同的是,一维只有一个遍历方向,那就是先物品后背包,并且背包是倒序遍历。 - 零钱兑换||
完全背包中的组合问题
该题揭示了完全背包中组合问题与排列问题的遍历顺序不同。
组合问题:先物品后背包
排列问题:先背包后物品