DAY46:动态规划(七)01背包应用:分割等和子集+最后一块石头重量Ⅱ+目标和

news2025/1/9 1:29:12

文章目录

    • 416.分割等和子集(回溯+01背包)
      • 思路
      • 回溯解法(类似组合总和Ⅱ)
        • 回溯解法存在的问题
      • 01背包思路
        • 为什么能抽象成背包问题
      • 01背包写法1:常规写法,考虑重量=价值
        • 重量=价值类问题的思考方式
        • DP数组含义
        • 递推公式
        • DP数组初始化
        • 遍历顺序
      • 写法1完整版
        • debug测试
      • 01背包写法2:只看能否装满重量,不考虑价值的写法
        • DP数组含义
        • 递推公式
        • 为什么这样写能收集所有和为target的情况
        • 初始化
      • 写法2完整版
      • 总结
    • 1049.最后一块石头的重量
      • 背包思路
      • DP数组含义
      • 递推公式
      • DP数组初始化
      • 遍历顺序
      • 完整版
        • 这两个背包都是装满的状态吗?
      • 总结
    • 494.目标和(递推公式重点:方案数问题模板)
      • 背包写法思路
        • (target + sum) / 2 向下取整的影响
      • 限制条件
      • DP数组含义
      • 递推公式(组合问题/求方案数问题的递推公式模板)
      • DP数组初始化(重要)
        • 遍历顺序
      • 完整版
      • 总结

  • 这三道题目都属于物品数组里没有分开重量和价值,我们令重量=价值的类型,这种类型背包问题很常见。
  • 也就是给出一个数组nums[i]判断nums[i]里能不能找出子集,令子集元素总和=某特定目标值。前两道题是能否找出子集,即能否填满背包;最后一题是找所有符合条件子集的个数,也就是填满背包的方案数目
  • 填满背包这个概念就是物品价值=重量的时候才有,因为背包问题推导的是物品的最大价值,且背包问题限制是背包最大重量为j。当价值=重量的时候,最大价值=最大重量<=背包容量j,才能对填满背包的情况进行分析。

416.分割等和子集(回溯+01背包)

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5][11]

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路

本题目的是把集合分成两个子集,使得分出来的两个子集的和相等。如果两个子集元素和相等,也就说明他们的和都是集合元素总和的一半

这道题目本质就是,例如集合总和为22,此时找出集合内有哪些元素相加=11,剩下的元素相加自然也=11.

回溯解法(类似组合总和Ⅱ)

本题本质上是求解集合内是否存在子集,其总和=sum/2,也就是总和是否=target。。这个问题看起来很像 39.组合总和 系列的问题。组合总和题目如下:

在这里插入图片描述

40.组合总和Ⅱ

在这里插入图片描述
组合总和Ⅱ对应写法:

class Solution {
public:
    void backtracking(vector<int>&path,vector<vector<int>>&result,vector<int>& candidates,int sum,int target,vector<int>&used,int startIndex){
        //终止条件
        if(sum>target){
            return;
        }
        if(sum==target){
            result.push_back(path);
            return;
        }
        //单层搜索
        for(int i=startIndex;i<candidates.size();i++){
            //防止访问下标-1越界,涉及到下标-1的都必须检查越界问题
            if(i>=1&&candidates[i]==candidates[i-1]&&used[i-1]==0){
                continue; //直接不处理,跳到for循环的下一个
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            //记录use过当前的i
            used[i]=1;
            //开始递归
            backtracking(path,result,candidates,sum,target,used,i+1);
            //回溯,重置use
            sum -= candidates[i];
            path.pop_back();
            used[i]=0;
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
		vector<int>path;
        vector<vector<int>>result;
        //注意这种带有初始大小和初始值的vector数组定义方式!需要访问used下标所以必须初始化
        vector<int>used(candidates.size(),0);
        int sum=0;
        int startIndex=0;
        sort(candidates.begin(),candidates.end());
        backtracking(path,result,candidates,sum,target,used,startIndex);
        return result;
    }
};

我们可以通过修改 “组合总和II” 的回溯方法来解决 “将数组分割成两个子集,使得两个子集的元素和相等” 这个问题。

按照组合总和Ⅱ的思路,本题的回溯写法如下:

  • 本题与组合总和II的主要区别在于我们在找到一个符合条件的组合后就直接返回,不再继续搜索,因为我们只关心是否存在这样的组合,而不关心有多少种组合。
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        // 计算数组所有元素之和
        int sum = accumulate(nums.begin(), nums.end(), 0);
        // 如果和不是偶数,无法平分,直接返回false
        if (sum % 2 != 0) {
            return false;
        }
        // 平分数组的目标和
        int target = sum / 2;
        // 对数组进行排序,有利于后续剪枝
        sort(nums.begin(), nums.end());
        // 初始化一个记录使用状态的数组
        vector<int> used(nums.size(), 0);
        // 从数组开始处开始进行回溯寻找
        return backtrack(nums, target, 0, used);
    }

