本专题为动态规划算法的应用
目录
0-1背包问题
0-1背包问题模板详情可见我的个人总结【动态规划专题】
1. 状态定义
0-1背包问题的二维状态定义**
f[i][j] : 只选前i个物品, 总体积 <= j 的 Max
2. 状态分析
(1) 当前背包容量不够时(j < v[i]),选不了第 i 个物品,因此前i个物品的最优解和前 i - 1一样
f[i][j] = f[i - 1][j]
(2) 当前背包容量够第 i 个时(j ≥ v[i]),可选第 i 个物品,因此需要继续决策 选 or 不选 第i个物品
- 选:
f[i][j] = f[i - 1][j - v[i] + w[i]
- 不选:
f[i][j] = f[i - 1][j]
- 最后取Max
3. 状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]
4. 优化 :0-1背包问题的一维状态
- 在分析二维状态时,第
i
轮的状态只和第i - 1
轮有关,因此可以去掉数组第一维 - [注] : 每轮遍历 j 的过程中, i 是不变的(都是第 i 轮的状态),
但是当前第i
轮的状态需要第i - 1
轮的状态, 所以需要逆序枚举
一些题目需要转为0-1背包问题去求解
LeetCode 494. 目标和
题目 示例:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
思路
题意要添加正/负号,使得数组和为target
,所以设所有正数的和为s
,数组所有数的和为sum_n
,则所有负数的和为sum_n-s
现在需要求出s - (sum_s -s) = target
的所有方案数
只需求出 s = (sum_n + t)/2
的所有方案数
记s = (sum_n + t)/2
为新的target
-----> 问题转换为从nums中选择一些数字, 使这些数字的和 s = target 所有的方案数(0-1背包问题)
其中num[i]
表示物品的重量,target
表示背包的容量
代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
// +的和 : s
// -的和 : sum_n - s
// s - (sum_n-s) = t
// 2s - sum_n = t
// s = (sum_n + t) / 2
// <==> 问题转换为从nums中选择一些数字, 使这些数字的和 = (sum_n + t)/2, 所有的方案数
target += accumulate(nums.begin(), nums.end(), 0);
if (target < 0 || target % 2) return 0;
target /= 2;
int n = nums.size();
int f[n+1][target+1];
// f[i][j] 代表考虑前 i 个数,当前计算结果为 j 的方案数
// f[0][0] 代表不考虑任何数,凑出计算结果为0的方案数为1种
memset(f, 0, sizeof(f));
f[0][0] = 1;
for(int i = 0; i < n; i++){
for(int c = 0; c <= target; c++) {
if(c < nums[i]) f[i+1][c] = f[i][c]; // 不够选
else f[i+1][c] = f[i][c] + f[i][c-nums[i]]; // 不选 + 选
}
}
return f[n][target];
}
};
空间优化方案一:一维DP数组+逆序枚举
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
target += accumulate(nums.begin(), nums.end(), 0);
if (target < 0 || target % 2) return 0;
target /= 2;
int n = nums.size();
int f[target+1];
memset(f, 0, sizeof(f));
f[0] = 1;
for(int i = 0; i < n; i++){
for(int c = target; c >= 0; c--) {
if(c >= nums[i]) f[c] = f[c] + f[c-nums[i]]; // 不选 + 选
}
}
return f[target];
}
};
空间优化方案二:利用求mod,滚动数组
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
target += accumulate(nums.begin(), nums.end(), 0);
if (target < 0 || target % 2) return 0;
target /= 2;
int n = nums.size();
int f[2][target+1];
// 空间优化方法二:只用2个滚动数组,进行状态转换
memset(f, 0, sizeof(f));
f[0][0] = 1;
for(int i = 0; i < n; i++){
for(int c = 0; c <= target; c++) {
if(c < nums[i]) f[(i+1)%2][c] = f[i%2][c]; // 不够选
else f[(i+1)%2][c] = f[i%2][c] + f[i%2][c-nums[i]]; // 不选 + 选
}
}
return f[n%2][target];
}
};
LeetCode 416. 分割等和子集
题目 示例:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
思路
从nums
中选一些数,使得这些数的总和sum = target
(target = sum_n / 2)
问题转换为0-1背包问题
dp[i][j]
表示从前i个数中选,是否存在【所选数的总和s=j
的方案】
- 如果存在
dp[i][j] = 1
- 如果不存在
dp[i][j] = 0
代码
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum % 2 == 1) return false;
int target = sum / 2, n = nums.size();
bool dp[2][target + 1];
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for(int i = 0; i < n; i++){
for(int j = 0; j <= target; j++){
if(nums[i] > j) dp[(i+1)%2][j] = dp[i%2][j] > 0;
else dp[(i+1)%2][j] = dp[i%2][j] + dp[i%2][j-nums[i]] > 0;
}
}
// cout << dp[n][target];
return dp[n%2][target];
}
};
空间优化:方法同上题,此处略
LeetCode 1049. 最后一块石头的重量 II
题目 示例:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
思路
从stones
中选两组数,记第一组数的总和为sum1
,第二组总和为sum2
要使得最后剩下的一块石头【重量最小】,则需要使得 | sum1 - sum2 |
最小
因此需要从stones
中选一些数,使得这些数总和sum
在不超过sum_n / 2
时,得到的sum
最大
问题转换为0-1背包问题
dp[i][j]
表示从前i个数中选,所选数的总和s<=j
的最大总和s
背包容量为sum_n / 2, 物品价值为sum
代码
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int n = stones.size();
int sum = accumulate(stones.begin(), stones.end(), 0);
int target = sum / 2;
int dp[n+1][target+1]; // 从前n个数中选, 体积不超过sum的最大value(此题value=sum)
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; i++){
for(int j = 0; j <= target; j++){
if(stones[i] > j) dp[i+1][j] = dp[i][j]; // 体积超过 不选
else{
dp[i+1][j] = max(dp[i][j], dp[i][j-stones[i]] + stones[i]);
}
}
}
// cout << dp[n][target];
return sum - 2 * dp[n][target];
}
};
完全背包问题
完全背包问题模板详情可见我的个人总结【动态规划专题】
图源灵神:
状态转移方程
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)
与01背包状态方程的区别在于:第i个物品可以重复选
LeetCode 322. 零钱兑换
题目 示例:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
【注意】ans为最少的硬币个数
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
int dp[n+1][amount+1];
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j <= amount; j++){
if(coins[i] > j){ // 选不了
dp[i+1][j] = dp[i][j];
}else{
// 物品价值看作1
// dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
dp[i+1][j] = min(dp[i][j], dp[i+1][j-coins[i]] +1);
}
}
}
if(dp[n][amount] < 0x3f3f) return dp[n][amount];
return -1;
}
};
空间优化1
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
int dp[2][amount+1];
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j <= amount; j++){
if(coins[i] > j){ // 选不了
dp[(i+1)%2][j] = dp[i%2][j];
}else{
// 物品价值看作1
// dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
dp[(i+1)%2][j] = min(dp[i%2][j], dp[(i+1)%2][j-coins[i]] +1);
}
}
}
if(dp[n%2][amount] < 0x3f3f) return dp[n%2][amount];
return -1;
}
};
空间优化2
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
int dp[amount+1];
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for(int i = 0; i < n; i++){
for(int j = coins[i]; j <= amount; j++){
// 物品价值看作1
// dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
dp[j] = min(dp[j], dp[j-coins[i]] +1);
}
}
if(dp[amount] < 0x3f3f) return dp[amount];
return -1;
}
};
LeetCode 518. 零钱兑换 II
题目 示例:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
【注意】ans为选择的方案数cnt
代码
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n = coins.size();
int dp[n+1][amount+1]; // 从前i个物品中选, 金额=amount 的 数量
memset(dp, 0, sizeof(dp));
dp[0][0] = 1; // 从前0个选amount = 0数量定义为1
for(int i = 0; i < n; i++){
for(int j = 0; j <= amount; j++){
if(coins[i] > j) dp[i+1][j] = dp[i][j]; // 不够选
else dp[i+1][j] = dp[i][j] + dp[i+1][j - coins[i]]; // 不选第i个物品 + 选第i个物品
}
}
return dp[n][amount];
}
};
空间优化
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n = coins.size();
int dp[amount+1]; // 从前i个物品中选, 金额=amount 的 数量
memset(dp, 0, sizeof(dp));
dp[0] = 1; // 从前0个选amount = 0数量定义为1
for(int i = 0; i < n; i++){
for(int j = coins[i]; j <= amount; j++){
dp[j] = dp[j] + dp[j - coins[i]]; // 不选第i个物品 + 选第i个物品
}
}
return dp[amount];
}
};