LeetCode刷题day55

本文通过三个经典的动态规划问题——494.目标和、474.一和零以及518.零钱兑换II,解析动态规划在解决组合计数问题上的应用。对于每个问题,分析了思路,给出了递推公式,并展示了参考代码。这些题目涉及将整数数组构造为特定和的表达式,以及在限制条件下的子集选择和硬币组合问题,均通过动态规划求解组合的最优解。
摘要由CSDN通过智能技术生成


494. 目标和

题目描述

给你一个整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1
思路分析

表达式和为target,可以看成left-right=target.即 left-(sum-left) = target=>left = (sum+target)/2

所以题目就可以转化为在集合nums中找出和为left的组合

如何转化为0-1背包问题呢。

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = S ==> x = (S + sum) / 2

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

但是此时需要注意一点:如果(S+sum) % 2 == 1,则是无解的,说明无法找到满足的.因为背包容量都是整数.

另外如果abs(target) > sum,则也无解.因为所有的数同号也无法达到target.

if(abs(target) > sum) {
    return 0;//没有方案 
}
if((sum+target) % 2==1) {//没有方案 
    return 0;
}

动规三部曲

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

dp[j]:填满j这么大容积的背包,有dp[j]种方法

这个题也可以使用二维dp数组,dp[i] [j] = 使用下标为[0,i]的nums[i]能够凑满j这么大容积的背包需要dp[i] [j]种方法.

  1. 确定递推公式

dp[j] = dp[j] + dp[j-nums[i]] ==>dp[j] 等于不使用当前物体(数)的情况+使用当前物体(数)的情况

  1. dp数组初始化

dp[0] = 1,装满容量为0的背包,只有一种方法,就是装0件物品.

  1. 确定遍历顺序

根据01背包问题,一维dp数组遍历,物品放在外层循环,背包容量放在内层倒序循环.

5.举例推导dp数组

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

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

dp数组状态变化如下:

image-20220322162618072

参考代码
#include<bits/stdc++.h>
using namespace std;
//打印dp数组
print(vector<int>& dp){
	for(int i = 0;i < dp.size(); i++){
		printf("%3d",dp[i]);
	}
	cout<<endl;
}
int findTargetSumWays(vector<int>& nums, int target) {

	int sum = 0;
	for(int i = 0; i < nums.size(); i++) {
		sum += nums[i];
	}
	if(abs(target) > sum) {
		return 0;//没有方案 
	}
	//left - (sum - left) = target==>left = (sum+target) / 2
	if((sum+target) % 2==1) {//没有方案 
		return 0;
	}
	int bageSize = (sum+target) / 2;
	vector<int> dp(bageSize+1,0);
	dp[0] = 1;
	for(int i = 0; i < nums.size(); i++) {
		for(int j =bageSize;  j>=nums[i]; j--) {
			dp[j] = dp[j]+dp[j-nums[i]];//当前物体放的情况d[j-nums[i]]+当前数字不放的情况d[j]便是总情况 
		}
		//cout<<"dp["<<i<<"]";
		//print(dp);
	}
	return dp[bageSize];
}

int main()
{
	vector<int> nums = {1,1,1,1,1};
	int target = 3;
	findTargetSumWays(nums,target);
	return 0;
 } 

474. 一和零

题目描述

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"}{"10","1","0"}{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2
思路分析

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

而m 和 n相当于是一个背包,两个维度的背包。所以本题依旧是一个01背包问题

动归五部曲

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

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

  1. 确定递推公式

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背包! 只不过物品的重量有了两个维度而已。

  1. dp数组初始化

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

  1. dp数组遍历

我们之前讲过背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

有同学可能想,那个遍历背包容量的两层for循环先后循序有没有什么讲究?

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

  1. 举例推导过程

以[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例

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

474.一和零

参考代码
#include<bits/stdc++.h>
using namespace std;
//打印dp
void print(vector<vector<int>>& dp,int& m,int& n){
	for(int i = 0;i <= m;i++){
		cout<<"dp["<<i<<"]";
		for(int j = 0; j<=n;j++){
			cout<<dp[i][j]<<" ";
		}
		cout<<endl;
	}
	cout<<"-------------------"<<endl;
	
}
int findMaxForm(vector<string>& strs, int m, int n) {//m:0个数 n:1个数 
	vector<vector<int>> dp(m+1,vector<int>(n+1,0));//定义dp   dp[i][j]:i个0,j个1的最大子集元素个数. 
	print(dp,m,n);
	for(string str : strs){//外层遍历物体 
		int oneNum = 0;
		int zeroNum = 0;
		for(char ch : str) {//统计当前字符串(物体)的zero和one个数 
			if(ch=='0'){
				zeroNum++;
			}else{
				oneNum++;
			}
		}
		
		for(int i = m; i >= zeroNum;i--) {//遍历背包体积(二维的:字符串中m和n个数) 
			for(int j = n; j >= oneNum;j--){
				dp[i][j] = max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
			}
		}
		//print(dp,m,n);
	}
	return dp[m][n];
}

int main()
{
	vector<string> strs = {"10", "0001", "111001", "1", "0"};
	int m = 5;
	int n = 3;
	findMaxForm(strs,m,n);
	return 0;
}

518. 零钱兑换 II

题目描述

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3

示例 3:

输入:amount = 10, coins = [10] 
输出:1
思路分析

这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。

但本题和纯完全背包不一样,纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!

注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?

例如示例一:

5 = 2 + 2 + 1

5 = 2 + 1 + 2

这是一种组合,都是 2 2 1。

如果问的是排列数,那么上面就是两种排列了。

组合不强调元素之间的顺序,排列强调元素之间的顺序

动规五步曲

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

dp[j]:凑成总金额j的货币组合数为dp[j]

  1. 确定递推公式

这个和 494.目标和相似,都是求组合,所以递推公式: dp[j] = dp[j] + dp[j - coins[i]];

  1. dp数组如何初始化

dp[0] = 1 凑成总金额0的货币组合数为1

  1. 确定遍历顺序

本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?

前面遍历得到的是组合数,后面遍历得到的是排列数

  1. 举例推导dp数组

输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:

518.零钱兑换II

参考代码
#include<bits/stdc++.h>
using namespace std;

int change(int amount, vector<int>& coins) {
	vector<int> dp(amount+1,0);
	dp[0] = 1;
	for(int i = 0; i < coins.size(); i++) {
		for(int j = coins[i]; j <=amount; j++) {
			dp[j]  += dp[j-coins[i]];
		}
	}
	return dp[amount];
}

如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的大李子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值