动态规划(01背包问题的拓展)

时间:2024-11-21 07:32:48

01背包问题

这个问题之前有讲到过,可以参考动态规划01背包问题
此次主要讲述01背包问题下的拓展出来的一些问题。

题目一、分割等和子集

题目链接:leetcode416.分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
  • 1
  • 2
  • 3

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5][11].
  • 1
  • 2
  • 3
  • 4
  • 5

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.
  • 1
  • 2
  • 3
  • 4
  • 5

这种题乍一眼看到感觉无从下手,其实不然。
思路
1.首先思考一下,既然要将它们分为相等的两份,那么他们的和必然是偶数,否则必定不会被分成相等的两份。
2.这个问题如何转换为01背包问题呢,其实很简单,首先确定什么是背包的容量,什么是物品的容量,什么又是物品的价值。
数组中的所有的数的和我们是可以求出来的,那么排除它是偶数后,它的一半也是能算出来的,也就是说,如果我用数组中这些数能得到总和的一半,那么就表示我一定有一种方法可以把它分为相等的两份。
假设数组所有数的和为偶数并记为sum,那么它的一半记为half_sum,那么它就是背包的容量。而整个数组既是物品的重量,也是物品的价值。
那么这道题就可以理解为,再容量为half_sum的大小下,我能装入的最大的物品价值是多少。
求出这个最优解后,只需要和half_sum比较是否相等即可。
递推公式
创建dp数组,用i表示物品数,j表示当前容量,dp[j]表示j容量下的最大总和。
初始化为0,因为只要当前总和为0,那么我总是可以获取到比0大的总和。
dp[j] = max(dp[j],dp[j - nums[i]] + nums[i])

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 == 1)  return false;
        //获得总和的一半,也是背包的容量
        int half_sum = sum/2;
        vector<int> dp(half_sum + 1,0);
        for(int i = 0;i<nums.size();i++)
        {
          for(int j = half_sum;j>=nums[i];j--)
          {
            dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);
          }
        }
        return dp[half_sum] == half_sum;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

题目二、最后一块石头的重量 II

题目链接:最后一块石头的重量 II
有一堆石头,每块石头的重量都是正整数。

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

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

示例:

输入:[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],这就是最优值。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

提示:

1 <= stones.length <= 30
1 <= stones[i] <= 1000
  • 1
  • 2

思路
1.同样想要用01背包问题的思想解决这个问题,那么就得要先明确背包容量,物品重量和物品价值,我想要得到最小的石头重量,那么我只需要将这一堆石头分为重量最接近的两份就可以了,这样粉碎下来,一定能得到最小的石头的重量。

2.分析出来问题的本质后,就往01背包上靠拢,首先确定背包的最大容量,同样的,我们将所有石头的总质量加起来,除以二就是背包的容量,注意,这里不需要判断是否为奇数,因为只需要将这一堆石头分为重量最接近的两堆,可以相等,也可以不相等。

3.同样,石头的重量既代表重量也代表它的价值。

递推公式
创建dp数组,用i表示物品数,j表示当前容量,dp[j]表示j容量下的最大总和。
初始化为0,因为只要当前最大质量和和为0,那么我总是可以获取到比0大的质量总和。
dp[j] = max(dp[j],stonesdp[j - [i]] + stones[i])

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0,half_sum = 0;;
        for(int i = 0;i<stones.size();i++)
        {
          sum+=stones[i];
        }
        half_sum = sum/2;
        vector<int> dp(half_sum + 1,0);
        for(int i = 0;i<stones.size();i++)
        {
          for(int j = half_sum;j>=stones[i];j--)
          {
            dp[j] = max(dp[j],dp[j - stones[i]] + stones[i]);
          }
        }
        //因为half_sum是sum/2向下取整的,所以sum - dp[half_sum]一定大于等于dp[half_sum]的
        return sum - 2*dp[half_sum];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

题目三、目标和

题目链接:目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 +-。对于数组中的任意一个整数,你都可以从 +-中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
  • 1
  • 2
  • 3

示例:

输入: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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

提示:

数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。
  • 1
  • 2
  • 3

这道题曾是我面试腾讯时的一道题,当时没做出来,下来结合大佬的思路自己想了想,总结出来。
首先大家肯定想到的是回溯法来解决这个问题,但是如果这个数组的长度特别的长,那么很容易超出时间复杂度。将这道题本质看出来,实则还是一道01背包的问题。
思路
首先,定义一下这些数的总和为sum,要得到目标数S,那么一定有从sum中拆出来两个数记为所有添加+号得和left_sum,所有添加-号得和为right_sum,则有:
left_sum - right_sum = S
而left_sum + right_sum = sum
则就推出:left_sum = (sum + S)/2
这里要注意一下:left_sum ,right_sum ,sum,S都是确定的,因此(sum + S)/2不能让它向下取整,即sum+S必须为偶数,否则没有方法能够得到目标数
因为sum是固定的,S也是固定的,那么就转换为求数组中能得到left_sum得总和。
定义dp数组,i表示当前数的下标,j表示当前容量,dp[j]表示能得到数字和为j得所有方法数。
则有:dp[j] +=dp[j - nums[i]]
例如:假设nums[i] = 2,我现在想要求当前情况下总和为5的方法总数,那么除了我已知的能获得5的方法总和外,我还需要直到dp[5 - 2]的方法总和,因为我知道dp[3]的方法数,那么dp[3]的方法数就是dp[5]的方法数,所以给dp[5]加上即可。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        for(int i = 0;i<nums.size();i++)
        {
            sum+=nums[i];
        }
        //判断条件
        if(sum < S || (sum + S) % 2 == 1)   return 0;
        int left_sum = (S + sum) / 2;
        vector<int> dp(size + 1,0);
        dp[0] = 1;
        for(int i = 0;i<nums.size();i++)
        {
            for(int j = left_sum;j>=nums[i];j--)
            {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[left_sum];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23