更新动态规划专题Markdown文件

This commit is contained in:
youngyangyang04
2021-05-10 17:07:39 +08:00
parent 8c9e147998
commit 8072aac750
56 changed files with 9063 additions and 63 deletions

View File

@@ -0,0 +1,144 @@
# 动态规划Carl称它为排列总和
## 377. 组合总和 Ⅳ
题目链接https://leetcode-cn.com/problems/combination-sum-iv/
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
## 思路
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!**
弄清什么是组合,什么是排列很重要。
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
大家在公众号里学习回溯算法专题的时候,一定做过这两道题目[回溯算法39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)和[回溯算法40.组合总和II](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)会感觉这两题和本题很像!
但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。
**如果本题要把排列都列出来的话,只能使用回溯算法爆搜**
动规五部曲分析如下:
1. 确定dp数组以及下标的含义
**dp[i]: 凑成目标正整数为i的排列个数为dp[i]**
2. 确定递推公式
dp[i]考虑nums[j])可以由 dp[i - nums[j]]不考虑nums[j] 推导出来。
因为只要得到nums[j]排列个数dp[i - nums[j]]就是dp[i]的一部分。
在[动态规划494.目标和](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw) 和 [动态规划518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中我们已经讲过了求装满背包有几种方法递推公式一般都是dp[i] += dp[i - nums[j]];
本题也一样。
3. dp数组如何初始化
因为递推公式dp[i] += dp[i - nums[j]]的缘故dp[0]要初始化为1这样递归其他dp[i]的时候才会有数值基础。
至于dp[0] = 1 有没有意义呢?
其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的仅仅是为了推导递推公式。
至于非0下标的dp[i]应该初始为多少呢?
初始化为0这样才不会影响dp[i]累加所有的dp[i - nums[j]]。
4. 确定遍历顺序
个数可以不限使用,说明这是一个完全背包。
得到的集合是排列,说明需要考虑元素之间的顺序。
本题要求的是排列那么这个for循环嵌套的顺序可以有说法了。
在[动态规划518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) 中就已经讲过了。
**如果求组合数就是外层for循环遍历物品内层for遍历背包**
**如果求排列数就是外层for遍历背包内层for循环遍历物品**
如果把遍历nums物品放在外循环遍历target的作为内循环的话举一个例子计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合因为nums遍历放在外层3只能出现在1后面
所以本题遍历顺序最终遍历顺序:**target背包放在外循环将nums物品放在内循环内循环从前到后遍历**。
5. 举例来推导dp数组
我们再来用示例中的例子推导一下:
![377.组合总和Ⅳ](https://img-blog.csdnimg.cn/20210131174250148.jpg)
如果代码运行处的结果不是想要的结果就把dp[i]都打出来,看看和我们推导的一不一样。
以上分析完毕C++代码如下:
```C++
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> 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];
}
};
```
C++测试用例有超过两个树相加超过int的数据所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
但java就不用考虑这个限制java里的int也是四个字节吧也有可能leetcode后台对不同语言的测试数据不一样
## 总结
**求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!**
本题与[动态规划518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)就是一个鲜明的对比一个是求排列一个是求组合遍历顺序完全不同
如果对遍历顺序没有深度理解的话做这种完全背包的题目会很懵逼即使题目刷过了可能也不太清楚具体是怎么过的
此时大家应该对动态规划中的遍历顺序又有更深的理解了
> **相信很多小伙伴刷题的时候面对力扣上近两千道题目感觉无从下手我花费半年时间整理了Github项目「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解常用算法模板总结以及难点视频讲解按照list一道一道刷就可以了star支持一波吧**
* 公众号[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png)
* B站[代码随想录](https://space.bilibili.com/525438321)
* Github[leetcode-master](https://github.com/youngyangyang04/leetcode-master)
* 知乎[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64)
![](https://img-blog.csdnimg.cn/20210205113044152.png)