【一看就会一写就废 指间算法】分割等和子集 —— 动态规划(0-1背包)

128 阅读3分钟

指尖划过的轨迹,藏着最细腻的答案~

题目:

给你一个 只包含正整数 的 非空 数组 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

分析:

将一个数组分割成两个等和的子集,如果一个数组和为s,则相当于

  • 从一个数组中使用某些元素,其元素和为s/2
  • s必须为偶数。

若s不为偶数,则s/2不是整数,直接放回false。 如果s是偶数,则相当于从物品数组s中挑选一些物品,其总价值为s/2

动态规划一般分为3步走:

  • 确定dp数组含义: 我们定义dp[i][j]为从0到i-1为下标的nums数组中是否正好有和为j的元素。
  • 状态转移方程: 对于某个元素x=nums[i],我们枚举其选或不选:
    • 选:子问题变为从0到i-1的nums数组中选出一个和恰好等于j-x的子序列;
    • 不选:子问题变为从0到i-1的nums数组中选出一个和恰好等于j的子序列。
    最终状态转移方程为: dp[i][j]=dp[i1][j]  dp[i1][jx]dp[i][j] = dp[i - 1][j]\ ||\ dp[i - 1][j - x]
  • 初始化: 对于上面状态转移方程,当i为0时会出现越界,那我们怎样来初始化边界条件呢?我们可以在dp[0]上面增加一行状态,这样dp[i]变为dp[i+1],dp[i-1]变为dp[i],状态转移方程变为: dp[i+1][j]=dp[i][j]  dp[i][jx]dp[i+1][j] = dp[i][j]\ ||\ dp[i][j - x] 将dp[0][0]设置为true,作为一个触发条件,其余值初始化为false。

最终dp[n][s/2]即为最终答案

AC代码:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int s = reduce (nums.begin(), nums.end());
        if (s % 2) {
            return false;
        }

        int n = nums.size();
        s /= 2;
        vector f(n + 1, vector<int>(s + 1));
        f[0][0] = true;
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            for (int j = 0; j <= s; j++) {
                f[i + 1][j] = j >= x && f[i][j - x] || f[i][j];
            }
        }

        return f[n][s];
    }
};

空间优化:

上面状态转移方程对于i来说只用到了i-1,因此我们可以使用一个一维数组来记录之前的状态,反复使用这个一维数组来记录:定义dp[j]为是否存在和为j的子序列。

此时上面状态转移方程变为: dp[j] =[jx]dp[j]\ |= [j - x] 最终答案变为:dp[s/2]

具体的,在代码中我们会将s直接除以2来使用;

并且在遍历j时,我们需要倒序遍历,这是因为我们反复使用一维数组dp,如果正序遍历,在读取dp[j - x]时已经覆盖了上一轮的状态。

此外,设前i个值的和为s2,由于前i个值的子序列的元素和不可能比s2大,因此j可以从min(s2,s)min(s2, s)开始倒着枚举。 并且我们还可以在循环过程中提前判断dp[s]是否为true。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int s = reduce (nums.begin(), nums.end());
        if (s % 2) {
            return false;
        }

        s /= 2;
        vector<int> f(s + 1);
        f[0] = true;
        int s2 = 0;
        for (int x : nums) {
            s2 = min(s2 + x, s);
            for (int j = s2; j >= x; j--) {
                f[j] |=  f[j - x];
            }
            if (f[s]) {
                return true;
            }
        }

        return false;
    }
};