Leetcode 416-分割等和子集

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

题解

题解转载自liweiwei1419

转换为 「0 - 1」 背包问题

做这道题需要做一个等价转换:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半。
本题与 0-1 背包问题有一个很大的不同,即:

0-1 背包问题选取的物品的容积总量 不能超过 规定的总量;
本题选取的数字之和需要 恰好等于 规定的和的一半。

状态与状态转移方程
  • 状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
  • 状态转移方程:很多时候,状态转移方程思考的角度是「分类讨论」,对于「0-1 背包问题」而言就是「当前考虑到的数字选与不选」。
  • 不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
  • 选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
    状态转移方程:

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

一般写出状态转移方程以后,就需要考虑初始化条件。

  • j - nums[i] 作为数组的下标,一定得保证大于等于 0 ,因此 nums[i] <= j;

  • 注意到一种非常特殊的情况:j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时「背包的容积」 j,这也是符合题意的。
    在这里插入图片描述

  • 初始化:dp[0][0] = false,因为候选数 nums[0] 是正整数,凑不出和为0;

  • 输出:dp[len - 1][target],这里 len 表示数组的长度,target 是数组的元素之和(必须是偶数)的一半。

class Solution {
    public boolean canPartition(int[] nums) {
        int len =nums.length;
        int sum=0;
        for(int i=0;i<len;i++){
            sum+=nums[i];
        }
        //sum为奇数则直接返回false
        if(sum%2!=0) return false;

        int target = sum/2;
        //dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j
        //为什么是target+1?需要包含dp[0][0]
        //i可以从0取到len-1
        boolean[][] dp = new boolean[len][target+1];
    
        dp[0][0]=false;//因为候选数 nums[0] 是正整数,凑不出和为0
        if (nums[0] <= target) {
            dp[0][nums[0]] = true;
        }


        //i必须要从1开始
        for(int i=1;i<len;i++){
            for(int j=0;j<=target;j++){
                //特殊判断,否则如果数组中包含nums[i]=target,dp[i][target]=dp[i][0]为false;
                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }

               //nums[i]一定要小于j
                if(nums[i]<j){
                //1.想要使得容量为j,不包含nums[i-1],这时dp[i][j]由dp[i-1][j]决定
                //2.想要使得容量为j,包含nums[i],这时dp[i][j]由dp[i][j-nums[i]]决定              
                    dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]];
                }else{
                    dp[i][j]=dp[i-1][j];
                }
                
            }
        }
        return dp[len-1][target];
    }
}

在这里插入图片描述

考虑空间优化(重要)

说明:这个技巧很常见、很基础,请一定要掌握。

「0-1 背包问题」常规优化:「状态数组」从二维降到一维,减少空间复杂度。

  • 在「填表格」的时候,当前行只参考了上一行的值,因此状态数组可以只设置 2 行,使用「滚动数组」的技巧「填表格」即可;
  • 实际上,在「滚动数组」的基础上还可以优化,在「填表格」的时候,当前行总是参考了它上面一行 「头顶上」 那个位置和「左上角」某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。

为什么从后往前填表?
因为i每加1代表新的一行开始,由于dp[j-num[i]]每次都得使用的是上一行的数据。但是如果你正序的话,那么你在计算dp[j]的时候用到的dp[j-num[i]]是本行的,而不是上一行的,所以用逆序,逆序用到的dp[j-num[i]]是上一行的。(参考图片理解)在这里插入图片描述

public class Solution {

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if ((sum & 1) == 1) {
            return false;
        }

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

        if (nums[0] <= target) {
            dp[nums[0]] = true;
        }
        for (int i = 1; i < len; i++) {
            for (int j = target; nums[i] <= j; j--) {
                if (dp[target]) {
                    return true;
                }
                dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值