    bool backtrack(vector<int>& nums, int target, int start, vector<int>& used) {
        // 如果target减为0,表示已经找到一组符合条件的子集,返回true
        if (target == 0) {
            return true;
        }
        // 开始单层搜索
        for (int i = start; i < nums.size(); ++i) {
            // 防止访问下标-1越界,涉及到下标-1的都必须检查越界问题
            // 直接跳过连续的、相同的元素,防止产生重复的子集
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {
                continue; //直接不处理,跳到for循环的下一个
            }
            // 剪枝:如果当前数字大于target,后续无需再进行,直接break
            if (nums[i] > target) {
                break;
            }
            // 做选择,将当前元素纳入子集,和减少nums[i]
            used[i] = 1;
            // 继续递归填充子集,如果找到一组,直接返回true
            if (backtrack(nums, target - nums[i], i + 1, used)) {
                return true;
            }
            // 撤销选择,回溯,恢复状态
            used[i] = 0;
        }
        // 当前没有找到符合条件的子集,返回false
        return false;
    }
};

回溯解法存在的问题

这种解法思路是正确的,可以通过小用例,但是大用例会超时。通过这道题我们也可以复习一下组合总和系列的回溯解法。

应该可以用记忆型搜索来优化,但是暂时不做尝试,后面再补充优化。

在这里插入图片描述

01背包思路

背包问题,是有N件物品一个最多能背重量为W 的背包第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、等等,要注意题目描述中商品是不是可以重复放入

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

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

为什么能抽象成背包问题

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

01背包问题的一种应用是,只看背包是否能够正好装满不在意背包的最大价值,也可以不在乎物品的价值,只看重量

确认了以下四点,才能把01背包套到本题上面来。

  • 背包的最大重量为sum / 2
  • 背包要放入的商品(集合里的元素)重量为元素的数值,(价值也为元素的数值,可以直接令价值=数量,也可不考虑价值)
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集
  • 背包中每一个元素不可重复放入

01背包写法1:常规写法,考虑重量=价值

重量=价值类问题的思考方式

首先明确一点,我们不可能放入总重量>背包容量的物品

因为01背包问题的递推公式是:

for(int i = 0; i < nums.size(); i++) {
    for(int j = target; j >= nums[i]; j--) { 
        dp[j] = max(dp[j], dp[j - weight[i]] +value[i]);
    }
}

这个公式的意思是,如果我要在背包容量为 j 的情况下,尝试放入第 i 个物品(weight[i]),那么我首先要保证我的背包容量 j 大于等于我要放入的物品的重量weight[i]。也就是 j >= weight[i]

这个限制,确保了我们不能在背包容量小于物品重量的情况下将物品放入背包,也就避免了总重量大于背包容量的情况。

当物品重量=价值的时候,也就是说,物品的最大价值dp[j],也就是他的最大重量。而根据上面推导得知,背包最大重量一定是小于容量j的。也就是说,如果背包想要装满,那么他的最大重量dp[j]需要满足dp[j]=j

