学习笔记 | 算法与数据结构 | 动态规划 之 背包问题
经典动态规划:0-1背包问题
【labuladong】0-1背包问题详解
何为背包问题?
形如: 给你一个容量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为wt[i],价值为 val[i],问你用这个背包装物品,最多能装的价值是多少?
第一步 | 关键:【状态】、【选择】
【状态】:【背包剩余容量】、【可选择的物品】
【选择】:当前物品【装入】或者【不装入】背包
动态规划的编程框架(套路):
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1 的所有取值:
for 状态2 in 状态2 的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...)
第二步 | 明确dp数组的定义
dp[i][w] 的含义:
对于 前 i 个物品,当背包容量为 w 时,可以装的最大价值是 dp[i][w]
由此含义=> 确定base case:
-
根据 dp[0][0] 的含义:
对于前 0 个物品,当背包容量为 0 时,可以装的最大价值是 dp[0][0]
对于前 0 个物品,当背包容量为 0 时,可以装的最大价值显然为 0
=> dp[0][0] = 0 -
根据 dp[0][wt] (wt = 1,2,3…)的含义:
对于前 0 个物品,当背包容量为 任意 wt 时,可以装的最大价值是 dp[0][wt]
对于前 0 个物品,当背包容量为 任意 wt 时,可以装的最大价值显然为 0
=> dp[0][wt] = 0 (wt = 1,2,3…) -
根据 dp[i][0] ( i = 1,2,3,…) 的含义:
对于前 0 个物品,当背包容量为 任意 i 时,可以装的最大价值是 dp[i][0]
对于前 0 个物品,当背包容量为 任意 i 时,可以装的最大价值显然为 0
=> dp[i][0] = 0 ( i = 1,2,3,…)
综上base case: dp[0][…] = dp[…][0] = 0
第三步 | 根据【选择】写出【状态转移方程(逻辑和代码)】
3.1. 状态转移逻辑
int[][] dp[N+1][W+1]
dp[0][...] = 0
dp[...][0] = 0
for i in [1...N]:
for w in [1...W]:
dp[i][w] = max(
把物品 i [装进]背包 # 在 w 的约束下,把物品 i 装进背包, 【最大价值】是多少?
把物品 i [不装进]背包 # 在 w 的约束下,把物品 i 不装进背包, 【最大价值】是多少?
)
return dp[N][W]
注意【索引偏移】问题:
因为会考虑第0个物品,0的背包剩余容量,所以dp数组行和列各多1个;
但实际上, val[0] 和 wt[0] 都是表示第1个物品的价值和重量;
即,在遍历dp数组中的 dp[i][…] 表示第 i 个物品,其价值为 val[i-1], 重量为 wt[ i -1]
3.2. 逻辑转为代码的关键:考虑以下两个问题,接着根据dp数组定义推导出代码
- 在 w 的约束下,把物品 i
装进
背包, 【最大价值】是多少? - 在 w 的约束下,把物品 i
不装进
背包, 【最大价值】是多少?
即有
- 在 w 的约束下,把物品 i
装进
背包, 【最大价值】是多少?
=> dp[i][w] = dp[i
][w- wt[i]
] +val[i]
- 在 w 的约束下,把物品 i
不装进
背包, 【最大价值】是多少?
=> dp[i][w] = dp[i-1
][w]
故
dp[i][w] = max(dp[i
][w - wt[i]
] + val[i]
, dp[i-1
][w])
3.3. 状态转移代码
背包问题变体:子集背包问题
本节内容:
- 体会 0-1背包思想 如何运用到 具体的题目上;
- 学习如何将二维动态规划 压缩为 一维动态规划。
输入一个只包含正整数的非空数组 nums,请你写一个算法,判断这个数组是否可以被分割成两个子集,使得两个子集的元素和相等。
对于这个问题,我们可以先对集合求和,得出 sum
,把问题转化为背包问题:
给一个可装载重量为 sum / 2
的背包和 N
个物品,每个物品的重量为 nums[i]
。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
就是背包问题的模型,甚至比我们之前的经典背包问题还要简单一些,下面就直接转换成背包问题,开始套前文讲过的背包问题框架即可。
第一步:明确状态和选择
【状态】:【背包剩余容量】、【可选择的物品】
【选择】:当前物品【装入】或者【不装入】背包
第二步:明确dp数组意义、初始化
dp[i][j] 的含义:
对于前 i 个物品,能否凑出 j 的总重量。若 dp[i][j] = true
,说明能;为false
则不能
base case:
dp[0][0] = true
对于前0 个物品,可以
凑出0 的总重量
dp[0][j] = false
(j = 1,…,sum/2)
对于前0 个物品,没法
凑出大于 0 的总重量
dp[i][0] = true
(i = 1,…,N)
对于任意个物品,可以
凑出 0 的总重量
=> 综上,basecase为 dp[0][j] = false
(j = 1,…,sum/2) 和 dp[i][0] = true
(i = 0,…,N)
第三步:明确状态转移方程,写出代码
// 关键代码
class Solution {
public:
bool canPartition(vector<int>& nums) {
...
// 将问题转化为背包问题
for(int i = 0; i<n;i++){
sum = sum + nums[i];
}
if(sum%2 != 0){
return false;
}
half = sum / 2;
vector<vector<bool>> dp(n+1,vector<bool>(half+1));
// base case:
for(int i = 0;i<=n;i++){
dp[i][0] = true;
}
for(int j = 1; j<= half;j++){
dp[0][j] = false;
}
// 状态转移逻辑
for(int i = 1; i <= n; i++){
for(int j = 1; j< half+1;j++){
if(nums[i-1] > j){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = (dp[i-1][j] || dp[i-1][j-nums[i-1]]);
}
}
}
return dp[n][half];
}
};
第四步 | 考虑【空间优化】
一般空间优化(降维打击)都是用在二维dp上(一维再压就没有了)。那么什么时候能够对二位动态dp的空间进行优化呢?——
观察其状态转移方程,若dp[i][j] 依赖的都是其相邻状态,则可以使用空间压缩的技巧进行空间优化。
完全背包问题
完全背包问题和上述的两种背包问题(经典、子集背包)最大的不同在于:每个物品数量无限
c++的代码执行错误,java没问题