DAY48:动态规划(十一)爬楼梯(进阶版)+零钱兑换(理解DP数组“装满“含义)

70.爬楼梯(改版题目)

  • 本题是装满背包的排列数问题

  • 看是组合还是排列问题,只要看取同样两个物品,顺序不同的时候,是不是同样的方案就行了。

  • 完全背包排列数问题,注意数组下标越界所以要加上限制条件

力扣原题:(斐波那契数列)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

改版:

假设你正在爬楼梯。需要 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]时,有两种可能:

  1. 不选这个硬币,那么dp[j]保持原来的值,即“前一步的最优解”。
  2. 选这个硬币,那么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数组的含义来分析)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值