Leetcode——分割等和子集 / 目标和 (01背包 / DP)
Posted Yawn,
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode——分割等和子集 / 目标和 (01背包 / DP)相关的知识,希望对你有一定的参考价值。
1. 分割等和子集
(0)背包问题
与494. 目标和 类似,属于01背包问题,可以把问题抽象为“给定一个数组和一个容量为x的背包,求有多少种方式让背包装满(有多少种子集能让子集之和等于背包容量"
附上01背包问题的模版:
//01背包
for (int i = 0; i < n; i++) {
for (int j = m; j >= V[i]; j--) {
f[j] = max(f[j], f[j-V[i]] + W[i]);
}
}
//完全背包
for (int i = 0; i < n; i++) {
for (int j = V[i]; j <= m; j++) {
f[j] = max(f[j], f[j-V[i]] + W[i]);
}
}
- f[j] 代表当前背包容量为 j 的时候,可以获取的最大价值。
- 完全背包是从左向右遍历,f[j-V[i]]取到的是拿第i个物品时的值,是新值,可以重复无限的拿,f[j]的值也会随之增加。
V:商品的体积
W:商品的价值
(1)DP
- 解决的基本思路是:物品一个一个选,容量也一点一点增加去考虑,这一点是「动态规划」的思想,特别重要。
- 具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始考虑。很多时候,我们需要考虑这个容量为 0 的数值。
状态转移方程:
- 状态定义: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 是数组的元素之和(必须是偶数)的一半。
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;
// 创建二维状态数组,行:物品索引,列:容量(包括 0)
//定义dp,dp[i][j]表示nums[0:i+1]中是否存在一些元素的和等于j+1
boolean[][] dp = new boolean[len][target + 1];
// 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
// 再填表格后面几行
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[len - 1][target];
}
}
(2)DP(优化)
递推公式:dp[i] = dp[i] + dp[i-num] ,对于当前的第i个物品,有拿和不拿两种情况,dp[i]表示不拿的情况,dp[i-num]表示拿的情况,因此要将两者相加
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
int sum = 0;
for (int n : nums) {
sum += n;
}
//整数相加不可能得小数
if (sum % 2 != 0)
return false;
//分割子集的和
int W = sum / 2;
int[] dp = new int[W + 1];
dp[0] = 1;
for (int num : nums) {
for (int i = W; i >= num; i--) {
//递推公式:dp[i] = dp[i] + dp[i-num]
//对于当前的第i个物品,有拿和不拿两种情况,dp[i]表示不拿的情况,dp[i-num]表示拿的情况,因此要将两者相加
dp[i] += dp[i - num];
}
}
return dp[W] != 0;
}
}
2 . 目标和
(1)DFS
数据范围只有 20不大,而且每个数据只有 +/-+/− 两种选择,因此可以直接使用 DFS 进行「爆搜」。
而 DFS 有「使用全局变量维护」和「接收返回值处理」两种形式。
接收返回值处理:
u: 数值下标
cur: 当前结算结果
class Solution {
public int findTargetSumWays(int[] nums, int t) {
return dfs(nums, t, 0, 0);
}
int dfs(int[] nums, int t, int u, int cur) {
//串联完所有整数
if (u == nums.length) {
return cur == t ? 1 : 0;
}
int left = dfs(nums, t, u + 1, cur + nums[u]);
int right = dfs(nums, t, u + 1, cur - nums[u]);
return left + right;
}
}
使用全局变量维护:
class Solution {
int ans = 0;
public int findTargetSumWays(int[] nums, int t) {
dfs(nums, t, 0, 0);
return ans;
}
void dfs(int[] nums, int t, int u, int cur) {
//串联完所有整数
if (u == nums.length) {
ans += cur == t ? 1 : 0;
return;
}
dfs(nums, t, u + 1, cur + nums[u]);
dfs(nums, t, u + 1, cur - nums[u]);
}
}
(2)记忆化DFS
- DFS 的函数签名中只有「数值下标 u」和「当前结算结果 cur」为可变参数,考虑将其作为记忆化容器的两个维度,返回值作为记忆化容器的记录值。
class Solution {
public int findTargetSumWays(int[] nums, int t) {
return dfs(nums, t, 0, 0);
}
Map<String, Integer> cache = new HashMap<>();
int dfs(int[] nums, int t, int u, int cur) {
String key = u + "_" + cur;
if (cache.containsKey(key)) return cache.get(key);
if (u == nums.length) {
cache.put(key, cur == t ? 1 : 0);
return cache.get(key);
}
int left = dfs(nums, t, u + 1, cur + nums[u]);
int right = dfs(nums, t, u + 1, cur - nums[u]);
cache.put(key, left + right);
return cache.get(key);
}
}
(3)DP
class Solution {
public int findTargetSumWays(int[] nums, int t) {
int n = nums.length;
int s = 0;
for (int i : nums)
s += Math.abs(i);
if (t > s)
return 0;
int[][] f = new int[n + 1][2 * s + 1];
f[0][0 + s] = 1;
for (int i = 1; i <= n; i++) {
int x = nums[i - 1];
for (int j = -s; j <= s; j++) {
if ((j - x) + s >= 0)
f[i][j + s] += f[i - 1][(j - x) + s];
if ((j + x) + s <= 2 * s)
f[i][j + s] += f[i - 1][(j + x) + s];
}
}
return f[n][t + s];
}
}
以上是关于Leetcode——分割等和子集 / 目标和 (01背包 / DP)的主要内容,如果未能解决你的问题,请参考以下文章