文章目录
70.爬楼梯(改版题目)
-
本题是装满背包的排列数问题
-
看是组合还是排列问题,只要看取同样两个物品,顺序不同的时候,是不是同样的方案就行了。
-
完全背包排列数问题,注意数组下标越界所以要加上限制条件
力扣原题:(斐波那契数列)
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
改版:
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 m
(0<m<n) 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路
每次可以爬m个台阶,意味着每一步都可以在1–m之间任意选择,因此可以转化为一个背包问题,每一步的m就是物品,n就是背包的容量,且物品可以重复选取(例如跳了1阶,还可以再跳1阶),因此是一个完全背包问题。
可以转化为,每一次取物品都在{1,2,……m}
之间进行取值,可以重复取同一个物品,最后需要填满容量为n的背包,共有多少种方法?
遍历顺序
本题重要的是确认到底是组合数还是排列数。因为每次取物品可以在{1,2,……m}
之间进行取值,而且取"1""2"和取“2""1"是不一样的!第一步先走1步还是先走2步,是两种不一样的方案!
看是组合还是排列,只要看取同样两个物品,顺序不同的时候,是不是同样的方案就行了。
因此,这是完全背包排列数问题,背包在外,物品在内。
完整版
- 也属于填满背包的方法问题,且为组合数,物品在外,背包在内
- 注意公式一直都是背包容量在左边!不管背包容量是i还是j!
class Solution {
public:
int climbStairs(int n,int m) {
vector<int>dp(n+1,0);
dp[0]=1;
//排列数,背包在外物品在内
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
//注意边界条件
if(i>=j){
dp[i]+=dp[i-j];//注意公式一直都是背包容量在左边!不管背包容量是i还是j!
}
}
}
return dp[n];
}
};
总结+面试情况
本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包。
面试可能的情况是,先给候选人出一个爬楼梯原题,再出本题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m](这里的意思是每次可以爬的台阶是{1,2……m}
,要注意确认意图)个台阶应该怎么写。
顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。
这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。
322.零钱兑换(DP数组含义的进一步理解)
- 本题与 474.一和零 很像,属于装满背包的最小物品个数,一和零 是装满背包最大物品个数
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
思路
首先本题也属于给定目标(背包容量),要求填满背包的问题。
题目描述中说每种硬币数量无限,因此是完全背包问题。
本题和 零钱兑换Ⅱ 的区别就是,零钱兑换Ⅱ 是要求返回凑成amount的组合数,而本题是返回凑成amount的最少硬币的数量。
求装满背包的最少物品数量的递推逻辑,和 474.一和零 类似:DAY46:动态规划(八)01背包应用2:一和零(二维容量01背包)_大磕学家ZYX的博客-CSDN博客
DP数组含义
dp[j]含义是容量为j的背包,填满背包的最少硬币数目是dp[j]
递推公式
和474.一和零 递推公式类似,求装满背包物品个数,也是一个求历史最小值的过程
dp[j]=min(dp[j],dp[j-coins[i]]+1);//这里的+1,是增加了一个物品的意思,因为求的是个数,就把value值改为了1!
遍历顺序
本题求物品最小个数,那么物品有顺序和没有顺序都可以,都不影响物品的最小个数!
所以本题并不强调集合是组合还是排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
初始化
求最小硬币个数,为了min值能够进行替代,最开始初始化需要都初始化为INT_MAX
!
最开始的写法
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//先定义为INT_MAX
vector<int>dp(amount+1,INT_MAX-1);
dp[0]=0;
//物品在外,实际上都可以
for(int i=0;i<coins.size();i++){
for(int j=1;j<=amount;j++){
if(j>=coins[i]){
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
}
return dp[amount];
}
};
debug逻辑错误:背包不一定装满
这个问题中, 因为dp数组的含义是填满容量为j的背包,最少硬币个数是dp[j],因此dp[j]如果有数值,就一定是填满了的!如果没有填满,值就是初始化的数值!
修改完整版
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//先定义为INT_MAX,DP数组含义是填满j背包,最少需要dp[j]个硬币
vector<int>dp(amount+1,INT_MAX-1);
//初始化很重要,最开始只有j=coins[i]的时候才会有继续的数值
dp[0]=0;
//物品在外,实际上都可以
for(int i=0;i<coins.size();i++){
for(int j=coins[i];j<=amount;j++){
//如果dp[j-coins[i]]是初始值,说明没有填满,凑不成这个金额j
if(dp[j-coins[i]]==INT_MAX) continue;
//能填满再更新数值
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount]==INT_MAX) return -1;
return dp[amount];
}
};
颠倒遍历顺序的版本:
- 颠倒for的顺序,再修改if里面的边界条件即可
// 版本二
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) { // 遍历背包
for (int j = 0; j < coins.size(); j++) { // 遍历物品
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
递归逻辑分析
dp[j]
表示凑出金额j
所需的最少硬币数量,初始化为 INT_MAX
,表示“不可能”。当我们选择硬币coins[i]时,有两种可能:
- 不选这个硬币,那么
dp[j]
保持原来的值,即“前一步的最优解”。 - 选这个硬币,那么
dp[j]
应该更新为dp[j-coins[i]] + 1
,即“选这个硬币后还需要凑出金额i-coins[j],再加上这一个硬币”。
注意到这里的dp[j-coins[i]] + 1
。如果dp[j-coins[i]]
是初始值INT_MAX
,表示“凑出金额j-coins[i]
是不可能的”,那么即使我们选择了硬币coins[i]
,仍然不能凑出金额j。因此在这种情况下我们应该跳过,保持dp[j]为原来的值。
另一方面,如果dp[j-coins[i]] + 1
不是初始值,即"凑出金额j-coins[i]
是可能的",也就是说这种情况下背包已经装满了。那么我们可以选择硬币coins[i],并更新dp[j]
。
背包是否”装满“的判断
在背包问题中,DP数组的含义和它的初始化是密切相关的。在 零钱兑换 问题中,我们是希望找到凑出目标金额所需的最小硬币数,所以dp数组的含义是“使用最小数量的物品装满背包”,这也就决定了我们需要将dp数组初始化为无穷大(INT_MAX)。在这种情况下,dp[j]值更新意味着我们找到了一种新的方式来填满金额j,也就是说背包是填满的。
然而,对于其它的背包问题,比如纯完全背包问题,dp[j]的含义是“背包容量为j时的最大价值”,初始化为0,表示开始时背包是空的,没有价值。对于每一个物品,我们可以选择拿或者不拿,如果拿的话,就是dp[j - weight[i]] + value[i],如果不拿,就是dp[j]。我们取两者中的最大值,这样可以保证我们总是得到最大价值。在这种情况下,背包并不一定是填满的,因为我们的目标是使背包的总价值最大,而不是一定要填满背包。
总结:装满背包最大/最小物品个数类
- 递推公式:因为求的是个数,所以,把完全背包的递推公式,物品value值改为1即可。递推公式最后+1,是增加了一个物品的意思。
- 遍历顺序:求物品个数的类型题,因为物品不管是排列方案数和组合方案数,都不影响物品的最后个数,因此物品个数类型题,遍历顺序都可以!并不需要限制是组合方案数!
- 初始化:如果求的是最小物品个数,还需要注意要全部初始化成
INT_MAX
! - 474.一和零 说的是该子集最多m个0,n个1,所以默认背包是填满的。如果要求填不满返回-1,还要考虑dp数组是不是为初始值的问题。这类问题,DP数组只要不是初始值,都是默认填满了的,如果DP数组dp[amount]是初始值,说明没有填满!(从DP数组的含义来分析)