因此,对于这种重量=价值的背包问题,判断背包装满的方法就是, dp[target]==target的时候,就说明背包装满了

DP数组含义

01背包中,dp[j]表示,容量为j的时候,背包的最大价值是dp[j]

本题每个元素重量=价值,也就是说,如果我们把容量为11的背包装满,他的价值应该也是11。按照上面重量=价值问题的分析思路,如果dp[j]=j,说明背包刚好装满了。(实际上最大价值=j,就说明最大重量达到j了,就说明装满了

递推公式

01背包的一维递推公式是:

dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//dp[j]由二维DP数组压缩得到,压缩了dp[i-1]

在本题中,weight[i]value[i]是相等的,都是数值nums[i]

因此递推公式为:

dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);

DP数组初始化

dp[0]=0,容量为0的背包所装价值最大为0。

因为涉及到最大值的取值,因此其余非0下标全部初始化为0。

遍历顺序

本题遍历顺序就是01背包的遍历顺序,也就是物品在外,背包在内,且背包为倒序遍历。(因为每个物品只有一个)

//一维DP遍历顺序不可颠倒,二维DP可以
for(int i=0;i<nums.size();i++){
    //背包容量为目标值target
    for(int j=target;j>=nums[i];j--){
        //递推公式
        dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
    }
}

写法1完整版

  • 注意,DP数组含义是容量为j的时候,dp[j]代表最大价值(最大重量)。定义DP数组的时候,j的最大值也就是最大容量,也就是vector<int>dp(target+1,0)(每个用例的target都不一样,这样不会发生越界错误)
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum=0;
        //先计算总和,如果是奇数直接返回
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
        }
        if(sum%2!=0) return false;//奇数不可能分成相等两部分
        
        int n=nums.size();
        int target=sum/2;
        
        //创建一维DP数组
        vector<int>dp(n+1,0);
        for(int i=0;i<n;i++){
            for(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)

debug测试

最开始的时候发生了越界错误,原因是一维DP数组大小设置成了nums.size(),也就是物品个数。实际上这是错误的,dp[j]中j的含义是背包容量,DP数组的大小应该设置为背包容量j的最大值

在这里插入图片描述
背包容量j是从target开始倒序遍历,因此DP数组大小改为vector<int>dp(target+1,0)即可。

01背包写法2:只看能否装满重量,不考虑价值的写法

这个问题实际上也可以抽象为一个只考虑重量的01背包问题,也就是集合有若干已知重量的物品,问容量为sum/2的背包能不能刚好装满。不考虑背包对应的价值,重新写一个递推公式。

DP数组含义

我们采用一维DP的01背包思路来解决,如果不考虑价值只考虑重量,那么dp[j]数组的含义是,能否通过选取数组中的一些数,使得这些数的和等于 j

背包装满的条件是:存在一个子集,它的和等于背包的容量target。这个条件可以通过检查dp[target]是否为真来判断。

递推公式

结合DP数组的含义,我们对于每一个dp[j],需要判断能不能选取数组数字,使得数字之和(也就是物品重量之和)是j

因此,我们在遍历过程中,可以让dp[0]=0,因此

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

这个递推公式也可以写成:

for (int i = 0; i < n; ++i) {
   for (int j = target; j >= nums[i]; --j) {
       if(dp[j-nums[i]]==true){
          dp[j]=true;
        }
   }
}

为什么这样写能收集所有和为target的情况

当我们在数组中遍历到元素nums[i]时,dp[j - nums[i]]为真,说明我们能在数组中选取若干元素,使得他们的和等于j - nums[i]。而这时如果我们再加上当前的元素nums[i],总和就会变成j - nums[i] + nums[i] = j。这就说明存在一个子集,他们的和等于j,所以我们可以更新dp[j] = true

举个例子,假设我们在遍历数组时,当前元素nums[i] = 5,我们希望找到和为11(target)的子集,即j = 11。这时,j - nums[i] = 11 - 5 = 6。若dp[6]为真,说明我们已经找到了和为6的子集。那么如果我们再加上当前的元素5,和就变成了6 + 5 = 11,所以我们就找到了和为11的子集,于是我们可以更新dp[11] = true

初始化

这种做法的思路是,遍历过程中通过判断dp[j-nums[i]]是否为真,nums[i]当前物品的重量,我们在初始化的时候令dp[0]=true,其他全部为false,那么只有满足j=nums[i](也就是刚好装得下)的时候,dp[j]才会变成true。

因此初始化方式是,dp[0]=true,其他全部为false

写法2完整版

对于每个数字nums[i],从targetnums[i]进行逆序遍历。如果dp[j - nums[i]]为真,说明存在一个子集,它的和为j - nums[i]。那么加上nums[i]之后,和就变成了j,所以此时可以将dp[j]更新为true

  • 集合有若干已知重量的物品,问容量为sum/2的背包能不能刚好装满
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n = nums.size();
        int totalSum = accumulate(nums.begin(), nums.end(), 0);
        int target = totalSum / 2;
        if (totalSum % 2 == 1) return false;
        vector<bool> dp(target + 1, false);
        dp[0] = true;
        for (int i = 0; i < n; ++i) {
            for (int j = target; j >= nums[i]; --j) {
                if(dp[j-nums[i]]==true){
                    dp[j]=true;
                }
            }
        }
        return dp[target];
    }
};

