s0416
All checks were successful
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
Sainnhe Park 2023-02-09 12:27:42 +08:00
parent 51980ea663
commit 43be72324d
4 changed files with 124 additions and 19 deletions

View File

@ -0,0 +1,13 @@
#ifndef S0416_PARTITION_EQUAL_SUBSET_SUM_HPP
#define S0416_PARTITION_EQUAL_SUBSET_SUM_HPP
#include <vector>
using namespace std;
class S0416 {
public:
bool canPartition(vector<int>& nums);
};
#endif

View File

@ -1,33 +1,42 @@
# 背包问题
`n` 件物品和一个最多能背重量为 `w` 的背包。第 `i` 件物品的重量是 `weight[i]`,得到的价值是 `value[i]`。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。其中 `weight[i]``value[i]` 都是整数。
`n` 件物品和一个最多能背重量为 `bagWeight` 的背包。
`i` 件物品的重量是 `weight[i]`,其价值是 `value[i]`,它们都是正整数。
每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- `dp[i][j]` 表示从下标为 `[0 - i]` 的物品里任意取,放进容量为 `j` 的背包,价值总和最大是多少。
- 递推公式为 `dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]}`
- 不放物品 `i`:由 `dp[i - 1][j]` 推出,即背包容量为 `j`,里面不放物品 `i` 的最大价值,此时 `dp[i][j]` 就是 `dp[i - 1][j]`
- 放物品 `i`:由 `dp[i - 1][j - weight[i]]` 推出,`dp[i - 1][j - weight[i]]` 为背包容量为 `j - weight[i]` 的时候不放物品 `i` 的最大价值,那么 `dp[i - 1][j - weight[i]] + value[i]`(物品 `i` 的价值),就是背包放物品 `i` 得到的最大价值。
- 递推公式:
- 不放物品 `i``dp[i - 1][j]`
- 放物品 `i``dp[i - 1][j - weight[i]] + value[i]`
- 这两种情况选价值最大的那个,即 `dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]}`
- 初始化
- `dp[i][0] = 0`
- `dp[0][j]``j < weight[0]` 时应该为 `0`,否则为 `value[0]`
- 从前往后遍历
- `dp[i][0] = 0` 即当前背包能装的重量为 `0`,就是什么都装不下,当然价值为 `0`
- `dp[0][j]`
- `if (j < weight[0])`, `dp[0][j] = 0` 背包装不下第 `0` 个物品
- `if (j >= weight[0])`, `dp[0][j] = value[0]` 背包能装下第 `0` 个物品
- `i = 0``j = 0` 的情况都初始化完了,因此我们写双重循环的时候 `i``j` 应该从 `1` 开始。
- `i``j` 都从前往后遍历
```cpp
void bag_problem_2d() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int w = 4;
int bagWeight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 之所以初始化 j 的范围是 0 ~ bagWeight + 1 ,是因为我们会索引 dp[i][bagWeight]
vector<vector<int>> dp(weight.size(), vector<int>(bagWeight + 1, 0));
// 初始化
for (int j = weight[0]; j <= w; j++) {
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
// 开始遍历
for (int i = 1; i < weight.size(); i++) { // 遍历物品
for (int j = 0; j <= bagweight; j++) { // 遍历背包容量
for (int j = 1; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i])
dp[i][j] = dp[i - 1][j];
else
@ -43,23 +52,32 @@ void bag_problem_2d() {
递推公式可以修改成:`dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}`
这就是滚动数组的思路,使用条件是上一层可以重复利用,直接拷贝到当前层。从递推公式来看,只要递推公式满足了右侧只用了 `dp[i - 1]` 那么就可以压缩。
这就是滚动数组的思路,当上一层可以重复利用的时候,我们直接把上一层拷贝到当前层。从递推公式来看,只要递推公式满足了右侧只用了 `dp[i - 1]` 那么就可以压缩。
来分析 DP 的思路:
- `dp[j]` 表示容量为 `j` 的背包所能背的物品的最大价值。
- 递推公式为 `dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}`
- 初始化 `dp[j] = 0`
- 这次应该从后往前遍历。每次我们访问 `dp[j - weight[i]] + value[i]` 的时候都把物品 `i` 放进去了一次。为了避免重复放进去,应该从后往前遍历。
- `dp[j]` 表示第 `i` 层容量为 `j` 的背包所能背的物品的最大价值。
- 递推公式:
- `dp[j] = dp[j]`, `if j < weight[i]` 因为如果现在的物品重量比背包容量还大,那背包就装不下了,只能不装现在的这一个,那就是 `dp[i][j] = dp[i - 1][j]`,也就是 `dp[j] = dp[j]`
- `dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}`, `if j >= weight[i]` 这是能装下的情况
- 总结一下就是只有当 `j >= weight[i]` 的时候我们才会调用第二个递推公式,否则 `dp[j]` 不变,于是我们可以将这一个放到第二层 for 循环里,第二层 for 循环的遍历范围是 `weight[i] <= j <= bagWeight`
- 由于滚动数组每次都会覆盖上一层,因此初始化的时候我们只需要将滚动数组作为二维数组的第一层初始化
- `dp[j]``j >= weight[0]` 时应该为 `value[0]` 也就是能装下第 `0` 个物品,否则为 `0`
- 如果初始化了 `i = 0` 的第一层的话,那么双重 for 循环就应该从 `1` 开始。
- 当你实际写代码的时候会发现,双重 for 循环可以用来初始化 `i = 0` 的情况,因此我们只需要初始化 `dp[j] = 0`,然后 `i``0` 开始就行。
- 遍历顺序:
- 物品 `i` 应该从前往后遍历
- 但是背包重量 `j` 应该从后往前遍历
```cpp
void bag_problem_1d() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int w = 4;
int bagWeight = 4;
// 初始化
vector<int> dp(w + 1, 0);
// 之所以初始化 j 的范围是 0 ~ bagWeight + 1 ,是因为我们会索引 dp[bagWeight]
vector<int> dp(bagWeight + 1, 0);
for (int i = 0; i < weight.size(); i++) { // 遍历物品
for (int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
@ -79,3 +97,30 @@ void bag_problem_1d() {
**Q: 一维从后往前的本质是什么?**
**A:** 如果从后往前的话,`dp[j - weight[i]] + value[i]` 就用的是上一层的数据(这才是我们想要的),但如果从前往后的话,`dp[j - weight[i]] + value[i]` 就用的是这一层的数据,这将会导致物品被重复放进去。
## [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
二维数组:
- `dp[i][j]` 表示是否可以从 0 ~ i 选取一些元素,使得总和等于 j
- 递推公式为:
- `dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]`, `j >= nums[i]`
- `dp[i][j] = dp[i - 1][j]`, `j < nums[i]`
- 初始化:
- `dp[0][j] = (j == nums[0])`
- `dp[i][0] = false`
- `i``j` 都从前往后
滚动数组:
- `dp[j]` 表示是否可以从 0 ~ i 选取一些元素,使得总和等于 j
- 递推公式为:
- `dp[j] = dp[j] || dp[j - nums[i]]`, `j >= nums[i]`
- `dp[j] = dp[j]`, `j < nums[i]`
- 因此第二层 for 循环的范围可以直接定成 `nums[i] <= j <= target`,然后调用第一个递推公式
- 初始化:
- `dp[j] = (j == nums[0])` 这是第一层也就是 `i == 0` 的情况
- 这种情况可以写成默认初始化为 `false`,而 `dp[nums[0]] = true`
- 遍历顺序:
- `i` 从前往后,范围是 `1 <= i < length`
- `j` 从后往前,范围是 `nums[i] <= j <= target`

View File

@ -0,0 +1,30 @@
#include "s0416_partition_equal_subset_sum.hpp"
bool S0416::canPartition(vector<int>& nums) {
int len = nums.size();
if (len < 2) return false;
int sum{0};
int maxVal = nums[0];
for (int i{0}; i < len; ++i) {
sum += nums[i];
if (nums[i] > maxVal) maxVal = nums[i];
}
// 如果为奇数则返回 false
if (sum % 2 != 0) return false;
// sum 除以 2 得到我们的目标值
int target = sum >> 1;
// 如果最大值大于目标值那就没必要再找了,因为除了目标值之外的所有值加起来也得不到 target
if (maxVal > target) return false;
// 如果最大值等于目标值也可以直接返回
if (maxVal == target) return true;
// 初始化
vector<int> dp(target + 1, false);
dp[nums[0]] = true;
// 开始遍历
for (int i{1}; i < len; ++i) {
for (int j = target; j >= nums[i]; --j) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}

View File

@ -0,0 +1,17 @@
#include "s0416_partition_equal_subset_sum.hpp"
#include <gtest/gtest.h>
TEST(Problem416, Case1) {
vector<int> nums{1, 5, 11, 5};
bool expected{true};
S0416 solution;
EXPECT_EQ(solution.canPartition(nums), expected);
}
TEST(Problem416, Case2) {
vector<int> nums{1, 2, 3, 5};
bool expected{false};
S0416 solution;
EXPECT_EQ(solution.canPartition(nums), expected);
}