leetcode 力扣 416 分割等和子集

115 阅读3分钟

动态规划写法一

算法思路

  • 能够分割成两个等和子集,数组的总和sum必为偶数。
  • 我们需要判断的是,和为sum / 2的子集是否存在,而不是两个子集都要找出来。

dp[i][j]的含义:

  • dp数组的行代表遍历到哪个nums[i],列代表背包[1, sum / 2]1 <= nums[i] <= 100,第0列是凑数的。
  • dp[i][j]表示nums中第i个数nums[i]能否填满容量为j的背包。
  • 可以是nums[i] == j,也可以是记忆中的j - nums[i]的背包是否被填满,是的话j - nums[i]的状态加上nums[i]可以填满j背包。

注意每一次计算dp[i][j]都要把之前的状态dp[i - 1][j]先转移过来(所谓的不选)

for(int i = 1; i < n; i++){
    for(int j = 1; j <= target; 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]];
        }
    }
}
  • 因为第10行有if判断,所以第3行的dp[i - 1][j]不能去掉。
  • 为了防止dp[i - 1][j - nums[i]dp[i][j]变成false,所以第11行的dp[i - 1][j]不能去掉,如下图。

2.jpeg

class Solution {
    public boolean canPartition(int[] nums) {
        // 先判断nums的和是否为偶数
        int sum = 0;
        for(int num : nums){
            sum += num;
        }

        if((sum & 1) == 1){
            return false;
        }

        int n = nums.length;
        int target = sum / 2;
        boolean[][] dp = new boolean[n][target + 1];
        if(nums[0] <= target){
            dp[0][nums[0]] = true;
        }
        

        for(int i = 1; i < n; i++){
            for(int j = 1; j <= target; 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]];
                }
            }
        }

        return dp[n - 1][target];
    }
}

由右下角往左上方可以看出11是怎么凑数来的路径,11 - 5 = 6, 6 - 5 = 1
所以11是由1 + 5 + 5得到的。 1.jpeg

动态规划写法二

dp数组的构成:

  • 与写法一不同的是,写法二比写法一多了一行。dp[0][0]默认初始化为true,表示物品重量与背包容量相同。第一个0不是代表0号物品,就当他占一行好了;第二个0可以当成j - nums[i] == 0,也就是物品重量nums[i]与背包容量j相同。第一行其余部分不用管,直接从第二行开始遍历。
  • 1n行,表示遍历nums第一个到最后一个数,代码中为nums[i - 1]。列就是背包容量。

为什么要多开一行?

  • 为了防止背包只有0的情况,也就是int[][] dp = new int[n][0 + 1]。比如下图中neg就是背包为0。如果在这种情况下使用写法一,会有导致数组越界,dp[0][nums[0]] = 1, 参考下图 494 目标和

3.jpeg

  • 当然这一题背包不会为0,因为if((sum & 1) == 1) {return false;},即便sum = 1, target = sum / 2 = 0,程序也会马上返回。

核心代码:

  • 可以看出比写法一更简洁。
  • 首先直接转移(或者说num > j,背包能否装满取决于上一行的背包是否装满。)
  • num <= j的情况:
  • 如果num == j,那么就取第一列,第一列统统都为true,表示物品重量与背包容量相同。
  • 如果num < j,那么就取决于上一行是否为true
for (int i = 1; i <= n; i++) {
    int num = nums[i - 1];
    for (int j = 1; j <= target; j++) {
        dp[i][j] = dp[i - 1][j];

        if (num <= j) {
            dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
        }
    }
}

2.jpeg

class Solution {
    public boolean canPartition(int[] nums) {
        // 先判断nums的和是否为偶数
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        if ((sum & 1) == 1) {
            return false;
        }

        int n = nums.length;
        int target = sum / 2;
        boolean[][] dp = new boolean[n + 1][target + 1];
        dp[0][0] = true;

        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 1; j <= target; j++) {
                dp[i][j] = dp[i - 1][j];

                if (num <= j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
                }
            }
        }

        return dp[n][target];
    }
}