总结

这道题目就是一道01背包应用类的题目,需要我们拆解题目,然后套入01背包的场景。

01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。

第一种写法是重量=价值的写法,相对来说好理解一些。需要注意的一点就是重量=价值,那么dp[j]代表的最大价值,一定<=j(最大重量),因为背包问题的大前提就是,放入背包的所有物品最大重量一定<=背包容量

1049.最后一块石头的重量

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 24,得到 2,所以数组转化为 [2,7,1,8,1],
组合 78,得到 1,所以数组转化为 [2,1,1,1],
组合 21,得到 1,所以数组转化为 [1,1,1],
组合 11,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 100

背包思路

本题的主要思路就是,尽量找重量相同的石头,才能让最后剩下的重量最小。也就是要让石头分成重量相等的两堆

这个思想其实就和416.分割等和子集问题很像了,找一个容量是sum/2的背包,先把石头装满两个背包剩下要么为0,要么还有剩余(sum不能整除2还有余数),此时剩余就是最小石头

我们可以把石头分成两组 x 和 y,有x + y = sum

我们假设 x <= y,那么我们希望 x 和 y尽可能接近,所以 x 越大越好。但是 x 最大也只能是 sum / 2,因为它比 y 小

所以,我们如果想让x和y尽可能接近,就是看 sum / 2 最多能装的价值是多少,sum / 2最多能装的价值,就是x的数值。

DP数组含义

本题因为每个石头只有一个,所以属于01背包问题。同时本题也是物品重量=价值的类型,和 416.分割等和子集 很像。

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

相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,最多可以装的价值为 dp[j] == 最多可以背的重量为dp[j]

递推公式

本题递推公式和上一题一样,都是为了装满,因此dp[j]仍然是最大重量/最大价值。

dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);

DP数组初始化

本题dp[0]代表装满容量0的最大价值,dp[0]=0。

因为其他也涉及max的对比,所以所有初始值都设置成0.

遍历顺序

遍历顺序同01背包遍历顺序。

完整版

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        //先求总和sum
        int sum=0;
        for(int i=0;i<stones.size();i++){
            sum+=stones[i];
        }
        int target = sum/2;//向下取整即可,这里不需要整除
        //定义DP数组
        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]);
            }
        }
        //总和减去两个背包就是剩下的最小石头
        int left = sum-dp[target]-dp[target];
        return left;

    }
};

这两个背包都是装满的状态吗?

