• Medium | LeetCode 416. 分割等和子集 | 0-1背包问题


    416. 分割等和子集

    给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

    注意:

    1. 每个数组中的元素不会超过 100
    2. 数组的大小不会超过 200

    示例 1:

    输入: [1, 5, 11, 5]
    
    输出: true
    
    解释: 数组可以分割成 [1, 5, 5] 和 [11].
    

    示例 2:

    输入: [1, 2, 3, 5]
    
    输出: false
    
    解释: 数组不能分割成两个元素和相等的子集.
    

    方法一: 暴力搜索

    枚举所有的子集, 判断其子集的所有和是否是target/2。

    方法二: 0-1 背包问题

    问题可转化为在若干个物品中选出一些物品, 每个物品只能使用一次, 这些物品恰好能够填满容量为sum/2的背包。

    经典的0-1背包问题: 0-1背包问题:在M件物品取出若干件放在体积为W的背包里,每件物品只有一件,它们有各自的体积 和价值,问如何选择使得背包能够装下的物品的价值最多。

    动态规划的思路:一个一个物品去尝试,一点一点扩大考虑能够容纳的容积的大小,整个过程就像是在填写一张二维表格。

    -设置状态:dp[i] [j]表示考虑下标[0, i]这个区间里的所有整数,在它们当中是否能够选出一些数,使得这些数之和恰好为整数j

    -状态转移方程:

    1. 不选择 nums[i]: dp[i] [j] = dp[i - 1] [j];

    2. 选择 nums [i]:
      ① nums [i] == j, dp[i] [j] = true;
      ② nums[i] < j, dp[i] [j] = dp[i - 1] [j - nums [i]];

    递归

    根据以上的状态转移方程, 可以写出如下的递归的代码

    public boolean canPartition(int[] nums) {
        int target = 0;
        for(int num: nums) {
            target += num;
        }
        if (target % 2 == 0) {
            target /= 2;
        } else {
            return false;
        }
        return knapsack(nums, target) == 1;
    }
    
    private int[][] dp;
    
    public int knapsack(int[] value, int w) {
        dp = new int[value.length+1][w+1];
        return knapsackDp(value, w, value.length - 1);
    }
    
    /**
     * @param value  每个物品的价值
     * @param w      包的最大承重
     * @param index  在前index个物品中选择物品装进背包中
     * @return 装载前index个物品进w承重的包的最大价值
     */
    private int knapsackDp(int[] value, int w, int index) {
        // 递归出口(前0件商品, 也就是第一件商品装进背包承重为w的最大价值)
        if (w < 0) {
            return -1;
        }
        if (index == 0) {
            return (w == 0) ?  1 : -1;
        }
        if (dp[index][w] != 0) {
            return dp[index][w];
        }
        return dp[index][w] = Math.max(
                // 把第index件物品装进背包, 即等价于于【把前index-1件物品装进重量为w - weight[index]的背包能获得的最大价值 + 当前index的价值】
                knapsackDp(value, w - value[index], index - 1),
                // 第index件物品不装进背包, 即等价于【把前index-1件物品装进重量为w的背包能获得的最大价值】
                knapsackDp(value, w, index - 1)
        );
    }
    

    迭代

    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return false;
        }
        int sum = 0, maxNum = 0;
        // 先对所有数字求和
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        // 判断奇偶性, 如果是奇数, 直接返回false
        if (sum % 2 != 0) {
            return false;
        }
        // 获取得到target的值
        int target = sum / 2;
        if (maxNum > target) {
            return false;
        }
        
        boolean[][] dp = new boolean[n][target + 1];
        // 填充第0列的值, 全部填充为true
        for (int i = 0; i < n; i++) {
            dp[i][0] = true;
        }
        // 填充第0行值
        dp[0][nums[0]] = true;
        
        for (int i = 1; i < n; i++) {
            // 从第1行开始从左往右, 从上往下开始遍历
            int num = nums[i];
            for (int j = 1; j <= target; j++) {
                if (j >= num) {
                    // 当前背包容量 大于 当前的体积, 
                    // 那么可有 不把当前物品装进背包dp[i - 1][j]
                    // 把当前物品装进背包两种选择
                    dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
                } else {
                    // 当前背包容量小于物品体积, 那么只能选择将当前物品不装进背包
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n - 1][target];
    }
    

    优化空间复杂度

    第二层的循环我们需要从大到小计算,因为如果我们从小到大更新dp 值,那么在计算 dp[j] 值的时候,dp[j−nums[i]] 已经是被更新过的状态,不再是上一行的dp 值。

    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return false;
        }
        int sum = 0, maxNum = 0;
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        if (sum % 2 != 0) {
            return false;
        }
        int target = sum / 2;
        if (maxNum > target) {
            return false;
        }
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            for (int j = target; j >= num; --j) {
                dp[j] |= dp[j - num];
            }
        }
        return dp[target];
    }
    

  • 相关阅读:
    MySQL调优篇 | 逻辑架构解读(1)
    SQLPlus 在连接时通常有四种方式
    Oracle解决索引碎片功能
    windows2003 ftp 无法下载 解决
    bat记录
    ACCESS字符串操作函数
    缓存和RAID如何提高磁盘IO性能
    TortoiseSVN 命令 (命令行执行工具)
    在RHEL5下实现磁盘分区和磁盘配额
    PS 命令详解
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14330293.html
Copyright © 2020-2023  润新知