📝前言说明:
- 本专栏主要记录本人的动态规划算法学习以及LeetCode刷题记录,按专题划分
- 每题主要记录:(1)本人解法 + 本人屎山代码;(2)优质解法 + 优质代码;(3)精益求精,更好的解法和独特的思想(如果有的话)
- 文章中的理解仅为个人理解。如有错误,感谢纠错
🎬个人简介:努力学习ing
📋本专栏:C++刷题专栏
📋其他专栏:C语言入门基础,python入门基础,C++学习笔记,Linux
🎀CSDN主页 愚润泽
你可以点击下方链接,进行不同专题的动态规划的学习
点击链接 | 开始学习 |
---|---|
斐波那契数列模型 | 路径问题 |
简单多状态(一) | 简单多状态(二) |
子数组系列(一) | 子数组系列(二) |
子序列问题(一) | 子序列问题(二) |
回文串(一) | 回文串(二) |
两个数组dp问题(一) | 两个数组的dp问题(二) |
01背包问题 | 完全背包 |
二维的背包问题 | 其他 |
题单汇总链接:点击 → 题单汇总
题目
- 背包问题开篇导论(重点)
- [DP41 【模板】01背包(开篇引导题)](#DP41 【模板】01背包(开篇引导题))
- [416. 分割等和子集](#416. 分割等和子集)
- [494. 目标和](#494. 目标和)
- [1049. 最后一块石头的重量 II](#1049. 最后一块石头的重量 II)
背包问题开篇导论(重点)
背包问题简介 :在有限容量约束下,通过合理选择物品使得总价值最大化。(我们需要关注:物品属性和背包属性)
- 当每个物品只能选一次:01背包,能选多次完全背包,
- 背包又分为必须装满 / 不必装满
- 01背包问题是其他背包问题的基础!
DP41 【模板】01背包(开篇引导题)
题目链接 → 题目链接

第一问(背包不必选满)
思路
我们把物品从 1 开始编号,我们的dp表也天然的多一行和一列。对于每一个物品,我们考虑选与不选,从第一个物品开始,则 01背包问题 变成 线性dp问题
状态表示
dp[i][j]
:从前i
个物品中挑选,总体积不超过j
,所有选法中,能选出来的最大价值
状态转移方程
线性 dp 状态转移方程分析方式,⼀般都是根据「最后一步」的状况,来分情况讨论:
- 不选第
i
个位置:dp[i][j] = dp[i - 1][j]
- 选第
i
个位置:if(j - v[i] >= 0) dp[i][j] = dp[i - 1][j - v[i]] + w[i]
(要确保选了物品不会超过体积)
初始化
对于dp表
- 当容量为
0
时,无法选择物品,所以价值为0
→ 第一列全为0
- 当从前
0
个物品选择,没有物品能选,所以价值也都为0
→ 第一行全为0
填表顺序
- 看状态转移方程依赖哪些位置的值,经过分析 → 从上往下
返回值
- 返回
dp[n][V]
第二问(背包必须满)
思路:
状态表示(因为必须要装满)
dp[i][j]
:从前i
个物品中挑选,总体积正好等于j
,所有选法中,能挑选出来的最大价值
状态转移方程
- 和第一问一样
- 但是:
dp[i][j]
不一定存在,因为总体积可能凑不到j
- 所以我们用
dp[i][j] == -1
来表示凑不到j
(因为0
被初始化占用了) - 相比于第一问的区别:第一问不要求正好装到
j
,也就是第一问的dp[i][j]
可能根本没装东西0 也 < j
- 所以我们用
- 因此,我们要注意我们使用的
dp
是否是-1
- 对于不选的情况,不需要要判断,因为是直接赋值(把
dp[i - 1][j]
的-1
赋值给dp[i][j]
是ok的,都代表不存在) - 对于选的情况(因为是取
max
,-1 + w[i] > -1
被选到,但是其实前面的dp[i-1][j-v[i]]
是不存在的),所以需要判断dp[i-1][j-v[i]]
是否存在
- 对于不选的情况,不需要要判断,因为是直接赋值(把
初始化
因为多了一种表示不存在的状态
- 当物品为
0
(第一行),容量要满足j
j
为0
:这种情况存在:0
物品,0
容量,所以dp[0][0] = 0
j > 0
:选择0
物品,但是还要有容量,所以不存在这种情况,dp[0][j] = -1
- 当容量为
0
(第一列),从前i
个物品选- 对于每个物品都不选,则可以容量为
0
,所以第一列全为0
- 对于每个物品都不选,则可以容量为
填表顺序和返回值
- 和第一问一样
代码(未优化写法):
cpp
int main() {
// 读取数据
int n, V;
cin >> n >> V;
// 从 1 开始编号
vector<int> v(n + 1);
vector<int> w(n + 1);
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// dp 填表(第一问)
vector<vector<int>> dp1(n + 1, vector<int>(V + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) {
dp1[i][j] = dp1[i - 1][j];
if (j - v[i] >= 0)
dp1[i][j] = max(dp1[i - 1][j], dp1[i - 1][j - v[i]] + w[i]);
}
}
// dp 填表第二问
vector<vector<int>> dp2(n + 1, vector<int>(V + 1, 0));
for(int j = 1; j <= V; j++) dp2[0][j] = -1; // 初始化
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) {
dp2[i][j] = dp2[i - 1][j];
// j - v[i]: 确保物体的体积不会超过容量
// dp中的 j - v[i]: 确保 + 上物体以后,总容量 == j
if(j - v[i] >= 0 && dp2[i - 1][j - v[i]] != -1)
dp2[i][j] = max(dp2[i - 1][j], dp2[i - 1][j - v[i]] + w[i]);
}
}
cout << dp1[n][V] << endl;
cout << (dp2[n][V] == -1 ? 0 : dp2[n][V]) << endl;
}
时间复杂度: O ( n V ) O(nV) O(nV)
空间复杂度: O ( n V ) O(nV) O(nV)
优化
- 利用滚动数组做空间上的优化(可直接在原始代码上修改)
- 第
i
行的状态只依赖第i - 1
行。因此我们可以只用两个数组来填写,当第i
行填完以后,把第i - 1
行"变成"第i + 1
行(即:无用变有用)然后继续填写。
更进一步 :
只用一行数组,我们在第i - 1
行上直接填数据,变成第i
行:
- 我们要观察我们到底依赖的是第
i - 1
行的哪些值 - 然后注意覆盖问题:选择填改行数组的方向
- 本题我们可以选择从右往左填:用完以后再覆盖
改代码:
- 删除横坐标
- 修改
j
的遍历顺序
代码:
cpp
int main() {
// 读取数据
int n, V;
cin >> n >> V;
// 从 1 开始编号
vector<int> v(n + 1);
vector<int> w(n + 1);
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// dp 填表(第一问)
vector<int> dp1(V + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--)
dp1[j] = max(dp1[j], dp1[j - v[i]] + w[i]);
}
// dp 填表第二问
vector<int> dp2(V + 1, 0);
for(int j = 1; j <= V; j++) dp2[j] = -1; // 初始化
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
if(dp2[j - v[i]] != -1)
dp2[j] = max(dp2[j], dp2[j - v[i]] + w[i]);
}
}
cout << dp1[V] << endl;
cout << (dp2[V] == -1 ? 0 : dp2[V]) << endl;
}
时间复杂度: O ( n V ) O(nV) O(nV),但是是有降低的
空间复杂度: O ( V ) O(V) O(V)
注意:不要强行解释优化后的状态表示以及状态转移方程(我们只是借助了变化的特点来优化罢了)
416. 分割等和子集
题目链接:https://leetcode.cn/problems/partition-equal-subset-sum/description/
优质解
思路:
- 我们带着把数组划分成两部分的思想就很难解题
- 因为最后要求把数组分割成两个元素和相等的子集,这意味着每个子集的元素和
= sum / 2
- 所以我们只需要看能不能在数组中挑选出一定的元素,使元素和
= sum / 2
- 于是问题就变成了01背包问题
代码:
cpp
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = 0;
for(auto x:nums)
sum += x;
if(sum % 2 != 0) return false;
vector<vector<bool>> dp(n + 1, vector<bool>(sum / 2 + 1, 0));
for(int i = 0; i <= n; i++) dp[i][0] = true; // 第一列初始化全为 0
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= sum / 2; j++)
{
dp[i][j] = dp[i - 1][j]; // 不选第 i 个数
if(j >= nums[i - 1] && dp[i - 1][j - nums[i - 1]]) // 选择第 i 个数且前面的和也存在
dp[i][j] = true;
}
}
return dp[n][sum / 2];
}
};
时间复杂度: O ( n ∗ s u m ) O(n*sum) O(n∗sum)
空间复杂度: O ( n ∗ s u m ) O(n*sum) O(n∗sum)
优化:
cpp
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = 0;
for(auto x:nums)
sum += x;
if(sum % 2 != 0) return false;
vector<bool> dp(sum / 2 + 1, false);
dp[0] = true;
for(int i = 1; i <= n; i++)
{
for(int j = sum / 2; j >= nums[i - 1]; j--)
{
if(dp[j - nums[i - 1]]) // 选择第 i 个数且前面的和也存在
dp[j] = true;
}
}
return dp[sum / 2];
}
};
时间复杂度: O ( n ∗ s u m ) O(n*sum) O(n∗sum) ,但是是有变快的
空间复杂度: O ( s u m ) O(sum) O(sum)
494. 目标和
题目链接:https://leetcode.cn/problems/target-sum/description/
优质解
思路:
- 转换成上一题,但是这个初始化和第一列的填写特别恶心
代码:
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
// 从数组中选数,和 == (sum + target) / 2;
int sum = 0, n = nums.size();
for(int i = 0; i < n; i++) sum += nums[i];
if(sum < abs(target) || (sum + target) % 2 != 0) return 0;
int m = (sum + target) / 2;
// dp[i][j] : 从前 i 个数里面选,能选出的`和 == j`的选法总数
// nums[i]位置:不选 + 选(两种情况的方法总和)
// dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 因为状态转移方程要依赖于上一行,所以我们先初始化第一行
// 当不选任何数字时, 和只能是 0
dp[0][0] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++) // 和为 0, 因为数组内元素可能有多个 0 ,所以从 前 i 个数里面选也不知道有几种选法
{
dp[i][j] += dp[i - 1][j];
if(j >= nums[i - 1])
dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
}
return dp[n][m];
}
};
时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
空间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
优化:
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int sum = 0, n = nums.size();
for(int i = 0; i < n; i++) sum += nums[i];
if(sum < abs(target) || (sum + target) % 2 != 0) return 0;
int m = (sum + target) / 2;
vector<int> dp(m + 1, 0);
dp[0] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= nums[i - 1]; j--)
{
dp[j] += dp[j - nums[i - 1]];
}
}
return dp[m];
}
};
时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
空间复杂度: O ( m ) O(m) O(m)
1049. 最后一块石头的重量 II
题目链接:https://leetcode.cn/problems/last-stone-weight-ii/description/
个人解
思路:
- 把前面的题目写好了,这道题就不难
- 选数,数的和 <= sum / 2(最大体积) -> 变成 01 背包问题(只不过石头价值和体积相同)
- 右边的数一定能把左边的数"粉碎"到只剩最小的吗?
- 一定的, 左边和右边先各自内部粉碎,剩下的数左边
<=
右边 再粉碎
- 一定的, 左边和右边先各自内部粉碎,剩下的数左边
屎山代码:
cpp
class Solution {
public:
int lastStoneWeightII(vector<int>& stones)
{
int sum = 0;
for(auto x: stones) sum += x;
int n = stones.size(), m = sum / 2;
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= m; j++)
{
dp[i][j] = dp[i - 1][j];
if(j >= stones[i - 1])
dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
}
return sum - 2 * dp[n][m];
}
};
时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
空间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
优化:
cpp
class Solution {
public:
int lastStoneWeightII(vector<int>& stones)
{
int sum = 0;
for(auto x: stones) sum += x;
int n = stones.size(), m = sum / 2;
vector<int> dp(m + 1, 0);
for(int i = 1; i <= n; i ++)
{
for(int j = m; j >= stones[i - 1]; j--)
dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
}
return sum - 2 * dp[m];
}
};
时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
空间复杂度: O ( m ) O(m) O(m)
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!