代码随想录第36天第37天 | 01背包应用 及 完全背包⭐
1049. 最后一块石头的重量 II
1049. 最后一块石头的重量 II
1. 思路是既然要找粉碎之后的最小的剩余重量
2. 则把石头分为两堆,让左右两边的重量差最小,则最后剩余的就是最小剩余重量
3. 实际操作可以用:使石头的重量尽可能接近sum/2,这样其实就转化为01背包问题——
每个物体只有一个,考虑选或者不选
则下面是dp的分析:
1. 使用一维数组方式,具体的在:代码随想录第34天 | ⭐背包问题-CSDN博客可以找到二维dp是如何变成一维的
2. dp[j]表示容量为j,可以装下的最大重量dp[j]
3. 递推公式:dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);//前一个是不选的情况,后一个是选择的情况
4. 初始化dp[0]=0,dp大小应该为重量的范围的一半(因为我们要求的是尽可能接近sum/2)
5. j倒序遍历,i正序遍历
——倒序遍历的原因是,先遍历第1个物品,若从j=其大小开始到sum/2,则下一个物品遍历时会覆盖第一个物品的dp[j]
若使用dp[j],每一轮都会一个dp[j]数组
class Solution {
public:int lastStoneWeightII(vector<int>& stones) {//把数组分成两块,这两块的重量尽可能相同,其差值就是最终的结果,其实可以只看一个的重量,让其尽可能接近sum/2,//dp[j]表示容量为j,最大重量dp[j]//dp[0]=0;//先遍历数组中的元素再遍历其中的容量int sum=0;for(int i=0;i<stones.size();i++){sum+=stones[i];}int t=sum/2;vector<int> dp(15000,0);for(int i=0;i<stones.size();i++){for(int j=t;j>=stones[i];j--){dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);}}return sum-dp[t]*2;}
};
494. 目标和
大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的提问 我们还会用到。
494.目标和
这题其实很像回溯问题(计算总和且是排序,下面是代码:
class Solution {
public:int res = 0;void backtrack(vector<int>& nums, int target, int index, int sum) {if (index == nums.size()) {if (sum == target) {res++;}return;}// 选择加号backtrack(nums, target, index + 1, sum + nums[index]);// 选择减号backtrack(nums, target, index + 1, sum - nums[index]);}int findTargetSumWays(vector<int>& nums, int target) {backtrack(nums, target, 0, 0);return res;}
};
那这个题如何转化为动态规划问题呢?下面是具体分析
1. 首先要所有元素前放上加号或减号,每个元素有两种选择,若我们把其分成前面放+号的集合,以及前面放-号的集合。
2. 设两个集合名分别为:z[],f[]
3. z的和+f的和=总的sum
4. z的和-f的和=target
5. 这两个公式放一起可以变为——z=(target+sum)/2;
现在就知道了z的和要达到这样一个数值才可达到要求
则可转化问题为——有一个容量为(target+sum)/2的背包,装满它共有多少种可能?
这就是妥妥的背包问题了
1. dp[j] 表示容量为j,dp[j]种方法使其装满
2. 初始化dp[0]=1
3. 推导公式:根据dp[j]的含义去推导,同时可以举几个例子,观察得出结论
对于每个数字
nums[i]
,我们考虑是否选择它:
如果不选
nums[i]
:方法数 =dp[j]
(保持不变)如果选择
nums[i]
:方法数 =dp[j - nums[i]]
(需要前面已经有方法装满j-nums[i]
),则再选择nums[i]就可以装满了dp[j] = dp[j] + dp[j - nums[i]]4. 推导,依旧是i从0开始往后,j从最大容量到nums[i]
上面示例的具体推导方法:
总和sum = 5
z = (3+5)/2 = 4
问题:从5个1中选若干个数,和为4的方法数初始: dp = [1,0,0,0,0] (dp[0]=1,其他为0)
处理第1个1:
dp[1] += dp[0] → dp[1] = 0+1 = 1
dp = [1,1,0,0,0]处理第2个1:
dp[2] += dp[1] → dp[2] = 0+1 = 1
dp[1] += dp[0] → dp[1] = 1+1 = 2
dp = [1,2,1,0,0]处理第3个1:
dp[3] += dp[2] → dp[3] = 0+1 = 1
dp[2] += dp[1] → dp[2] = 1+2 = 3
dp[1] += dp[0] → dp[1] = 2+1 = 3
dp = [1,3,3,1,0]处理第4个1:
dp[4] += dp[3] → dp[4] = 0+1 = 1
dp[3] += dp[2] → dp[3] = 1+3 = 4
dp[2] += dp[1] → dp[2] = 3+3 = 6
dp[1] += dp[0] → dp[1] = 3+1 = 4
dp = [1,4,6,4,1]处理第5个1:
dp[4] += dp[3] → dp[4] = 1+4 = 5 ✓
dp[3] += dp[2] → dp[3] = 4+6 = 10
dp[2] += dp[1] → dp[2] = 6+4 = 10
dp[1] += dp[0] → dp[1] = 4+1 = 5
dp = [1,5,10,10,5]
class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int sum = 0;for (int num : nums) {sum += num;}// 边界条件判断if (abs(target) > sum) return 0;if ((target + sum) % 2 != 0) return 0;int bagSize = (target + sum) / 2;if (bagSize < 0) return 0;vector<int> dp(bagSize + 1, 0);dp[0] = 1; // 关键初始化for (int i = 0; i < nums.size(); i++) {for (int j = bagSize; j >= nums[i]; j--) {dp[j] += dp[j - nums[i]];}}return dp[bagSize];}
};
474.一和零(
通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
474.一和零
这题是分为一个0数量,一个1数量的集合,其中0数量集合的容量为m,1数量集合的容量为n.
实际上这些只能在同一个集合里进行判断其两个不同的维度:0和1的数量(容量
这题是零一背包,每个物体只能用一次(只是有两个维度去判断,之前的零一背包都是重量或容量这一个维度)
1. dp[i][j]的含义:容量为i个0和j个1,包含最多字符串的数量为dp[i][j](基本上含义和题目要求的差不多)
2. 推导公式:之前的零一背包问题的公式是:
dp[j]=max(dp[j],dp[j-nums[i]+w[i]);//前后两个分别表示不选第i个物品和选择第i个物品
则这题的推导公式也是从这两个层面去思考的
dp[i][j]=(dp[i-x][j-y]+1,dp[i][j]);//前一个表示选择这个物品,第二个表示不选这个物品
x为这个物品0的数量,y表示这个物品1的数量。
3. 初始化: dp[0][0]=1;
4. 推导同上题
5. 举例验证
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {// dp[i][j]表示使用i个0和j个1能包含的最大字符串数量vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));for (string& str : strs) {// 统计当前字符串中0和1的数量int zeros = 0, ones = 0;for (char c : str) {if (c == '0') zeros++;else ones++;}// 二维01背包,逆序遍历for (int i = m; i >= zeros; i--) {for (int j = n; j >= ones; j--) {dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1);}}}return dp[m][n];}
};
完全背包理论基础
完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]
每个物品有无限个,求怎样装物品可以使价值最大。
下面还是按照正常的dp步骤来分析:
1. dp[i][j] 表示选取物品0 到 物品i,容量为j,其最大的价值为dp[i][j]
2. 确定推导公式:对于纯零一背包问题,其推导公式从:到底选不选第i个物品来得出公式
dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);//前面是不选,后面是选(根据dp[i][j]定义来弄)
这里再来解释一下推导公式:
思路是:不选的只能选0到上一个位置也即是dp[i-1],对于选的,需要先给第i个留位置则j变为j-weight[i],而i表示的是选取0到i,i不变,最终加上价值即可
和01背包的不同在于:
01:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
完全:dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
1. 01背包的核心在于:选第i个物品时,剩下的容量要从前i-1个物品中选
2. 完全背包则是:选第i个物品时,剩下的容量可以从前i个物品中选(包括第i个物品本身),则公式是dp[i][j-weight[i]],而不选的时候就是前i-1个物品
🆗下面继续进行动态规划的步骤:
3. 初始化:dp[i][0]=0 dp[0][j]=选0物品,在j容量内的最大价值=在容量范围内不断选择0物品
4. 遍历顺序:先遍历物品,再遍历容量
下面是代码:
#include <iostream>
#include <vector>
using namespace std;int main() {int n, w;//种类和总重量cin >> n >> w;vector<int> weight(n);vector<int> value(n);for (int i = 0; i < n; i++) {cin >> weight[i] >> value[i];}vector<vector<int>> dp(n, vector<int>(w+1, 0));// 初始化dp[0][j]for (int j = weight[0]; j <= w; j++){dp[0][j] = dp[0][j - weight[0]] + value[0];}for (int i = 1; i < n; i++) { //选择先遍历物品for(int j = 0; j <= w; j++) { //再遍历背包容量if (j < weight[i]) dp[i][j] = dp[i - 1][j];//else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);}}cout << dp[n - 1][w] << endl;return 0;
}
一维完全背包:
dp[i][j] 依赖于: 1. dp[i-1][j] ← 正上方(上一行) 2. dp[i][j-w] ← 同一行,左边某个位置
-
则dp[j]
保存的是上一行(i-1行)的结果
//1
for (int i = 0; i < n; i++) { // 遍历物品for (int j = weight[i]; j <= w; j++) { // 正序遍历容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}//2
vector<int> dp(w+1, 0);for(int i = 0; i < n; i++) {for(int j = 0; j <= w; j++) {if (j - weight[i] >= 0){//这个需要单独判断是否大于0dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);//压缩}}}
cout << dp[w] << endl;
//上面两个均为先物品后容量,均用于组合数
遍历顺序是正序的,这和01背包的一维不同,下面是解释:
1. 首先完全背包是每个物品可用多次,则再dp[j]计算之前的dp[j-weight[i]]可能已经包含当前物品,则dp[j]再计算时,就可以符合每个物品用多次的条件。
2. 而逆序是存储的上一轮结果,也就是上一轮选择i已经过去了,下一轮要选择第i+1个,则不能再选第i个了,不符合题目要求
518. 零钱兑换 II
518. 零钱兑换 II
首先观察题目,amount显然是充当容量的角色,整数数组表示“物品"
组合数问题-递推公式是计算组合和,遍历顺序是先物品再容量。
排列数问题-遍历顺序是先容量再物品
则按照完全背包的步骤来进行分析:
1. dp[i][j]表示取前i个硬币,金额为j,组合数为dp[j]
2. 推导公式:取第i位不取?
dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]]//若选第i个硬币,则需要判断是否<=金额3. 初始化:dp[i][0]表示金额为0,等于1, dp[0][j]=在范围内不断取第0个硬币
4. 遍历方向:i从1到n,j从1到amount,因为0已经初始化了
5. 举例判断
class Solution {
public:int change(int amount, vector<int>& coins) {int n = coins.size();// 使用long long防止溢出vector<vector<long long>> dp(n + 1, vector<long long>(amount + 1, 0));// 初始化for (int i = 0; i <= n; i++) {dp[i][0] = 1;}for (int i = 1; i <= n; i++) {for (int j = 1; j <= amount; j++) {// 不选第i种硬币dp[i][j] = dp[i-1][j];// 选第i种硬币if (j >= coins[i-1]) {dp[i][j] += dp[i][j - coins[i-1]];}}}return dp[n][amount];}
};
这里需要使用unsigned long long,因为数组元素很大。
下面是一维的分析:
1. dp[j]表示金额为j的组合数
2. dp[j]=dp[j]+dp[j-coins[i]],但要判断是否j为大于0且<=amount的
3. 初始化:d[0]=1
4. 遍历顺序是先物品再容量
class Solution {
public:int change(int amount, vector<int>& coins) {vector<int> dp(amount + 1, 0);dp[0] = 1; // 金额0有1种组合for (int i = 0; i < coins.size(); i++) {for (int j = coins[i]; j <= amount; j++) {dp[j] += dp[j - coins[i]];}}return dp[amount];}
};
377. 组合总和 Ⅳ
377. 组合总和 Ⅳ
1. dp[j]表示和为j,组合数为dp[j]
2. dp[j]+=dp[j-nums[i]];
3. dp[0]=1;
4. 遍历顺序:要明晰是排列还是组合,也就是看(1,2,2)和(2,1,2)是否算作一个答案,若是算则是组合,若是不算则是排列。这题是排列——要先遍历容量再遍历物品
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {//每个数可取无限次,使最终的总和=target//dp[j]表示总和为j的,组合数为dp[j]//dp[j]=dp[j]+dp[j-nums[i]];//dp[j]初始化dp[0]=1//遍历是先容量再物品vector<unsigned long long> dp(target+1,0);dp[0]=1;for(int j=0;j<=target;j++){for(int i=0;i<nums.size();i++){//正序if(j>=nums[i]){dp[j]+=dp[j-nums[i]];}}}return dp[target];}
};
使用unsigned long long,比使用int更合适,数组的元素实际上的范围很大。
至于为什么不选择long long原因如下:
unsigned long long
比long long
有更大的正值范围组合数问题天然适合无符号类型(不可能是负数)
70. 爬楼梯 (进阶)
这道题目 爬楼梯之前我们做过,这次再用完全背包的思路来分析一遍
70. 爬楼梯 (进阶)
#include<iostream>
#include<vector>
using namespace std;int main(){int n,m;cin>>n>>m;//n表示最终需要到达楼顶的阶梯数//m表示每次可用爬的最大阶梯数//dp[j]表示爬j阶,共有dp[j]种方法//dp[j]+=dp[j-i];//dp[0]=1;//排列,则遍历顺序为先容量(爬的阶数)再物品(每次爬多少vector<unsigned long long> dp(n+1,0);dp[0]=1;for(int j=0;j<=n;j++){for(int i=1;i<=m;i++){if(j>=i){dp[j]+=dp[j-i];}}}cout << dp[n] << endl; //用cout输出结果return 0;
}