不一定,这个解决方案并不是要求两个背包都必须装满,而是尽可能让这两个背包的重量接近。在这种情况下,可能会有剩余的石头,这些就是剩下的石头的重量。dp[j]表示背包容量为j时能装的石头的最大重量。而最后的结果是sum - 2 * dp[target],这里的sum是所有石头的总重量,dp[target]表示能够装满容量为sum/2的背包的石头的最大重量,即两个背包的重量和

此时,sum - 2 * dp[target]就表示剩下的石头的重量,也就是我们要求的答案。

总结

本题其实和 416. 分割等和子集 几乎是一样的,只是最后对dp[target]的处理方式不同。

  1. 分割等和子集 相当于是求背包是否正好装满,而本题是求背包最多能装多少

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

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

背包写法思路

本题最重要的是推导过程,以及为什么能套用01背包。

题目要求在数字前面加+或者-,求出经过正负赋值之后数组总和运算结果等于 target 的不同表达式的数目,也就是求方案数目

也就是说会出现一批正数和一批负数。我们假设所有正数的和为x,所有负数(目前还是正整数的状态)的和为y。可以列出如下式子:

  • x+y=Sum
  • x-y=target

可以得出x=(Sum+target)/2

由于x是正数的和,因此我们此时就可以把问题转换为背包问题,也就是总容量为x的背包物品从nums[i]里面抽取要求正好装满这个容量为x的背包,共有多少组nums[i]?

本题和前几道题一样,都属于物品重量=价值类型的题目(因为数组内只有单一数字),也就是属于规定一个目标和target看数组nums[i]里有没有加起来总和刚好等于目标值的子集/有多少个总和刚好=target的子集的问题。

(target + sum) / 2 向下取整的影响

由上一道石头的题目我们也可以知道,/2的操作如果除不尽,是向下取整的。例如5/2=2。

但是实际上,本题向下取整并没有影响,因为**(target + sum)如果是奇数,说明x和y是无解的**。本题要求的就是x和y,无解没有意义。

同时,如果sum值已经<target,也是一定无解的。

限制条件

由上面的分析可以得到两个限制:

  • 原数组sum值<target无解,注意target需要是绝对值,因为target有可能是负数
  • (target+sum)是奇数无解
if(sum<abs(target)) return 0;
//这里注意target需要是绝对值,因为target有可能是负数!
if((target+sum)%2!=0) return 0;

DP数组含义

本题和之前遇到的两道题不太一样,虽然都属于物品重量=价值的情况,但是之前都是求容量为j的背包装满的时候是什么情况

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

在这个组合问题中,dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法。

递推公式(组合问题/求方案数问题的递推公式模板)

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

在遍历中,我们先是知道i从0到n的所有取值,再针对每个i的取值,对所有的背包容量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]] 累加起来。也就是相当于,把背包问题每一个i对应的DP数组dp[j]这个位置上的所有数值,都进行累加

类似下图的情况,dp[5]的所有方案,是所有物品都遍历完,都考虑在内之后的累加总和

在这里插入图片描述
所以,求组合类/方案数目问题的公式,都是类似这种:

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

DP数组初始化(重要)

组合问题/求方案数问题,初始化非常重要,因为方案数递推公式的推导,全部都是基于dp[0]进行的

本题dp[0]含义是背包容量为0的时候,有多少种方案。我们直接把dp[0]代入递推公式,从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0

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

所以本题我们应该初始化 dp[0] 为 1。

遍历顺序

本题的物品不能重复使用,是划分子集类的问题,因此属于01背包。01背包的遍历顺序是物品在外,背包容量在内,且背包容量倒序遍历。

完整版

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        //首先排除无解的情况
        int sum=0;
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
        }
        int totalSum = sum+target;
        //如果是奇数,没有x
        if(totalSum%2!=0) return 0;
        //如果原数组sum<target绝对值,必然无解
        if(sum<abs(target)) return 0;
        
        int x=totalSum/2;//背包最大容量
        //定义DP数组
        vector<int>dp(x+1,0);
        //初始化dp[0],这一步在方案数问题很重要
        dp[0]=1;
        for(int i=0;i<nums.size();i++){
            for(int j=x;j>=nums[i];j--){
                dp[j] += dp[j-nums[i]];
            }
        }
        //dp[x]就是方案数目
        return dp[x];

    }
};
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量
  • 空间复杂度:O(m),m为背包容量

