[动态规划] leetcode 416. 分割等和子集

问题描述:

   分割等和子集:给你一个只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
  例子:输入nums = {1, 5, 11 , 5}; 输出true。

动态规划求解

  这是一个0-1背包问题的变种,也就是每种物品只能选择一次。与之对应的是完全背包问题,选择每种物品的数量是不限制的,可以与另一篇博文对照来看。将非空数组 nums,分为两部分,使得两部分的和相等,该问题等价于从数组中选择部分数字,使得其和等于数组总和的一半。特别的当数组总和sum为基奇数,不可能将数组拆成相等的两部分,直接返回false。
  令 d p [ i ] [ j ] dp[ i ][ j ] dp[i][j]表示,选择数组前 i i i个元素,其和是否能为 j j j。则有:
  当 j > = n u m s [ i − 1 ] j >= nums[i-1] j>=nums[i1]时, 可以选择不添加该元素直接得到j,或者由添加该元素得到j,即
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ∣ ∣ d p [ i − 1 ] [ j − n u m s [ i − 1 ] ] dp[ i ][ j ] = dp[i-1][j] || dp[i-1][j-nums[i-1]] dp[i][j]=dp[i1][j]dp[i1][jnums[i1]]
   当 j < n u m s [ i − 1 ] j < nums[i-1] j<nums[i1]时,当前元素不可选择,否则会直接超出j,有
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[ i ][ j ] = dp[i-1][j] dp[i][j]=dp[i1][j]
   特别的 d p [ 0 ] [ 0 ] = 1 dp[ 0 ][ 0 ] = 1 dp[0][0]=1,因为和为0只有一种情况:不选择任何元素。
   在本示例中有, d p [ 2 ] [ 6 ] = d p [ 1 ] [ 6 − n u m s [ 1 ] ] ∣ ∣ d p [ 1 ] [ 6 ] = d p [ 1 ] [ 1 ] ∣ ∣ d p [ 1 ] [ 6 ] = t r u e dp[2][6] = dp[1][6-nums[1]] || dp[1][6] = dp[1][1] || dp[1][6] = true dp[2][6]=dp[1][6nums[1]]dp[1][6]=dp[1][1]dp[1][6]=true,返回值即为 d p [ n u m s . l e n g t h ] [ s u m / 2 ] = d p [ 4 ] [ 11 ] = t r u e dp[nums.length][sum/2] = dp[4][11] = true dp[nums.length][sum/2]=dp[4][11]=true

在这里插入图片描述

  程序实现需要编写两层循环,分别对应数组长度 i i i和和 j j j。需要注意的是 i i i循环时从1开始,总数 j j j循环时从0开始,第 i i i元素的值为 n u m s [ i − 1 ] nums[i-1] nums[i1]。运行结果为33 ms 42.4 MB。

class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum % 2 != 0){
            return false;
        }
        int target = sum /2;
        boolean[][] dp = new boolean[len +1][target + 1];
        dp[0][0] = true;
        for (int i = 1; i <=len; i++) {
            for (int j = 0; j <=target; j++) {
                if(nums[i-1] <= j){
                    dp[i][j] = dp[i - 1][j] || dp[i-1][j - nums[i-1]];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[len][target];
    }
}

空间复杂度优化

考虑到 d p [ i ] [ j ] dp[ i ][ j ] dp[i][j]的值仅与第 i − 1 i-1 i1行有关系,可以采用滚动数组的思想来降低空闲复杂度。具体的,只维护一维的 d p dp dp数组,其状态更新关系为:
d p [ j ] = d p [ j − n u m s [ i − 1 ] ] + d p [ j ] dp[ j ] = dp[ j-nums[i-1]] + dp[j] dp[j]=dp[jnums[i1]]+dp[j]
在这里插入图片描述
  需要注意的是,第二层的循环我们需要从大到小计算,因为如果我们从小到大更新 d p dp dp值,那么在计算 d p [ j ] dp[j] dp[j] 值的时候, d p [ j − n u m s [ i − 1 ] ] dp[j- nums[i-1]] dp[jnums[i1]]已经是被更新过的状态,不再是上一行的 d p dp dp 值。运行结果为 19 ms 39.6 MB,可以看到空间使用情况有所降低。

class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum % 2 != 0){
            return false;
        }
        int target = sum /2;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 1; i <=len; i++) {
            for (int j = target; j >=nums[i-1]; j--) {
                    dp[j] = dp[j] || dp[j - nums[i-1]];
            }
        }
        return dp[target];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值