DP问题常见模型
DP问题常见模型
- 背包问题(首先转化问题&&再直接套用背包模板)
- 01背包问题例1:
- 步骤 1 :问题拆解
- 步骤 2 :套用01背包问题
- 步骤 3 :实现
- 步骤 4 :问题变种 && 如何应对
- 01背包问题例2:
- 步骤 1 :问题拆解
- 步骤 2 :套用01背包问题
- 步骤 3 :实现
- 完全背包问题例1:
- 套用完全背包问题
- 实现
背包问题(首先转化问题&&再直接套用背包模板)
1、何时使用?当我们把题目拆解,并发现,题目本质是
在一个给定的范围内,每个元素都有各自的价值+所占体积,题目会给一个总体积
我们需要求得一个子序列,针对每个元素都面临着选/不选进入该子序列中
最终可能求价值最大/小、方案数、是否满足分割条件等等
此时就是非常经典的背包问题
2、背包问题分类以及区别
- 01背包:每个元素只考虑一次,在选择物品i后,不可停留,因为不可以重复选择
- 完全背包:每个元素可以考虑多次,在选择物品i后,可以继续停留选择物品i,因为可以重复选择
3、01背包经典的场景&&转移方程(图片来自于B站灵茶山艾府:0-1背包 完全背包【基础算法精讲 18】)
- 需要注意,这个转移方程式,只是众多01背包问题的一种,只是展现了选(剩余体积减少)或者不选(剩余体积不变)这个问题,具体方程式需要根据题中要求,比如方案数就是二者作和,判断是否可以成功划分使用的是布尔判断
- 使用i && i-1,在处理第一个元素时容易越界,所以把i全部+1,在转移数组上面增加一行,转移方程是就变成了上图中第二行
- 这时候,数组F第一行变成了初始化的关键点,这里会分成“恰好”“至多”“至少”,三类问题,我们将在后面给出不同的初始化方案;c-W[I]这一步骤,也会受到前面三类问题的影响,我们在后面分别介绍,要根据不同的问题选择不同的初始化&&应对策略。
- 最好是:外层循环元素,从0到n-1;内层循环可能的体积大小,[0,m]
4、完全背包转移方程
5、注意:背包问题一般都会被一层帽子盖住,一定要善于分解问题,发现本质的背包问题,直接套板子解决
01背包问题例1:
我们以力扣494.目标和为例讲解这个问题。
步骤 1 :问题拆解
1、数组中每个数都是正整数;我们需要选择一个子序列前面使用正好,剩下的子序列使用负号,二者之和==target
2、所有正数之和设为p,选作添加负数子序列的和(绝对值)=sum - p,所以:
p + (-1)*(sum-p) = target
=> 2*p - sum = target
=> p = (sum + target) / 2
3、正数之和p,一定是正整数,所以sum + target,必须大于等于0且为偶数,否则不满足题意
4、那么问题就转化为:
在nums上寻找一个序列,对于每个元素都是选不不选,到最后让序列之和 == p
=> 马上转化为01背包问题(因为不能重复选择)
步骤 2 :套用01背包问题
1、dp数组多加一行,所以第一个维度是n+1,第二个维度要保证取值是[0,target],所以是target + 1这么长
2、因为是恰好值 == target,所以只有dp[0][0]才是刚好装满,方案数=1,其余都是非法情况,为0.
3、转移方程:本题要找方案数,选和不选都是方案,所以要做和才是总方案数
4、因为是恰好值 == target,所以在剩余空间不足的时候,绝对不可能继续选择i,单独判断剩余空间大小。
步骤 3 :实现
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
for (int x : nums) {//sum + target
target += x;
}
if (target < 0 || target % 2 != 0) {//su异常情况排除
return 0;
}
target /= 2;//计算最终target
vector<vector<int>> dp(n + 1, vector<int>(target + 1));
dp[0][0] = 1;//初始化
for (int i = 0;i < n;i++) {
for (int j = 0;j <= target;j++) {
if (nums[i] > j) {//s剩余空间不足以选择i,只能不选择
dp[i + 1][j] = dp[i][j];
}
else {
//s剩余空间足够,因为是求方案数,所以选与不选方案数之和才是才是总共的方案数
dp[i + 1][j] = dp[i][j] + dp[i][j - nums[i]];
}
}
}
return dp[n][target];
}
};
步骤 4 :问题变种 && 如何应对
1、如果是值至多是target:
当剩余空间j < 当前物品体积,因为我们至多使用target空间(不许超,只能是少于或者刚刚好),所以我们不再往里放东西:
1)双重循环内部代码不变,当空间不足该物品选不了
2)初始化代码要改变:dp[0][0] == dp[0][>0] == 1:
因为前者代表刚刚好放得下 ;后者代表放不进去了,根据题意,我们也没必要再放,此时也是一个成功的方案,所以也初始化为1
2、如果是值至少是target:
当剩余空间j < 当前物品体积,因为我们至少使用target空间(必须超,只能是超出或者刚刚好),所以我们一定再往里面加东西
1)初始化代码不变,因为dp[0][>0]代表放不进去了选择不继续添加,与题意是相反的,所以不是方案数,初始为0
2)根据题意,如果空间超出会出现负数索引,但都和dp[0][0]表达一个意思,是一个可行的方案,所以将之划分到dp[0][0]
3)综上,转移方程不需要判断nums[i] 与 j关系,一定考虑添加该物品,直接使用:
dp[i + 1][j] = dp[i][j] + dp[i][max(j - nums[i],0)];
01背包问题例2:
我们以力扣416. 分割等和子集为例讲解这个问题。
步骤 1 :问题拆解
1、 根据题意,我们的目标是:将整个数组分割成两个子序列,每个子序列和分别是 x1 x2
x1 == x2 && x1 + x2 = sum
=> x1 = x2 = sum / 2
2、所以这个问题就变成了:
我们在0到nums.size()-1的范围内,挑选几个元素,使之元素和为 sum / 2,每个元素都只能选一次。
很明显的 01背包问题,背包容量为 sum / 2,物品的占用体积为 nums[i],对于每个物品而言我们就是:选或者不选。
步骤 2 :套用01背包问题
1、我们求解的是:能否划分出这个背包,所以状态存储的是布尔值,能不能成功划分;选或不选,俩方案一个走通就行,所以中间取“或运算”
2、转移方程:
dp[i][j] = dp[i-1][j](第i个物品我们不选) || dp[i-1][j-nums[i]](第i个物品如果空间够,我们选)
3、初始化,当恰好分割dp[0][0] = true;有剩余空间dp[0][>0] = false
步骤 3 :实现
class Solution {
public:
bool canPartition(vector<int>& nums) {
/*
01背包问题,模板:dp[i][j]表示从0到i个物品中,能否选出若干物品,使得它们的体积和为j
我们从后向前思考(最后代码从前到后写):
dp[i][j] = dp[i-1][j](第i个物品我们不选) || dp[i-1][j-nums[i]](第i个物品我们选)
1、这里我们要注意,如果j<nums[i],那么我们就不能选第i个物品了,dp[i-1][j-nums[i]]直接就是不能选,这个选项判负,所以再选物品前,一定看好能不能选择它
2、01背包,每个物品只能选一次,所以选完就走
3、每个物品选或者不选,我们最后要看的是有没有满足要求的方案,所以数组存储bool值,当恰好装满背包时,dp[i][j] = true,并且两个方案一个走通就行
4、如果我们处理第一个物品时,i==0,i-1就会越界,所以我们需要一个额外的行来处理这个问题,我们在dp数组最上面多加一行,表示这个状态,dp[0][0] = true;dp[0][>0] = false,所以第一项整体加一
dp[i + 1][j] = ((j >= x && dp[i][j - nums[i]]) || dp[i][j]);
*/
//总价值在遍历时,最小0最大sum
int sum = 0;
for (int x : nums) {
sum += x;
}
if (sum % 2 != 0) {
return false;
}
sum /= 2;
int n = nums.size();
vector<vector<bool>> dp(n + 1, vector<bool>(sum + 1));
dp[0][0] = true;
for (int i = 0;i < n;i++) {
int x = nums[i];
for (int j = 0;j <= sum;j++) {
dp[i + 1][j] = ((j >= x && dp[i][j - x]) || dp[i][j]);
}
}
return dp[n][sum];
}
};
完全背包问题例1:
我们以力扣322. 零钱兑换为例讲解这个问题。
套用完全背包问题
1、分析题目
- 每个元素可重复选择;
- 一个范围内选择子序列,总体积恰好达到amount;
- 硬币数量要最少,所以状态中动态维护最少的硬币数量;
- 每个硬币价值为1,代表我选择了这个硬币,所以选择的话,硬币数量要+1
2、所以使用完全背包:
1)初始化:dp[0][0] = 0代表刚刚好总体积恰好达到amount,此时数量==0,dp[0][>0]都是失败情况,因为要最小值,所以初始化无穷大
2)dp[i + 1][j] = min(dp[i][j], dp[i + 1][j - coins[i]] + 1);(如果coins[i] > j,则硬币不可选)
实现
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<vector<int>> dp(n + 1, vector<int>(amount + 1, INT_MAX / 2));
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 {
dp[i + 1][j] = min(dp[i][j], dp[i + 1][j - coins[i]] + 1);
}
}
}
return dp[n][amount] == INT_MAX / 2 ? -1 : dp[n][amount];//如果最终答案是无穷大,说明没有可行方案
}
};