总结

实际上,回溯算法:39. 组合总和 的系列问题,也可以用dp来做,如果仅仅是求所有组合个数的话,用dp比用回溯节省很多时间

但回溯算法:39. 组合总和 (opens new window)要求的是把所有组合都列出来,还是要使用回溯法爆搜的。

可以作为模板记住,在求装满背包有几种方法的情况下,递推公式一般为

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

完全背包还会用到这个递推公式。

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

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

相关文章

综合小实验

第一步&#xff1a;计划IP R1的环回&#xff1a;192.168.1.0/28 R2的环回&#xff1a;192.168.1.16/28 R123的O/O/0接口&#xff1a;192.168.1.32/28 R3-4&#xff1a;192.168.1.128/30 Vlan2&#xff1a;192.168.1.48/28 vlan3&#xff1a;192.168.1.64/28 192.168.1.0/24 0区…

力扣 279. 完全平方数

一、题目描述 给你一个整数 n&#xff0c;返回和为 n 的完全平方数的最少数量 。 完全平方数是一个整数&#xff0c;其值等于另一个整数的平方&#xff1b;换句话说&#xff0c;其值等于一个整数自乘的积。例如&#xff0c;1、4、9 和 16 都是完全平方数&#xff0c;而 3 和 …

3Ds max入门教程:创建马来西亚双子塔3D模型

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 最终图像&#xff1a; 步骤-1 下面给出了这个双子塔的基本轮廓。 步骤-2 由于它是一栋88层的建筑&#xff0c;所以我一开始打算把它建到40层。为此&#xff0c;我使用标准的基元类型&#xff1a;盒子和圆…

python 使用 subprocess 实现交互式命令的非交互式执行

背景 想要定时执行某些脚本, 但是脚本是交互式的, 例如下面的bat 脚本 echo offset /p nameName: echo Name is %name%echo exit set /p byeBye: echo Bye is %bye%需要先输入Name, 在看到 exit 后在输入 Bye, 然后程序退出. 解决方案 使用subprocess 来实现 import subpr…

【Solidworks加密软件】Solidworks图纸文件加密方法

Solidworks是一款广泛应用于机械设计和工程领域的三维建模软件。由于Solidworks文件中可能包含敏感的设计和知识产权信息&#xff0c;保护这些图纸的安全性变得至关重要。本文将介绍Solidworks图纸加密的方法和最佳实践&#xff0c;以确保文件的机密性和安全性。 为什么需要加…

Redis字典

1.前言 我们回顾一下之前讲到的Redis的字典结构&#xff0c;示意图如下&#xff1a; Redis的字典本质上来说也是数组链表的数据结构&#xff0c;这与Java中HashMap的数据结构很类似。 由上述结构示意图也能看出&#xff0c;字典dict中维护了一个ht数组&#xff0c;而且只有两…

使用IDEA工具debug java annotation processors

最近看Spring提供的自动生成spring-configuration-metadata.json文件的组件。组件依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</opti…

IO线程NO

在处理问题&#xff1a; Got fatal error 1236 from master when reading data from binary log: Could not find first log file name in binary log index file 好翻译过来就是&#xff1a; 从二进制日志读取数据时&#xff0c;从主服务器收到致命错误 1236&#xff1a;“无法…

opencv-06 使用numpy.array 操作图片像素值

opencv-06 使用numpy.array 操作图片像素值 **1&#xff0e;二值图像及灰度图像****利用item 读取某一个像素值****利用itemset 修改像素值****彩色图像numpy.arry 像素值操作** numpy.array 提供了 item()和 itemset()函数来访问和修改像素值&#xff0c;而且这两个函数都是经…

与时代并进,轻创时代愿做“Ai数字人产业的导向标”

