Leetcode416.分割等和子集
Leetcode416.分割等和子集

Leetcode416.分割等和子集

从题目分析很容易想到回溯法,但是会超时。如果不是提示了有动态规划,很难想到用动规求解。

其实即便知道是动规,初见写也不会做(实话),只从题目怎么能想到01背包呢?

还是有几个特征可以让我们联想到的:

  • 每个元素只取一次且不放回
  • 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
  • 有确定的价值:sum/2

如果想到了01背包(以下采用滚动数组求解),直接开启动规五部曲:

1. 确定 dp 数组的含义:

容量为 j 的背包,放入的重量总和是 dp[j]

这里还有个问题,创建 dp 数组时的长度是多少?

因为我们最后求的价值是 nums 数组和的一半,所以这个长度就是整个范围值的一半,即 200*100/2,赋值一个10001即可。

2. 确定递推公式:

因为背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值,所以很容易套用01背包的原始递推公式得出:

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

3. 初始化

首先 dp[0] = 0 这是毫无疑问的。

如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

这样才能让 dp 数组在遍历过程中取最大的价值,而不是被初始值覆盖。

4. 确定遍历顺序

按照正常01背包滚动数组的遍历顺序遍历就可以了,即外层顺序内层逆序遍历:

for (int i = 0; i < nums.length; i++) {
    for (int j = target; j >= nums[i]; j--) {	// target时sum/2
        dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

5. 模拟举例dp数组:这个可以通过debug来查看


给出完整代码:

class Solution {
    public boolean canPartition(int[] nums) {
        // 容量为j的背包的,放入的重量总和是dp[j]
        int[] dp = new int[10001];

        int sum = Arrays.stream(nums).sum();
        if (sum % 2 == 1) return false;
        int target = sum / 2;

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

        if (dp[target] == target) return true;

        return false;
    }
}


总结一下,这种题目还是做的太少,以至于想不到动态规划。

看到了讨论区的一个回答,感觉说的很好,需要牢记:

问:怎么想到01背包?

答:做多了类似题目就想到了。这是「从序列中选择子序列使得和接近target」系列的题目,一般都是双向dfs或者01背包问题来完成。

同样思路的题目还有 Leetcode1049.最后一块石头的重量 II

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注