前言
动态规划专题,从简到难通关动态规划。
每日一题
今天的题目是 416. 分割等和子集,难度为中等
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入: nums = [1,5,11,5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入: nums = [1,2,3,5] 输出: false 解释: 数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= 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
};
可以进行一些剪枝优化,但是结果依然超时
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背包的问题。
-
问题是否具有求最大价值或最小价值的特点,即问题是否需要优化一个目标函数;
-
问题是否具有完全背包问题或 0-1 背包问题的特点,即每个物品最多只能选一次或者可以选无限次;
-
问题中是否涉及到物品的大小和空间的限制,即背包容量是否有限制;
对于这道题来说,每个元素只能选或者不选,背包的最大体积为 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数组
| dp | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | T | F | F | F | F | F | F | F | F | F | F | F | F |
| 1 | T | T | F | F | F | F | F | F | F | F | F | F | F |
| 2 | T | T | F | F | F | T | F | F | F | F | F | F | F |
| 3 | T | T | F | F | F | T | F | F | F | T | F | F | F |
| 4 | T | T | F | F | F | T | F | F | T | T | F | T | F |
值得注意的是,每次我们在决定是否选择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];
}