动态规划-416. 分割等和⼦集

120 阅读1分钟

4月日新计划更文活动 第9天

前言

动态规划专题,从简到难通关动态规划。

每日一题

今天的题目是 416. 分割等和子集,难度为中等

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

题解

回溯

回溯的本质就是暴力拆解,通过遍历所有的情况来得到想要的答案,这道题最简单的回溯就是新建一个数组,不断地往其中添加数和弹出数,保证每次添加的都是数组里面没有的元素就可以

function canPartition(nums) {
    const sumHalf = nums.reduce((x,y)=>x+y)/2

    let res = false
    let path = []
    const dfs = (n) => {
        if(path.reduce((x,y)=>x+y, 0) == sumHalf) return res=true
        for(let i=n;i<nums.length;i++) {
            path.push(nums[i])
            dfs(i+1)
            path.pop()
        }
    }
    dfs(0)
    return res
};

image.png

image.png

可以进行一些剪枝优化,但是结果依然超时

function canPartition(nums) {
    const sumHalf = nums.reduce((x,y)=>x+y)/2
	
    let res = false
    let path = 0
    const dfs = (n) => {
        if(res==true) return
        if(path == sumHalf) return res=true
        for(let i=n;i<nums.length;i++) {
            path+=nums[i]
            dfs(i+1)
            path-=nums[i]
        }
    }
    dfs(0)
    return res
};

动态规划

碰到一道题的时候,我们可以通过一些特点来判断他是不是可以转为01背包的问题。

  1. 问题是否具有求最大价值或最小价值的特点,即问题是否需要优化一个目标函数;

  2. 问题是否具有完全背包问题或 0-1 背包问题的特点,即每个物品最多只能选一次或者可以选无限次;

  3. 问题中是否涉及到物品的大小和空间的限制,即背包容量是否有限制;

对于这道题来说,每个元素只能选或者不选,背包的最大体积为 sum/2 元素的值就是物品的价值,我们需要找到总和刚好为 sum/2 的组合。

所以我们可以用01背包的思路来解决这道题,首先确定01背包的dp数组的定义

dp[i][j] 表示前 i 个数字能否填满容量为 j 的背包。

对于第 i 个数字,有两种可能,要么装入背包,要么不装入背包。如果不装入,则 dp[i][j] = dp[i-1][j];如果装入,则 dp[i][j] = dp[i-1][j-nums[i-1]],因为此时要去找填满容量为 j - nums[i-1] 的子问题。 据此,我们得出状态转移方程如下:

dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];

初始化dp数组

根据dp数组的含义,前i个数能否填满大小为j的背包,对于元素有两种选择,选或者不选,那么要是每个都不选的话,能够填充的大小一定为0,所以背包大小为0的时候一定是初始化为 true。

确定遍历顺序

遍历一遍元素,在遍历一遍背包

推导dp数组

dp0123456789101112
0TFFFFFFFFFFFF
1TTFFFFFFFFFFF
2TTFFFTFFFFFFF
3TTFFFTFFFTFFF
4TTFFFTFFTTFTF

值得注意的是,每次我们在决定是否选择i的时候,需要去判断一下,选择了i会不会导致已经超出了背包,超出了就一律不做选择

function canPartition(nums) {
    // 计算数组的总和
    let sum = nums.reduce((x, y) => x + y)
    // 如果总和是奇数,则无法分成两个和相等的子集
    if (sum % 2 !== 0) {
        return false;
    }
    // 初始化二维数组
    let dp = new Array(nums.length + 1).fill(0).map(e => [true])
    for (let j = 1; j <= sum / 2; j++) {
        dp[0][j] = false;
    }
    // 进行状态转移
    for (let i = 1; i <= nums.length; i++) {
        for (let j = 1; j <= sum / 2; j++) {
            dp[i][j] = dp[i - 1][j];
            if (j - nums[i - 1] >= 0) {
                dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
    }
    // 返回结果
    return dp[nums.length][sum / 2];
}

image.png