近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;越来越多的企业开始关注并寻求AI数字人的解决方案&#xff0c;以提升业务效率和创造竞争优势。在这个激烈竞争的市场中&#xff0c;轻创时代作为行业黑马出现在人们视野中&#xff0c;以卓越的创新能力立志成为中小型…

【剑指offer】19. 链表中倒数最后k个结点(java)

文章目录 链表中倒数最后k个结点描述示例1示例2思路完整代码 链表中倒数最后k个结点 描述 输入一个长度为 n 的链表&#xff0c;设链表中的元素的值为 ai &#xff0c;返回该链表中倒数第k个节点。 如果该链表长度小于k&#xff0c;请返回一个长度为 0 的链表。 数据范围&a…

【软件测试】在Windows环境安装Docker(详细步骤)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 下载和安装 1、地…

免费内网穿透方案twingate搭建,适用pve,exsi等虚拟机访问场景

最近学习devops环境搭建&#xff0c;其中需要安装很多中间件和虚拟机&#xff0c;之前黑裙和pve都用的zerotier,可以点对点通信&#xff0c;但是机器多了就要一台一台去部署比较费力&#xff0c;而且在使用过程中发现&#xff0c;pve的容器和pve宿主机的出口IP是一样的&#xf…

聊聊微服务 架构思想

用了好多年微服务架构了&#xff0c;我经常会反思&#xff0c;这个项目为啥用微服务&#xff1f;真的能帮我们解决一些痛点吗&#xff1f;这个项目有必要用微服务吗&#xff1f;这个项目体现出微服务的价值了吗&#xff1f; 我是从2017年开始入手微服务&#xff0c;距今已经五六…

从小白到大神之路之学习运维第59天--------inotify+rsync同步和实时同步(单台同步和多台同步)

第三阶段基础 时 间&#xff1a;2023年7月13日 参加人&#xff1a;全班人员 内 容&#xff1a; inotifyrsync同步和实时同步 目录 一、rsync远程同步 二、源端到发起端同步 安装部署&#xff1a; 源端&#xff08;服务端&#xff09;&#xff1a; &#xff08;单台…

ROS安装注意事项

输入roscore报错&#xff1a;"roscore" not found 输入 sudo apt install ros-​melodi​c-roslaunch​

概率论的学习和整理17:EXCEL里直接对应的分布公式计算概率

1EXCEL计算这些特殊分布的方差 1.1 用原始的概率&#xff0c;期望和方差的方法 虽然计算概率&#xff0c;需要用对应分布的公式P(xn) 想了解的随机变量是总次数n&#xff0c;需要对应几何分布&#xff0c;负二项分布P(xk) 想了解的随机变量是成功次数k&#xff0c;需要对应超几…

【Spring】注解读取和存储Bean对象(下)

三、Spring 获取 bean 对象 获取 bean 对象也叫 “对象装配”&#xff0c;“对象注入”&#xff0c; 意思就是把对象取出来放到某个类中。 对象装配&#xff08;对象注入&#xff09;的实现有以下三种方法&#xff1a; 1. 属性注入 2. 构造方法注入 3. Setter 注入 接下来…

【Unity面试篇】Unity 面试题总结甄选 |Unity进阶篇 | ❤️持续更新❤️

前言 关于Unity面试题相关的所有知识点&#xff1a;&#x1f431;‍&#x1f3cd;2023年Unity面试题大全&#xff0c;共十万字面试题总结【收藏一篇足够面试&#xff0c;持续更新】为了方便大家可以重点复习某个模块&#xff0c;所以将各方面的知识点进行了拆分并更新整理了新…

总结926

总结&#xff1a;今晚状态极佳&#xff0c;回去路上差点被宿管阿姨锁在楼下。之前每每学到晚上脑子都转不动了&#xff0c;不过今晚就像是适应了一样。这也说明&#xff0c;学习&#xff0c;是可以上瘾的。只要循序渐进&#xff0c;步步为营&#xff0c;就不断收获学习的乐趣。…