给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
初步想法排序后双指针,发现不行
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()<2){
return false;
}
sort(nums.begin(),nums.end());
int left=0,right=nums.size()-1;
int left_sum=nums[left],right_sum=nums[right];
while(left<right-1){
if(left_sum>right_sum){
right--;
right_sum += nums[right];
}else{
left++;
left_sum += nums[left];
}
}
cout<<left_sum<<" "<<right_sum<<endl;
return left_sum==right_sum;
}
};//nums = [2,2,1,1]
只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯,直接上01背包吧。
确定dp数组以及下标的含义:dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。
确定递推公式: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]);
dp数组如何初始化:首先dp[0]一定是0。
class Solution {
public:
bool canPartition(vector<int>& nums) {
// int sum=0;
// vector dp(10001,0);
// for(auto i:nums){
// sum+=i;
// }
// if(sum % 2 == 1){
// return false;
// }
// int half_sum = sum/2;
// for(int i=0;i
// for(int j=half_sum;j>=nums[i];j--){
// dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
// }
// }
// if(dp[half_sum]==half_sum){
// return true;
// }
// return false;
};
不太好理解,但是可以用二维的数组,横坐标为sum/2的试探,纵坐标为元素的探索
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()<2){
return false;
}
int sum=0;
for(int i:nums){
sum+=i;
}
if(sum%2==1){
return false;
}
int target = sum/2;
vector<vector<bool>> dp(nums.size(),vector<bool>(target+1,false));
if(nums[0]<=target){
dp[0][nums[0]] = true;
}
for(int i=1;i<nums.size();i++){
for(int j=0;j<dp[0].size();j++){
dp[i][j] = dp[i-1][j];
if(nums[i]==j){
dp[i][j]=true;
continue;
}
if(nums[i]<j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
if(dp[i][target]==true){
return true;
}
}
return false;
}
};
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
本题物品的重量为stones[i],物品的价值也为stones[i]。对应着01背包里的物品重量weight[i]和 物品价值value[i]。
确定dp数组以及下标的含义:dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]。
确定递推公式:01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
dp数组如何初始化:因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。
确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
那么分成两堆石头,一堆石头的总重量是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:stones){
sum += i;
}
int target = sum / 2;
int dp[1501] = {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-dp[target]-dp[target];
}
};
时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数;空间复杂度:O(m)
首先对于背包的所有问题中,01背包是最最基础的,其他背包也是在01背包的基础上稍作变化。
确定dp数组以及下标的含义:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
确定递推公式:dp[i][j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]);
dp数组如何初始化
确定遍历顺序:01背包二维dp数组在遍历顺序上,外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!
举例:背包最大重量为4。物品为:【物品0;1;15】【物品1;3;20】【物品2;4;30】对应的dp数组的数值,如图:
给你一个非负整数数组 nums
和一个整数 target
。向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
假设加法的总和为x,那么减法对应的总和就是sum - x。所以我们要求的是 x - (sum - x) = target;x = (target + sum) / 2;此时问题就转化为,装满容量为x的背包,有几种方法。这里的x,也就是我们后面要求的背包容量。
确定dp数组以及下标的含义:dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法;其实也可以使用二维dp数组来求解本题,dp[i] [j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i] [j]种方法。
确定递推公式:只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
dp数组如何初始化:在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int i:nums){
sum+=i;
}
if(abs(target)>sum){
return 0;
}
if((target+sum) & 1){
return 0;
}
int left=(target+sum)/2;
vector<int> dp(left+1,0);
dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=left;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[left];
}
};
时间复杂度:O(n × m),n为正数个数,m为背包容量;空间复杂度:O(m),m为背包容量
class Solution {
public:
int count=0;
void track(vector<int> &nums,int target,int sum,int start){
if(start==nums.size()){
if(sum==target){
count++;
}
}else{
track(nums,target,sum+nums[start],start+1);
track(nums,target,sum-nums[start],start+1);
}
}
int findTargetSumWays(vector<int>& nums, int target) {
track(nums,target,0,0);
return count;
}
};//回溯
时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n 是数组 nums \textit{nums} nums 的长度。回溯需要遍历所有不同的表达式,共有 2 n 2^n 2n 种不同的表达式,每种表达式计算结果需要 O(1) 的时间,因此总时间复杂度是 。空间复杂度:O(n),其中 n 是数组 nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n。
构建一个二维数组进行存放好理解一些:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int &num:nums){
sum+=num;
}
int diff=sum-target;
if(diff<0 || diff%2!=0){
return 0;
}
int n=nums.size(),neg=diff/2;
vector<vector<int>> dp(n+1,vector<int>(neg+1));
dp[0][0]=1;
for(int i=1;i<=n;i++){
int temp=nums[i-1];
for(int j=0;j<=neg;j++){
dp[i][j]=dp[i-1][j];
if(j>=temp){
dp[i][j]+=dp[i-1][j-temp];
}
}
}
return dp[n][neg];
}
};
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
**本题中strs 数组里的元素就是物品,每个物品都是一个!**确定dp数组(dp table)以及下标的含义:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
确定递推公式: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])。
dp数组如何初始化为0
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 one_count=0,zero_count=0;
for(char c:str){
if(c=='0'){
zero_count++;
}else{
one_count++;
}
}
for(int i=m;i>=zero_count;i--){
for(int j=n;j>=one_count;j--){
dp[i][j] = max(dp[i-zero_count][j-one_count]+1,dp[i][j]);
}
}
}
return dp[m][n];
}
};
时间复杂度: O(kmn),k 为strs的长度;空间复杂度: O(mn)。经典的背包问题只有一种容量不同,这道题有两种容量,即选取的字符串子集中的 0 和 1 的数量上限。
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。假设每一种面额的硬币有无限个。题目数据保证结果符合 32 位带符号整数。
但本题和纯完全背包不一样,**纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!**注意题目描述中是凑成总金额的硬币组合数,组合不强调元素之间的顺序,排列强调元素之间的顺序。
确定dp数组以及下标的含义:dp[j]:凑成总金额j的货币组合数为dp[j]
确定递推公式:dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。所以递推公式:dp[j] += dp[j - coins[i]];
所以递推公式:dp[j] += dp[j - coins[i]];首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。
class Solution {
public:
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]];
}
}
// for(auto i:dp){
// cout<
// }
return dp[amount];
}
};
时间复杂度: O(mn),其中 m 是amount,n 是 coins 的长度;空间复杂度: O(m)
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。题目数据保证答案符合 32 位整数范围。
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!**组合不强调顺序,(1,5)和(5,1)是同一个组合。排列强调顺序,(1,5)和(5,1)是两个不同的排列。本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。把排列都列出来的话,只能使用回溯算法爆搜。
确定dp数组以及下标的含义:dp[i]: 凑成目标正整数为i的排列个数为dp[i]
确定递推公式:dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。
dp数组如何初始化:因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
确定遍历顺序:个数可以不限使用,说明这是一个完全背包。个数可以不限使用,说明这是一个完全背包。
用示例中的例子推导一下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0]=1;
for(int i=0;i<target+1;i++){
for(int j=0;j<nums.size();j++){
if(i-nums[j]>=0 && dp[i] < INT_MAX-dp[i-nums[j]]){
// C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
dp[i] += dp[i-nums[j]];
}
}
}
return dp[target];
}
};