# 本周小结!(动态规划系列五) ## 周一 [动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)中给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数(顺序不同的序列被视作不同的组合)。 题目面试虽然是组合,但又强调顺序不同的序列被视作不同的组合,其实这道题目求的是排列数! 递归公式:dp[i] += dp[i - nums[j]]; 这个和前上周讲的组合问题又不一样,关键就体现在遍历顺序上! 在[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) 中就已经讲过了。 **如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 **如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面! 所以本题遍历顺序最终遍历顺序:**target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历**。 ```CPP class Solution { public: int combinationSum4(vector& nums, int target) { vector dp(target + 1, 0); dp[0] = 1; for (int i = 0; i <= target; i++) { // 遍历背包 for (int j = 0; j < nums.size(); j++) { // 遍历物品 if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) { dp[i] += dp[i - nums[j]]; } } } return dp[target]; } }; ``` ## 周二 爬楼梯之前我们已经做过了,就是斐波那契数列,很好解,但[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)中我们进阶了一下。 改为:每次可以爬 1 、 2、.....、m 个台阶。问有多少种不同的方法可以爬到楼顶呢? 1阶,2阶,.... m阶就是物品,楼顶就是背包。 每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。 问跳到楼顶有几种方法其实就是问装满背包有几种方法。 **此时大家应该发现这就是一个完全背包问题了!** 和昨天的题目[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)基本就是一道题了,遍历顺序也是一样一样的! 代码如下: ```CPP class Solution { public: int climbStairs(int n) { vector dp(n + 1, 0); dp[0] = 1; for (int i = 1; i <= n; i++) { // 遍历背包 for (int j = 1; j <= m; j++) { // 遍历物品 if (i - j >= 0) dp[i] += dp[i - j]; } } return dp[n]; } }; ``` 代码中m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯可以AC的代码了。 ## 周三 [动态规划:322.零钱兑换](https://programmercarl.com/0322.零钱兑换.html)给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数(每种硬币的数量是无限的)。 这里我们都知道这是完全背包。 递归公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); 关键看遍历顺序。 本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数**。 所以本题并不强调集合是组合还是排列。 **那么本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!** 外层for循环遍历物品,内层for遍历背包: ```CPP // 版本一 class Solution { public: int coinChange(vector& coins, int amount) { vector dp(amount + 1, INT_MAX); dp[0] = 0; for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int j = coins[i]; j <= amount; j++) { // 遍历背包 if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过 dp[j] = min(dp[j - coins[i]] + 1, dp[j]); } } } if (dp[amount] == INT_MAX) return -1; return dp[amount]; } }; ``` 外层for遍历背包,内层for循环遍历物品: ```CPP // 版本二 class Solution { public: int coinChange(vector& coins, int amount) { vector 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]; } }; ``` ## 周四 [动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html)给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少(平方数可以重复使用)。 如果按顺序把前面的文章都看了,这道题目就是简单题了。 dp[i]的定义,递推公式,初始化,遍历顺序,都是和[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html) 一样一样的。 要是没有前面的基础上来做这道题,那这道题目就有点难度了。 **这也体现了刷题顺序的重要性**。 先遍历背包,在遍历物品: ```CPP // 版本一 class Solution { public: int numSquares(int n) { vector dp(n + 1, INT_MAX); dp[0] = 0; for (int i = 0; i <= n; i++) { // 遍历背包 for (int j = 1; j * j <= i; j++) { // 遍历物品 dp[i] = min(dp[i - j * j] + 1, dp[i]); } } return dp[n]; } }; ``` 先遍历物品,在遍历背包: ```CPP // 版本二 class Solution { public: int numSquares(int n) { vector dp(n + 1, INT_MAX); dp[0] = 0; for (int i = 1; i * i <= n; i++) { // 遍历物品 for (int j = 1; j <= n; j++) { // 遍历背包 if (j - i * i >= 0) { dp[j] = min(dp[j - i * i] + 1, dp[j]); } } } return dp[n]; } }; ``` ## 总结 本周的主题其实就是背包问题中的遍历顺序! 我这里做一下总结: 求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) 求排列数:[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) 求最小数:[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)、[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html) 此时我们就已经把完全背包的遍历顺序研究的透透的了!