leetcode-master/problems/0377.组合总和Ⅳ.md

294 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 377. 组合总和 Ⅳ
[力扣题目链接](https://leetcode.cn/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。
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[装满背包有几种方法?求排列数?| LeetCode377.组合总和IV](https://www.bilibili.com/video/BV1V14y1n7B6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
## 思路
对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!**
弄清什么是组合,什么是排列很重要。
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
大家在公众号里学习回溯算法专题的时候,一定做过这两道题目[回溯算法39.组合总和](https://programmercarl.com/0039.组合总和.html)和[回溯算法40.组合总和II](https://programmercarl.com/0040.组合总和II.html)会感觉这两题和本题很像!
但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。
**如果本题要把排列都列出来的话,只能使用回溯算法爆搜**
动规五部曲分析如下:
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://programmercarl.com/0494.目标和.html) 和 [动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中我们已经讲过了求装满背包有几种方法递推公式一般都是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://programmercarl.com/0518.零钱兑换II.html) 中就已经讲过了。
**如果求组合数就是外层for循环遍历物品内层for遍历背包**
**如果求排列数就是外层for遍历背包内层for循环遍历物品**
如果把遍历nums物品放在外循环遍历target的作为内循环的话举一个例子计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合因为nums遍历放在外层3只能出现在1后面
所以本题遍历顺序最终遍历顺序:**target背包放在外循环将nums物品放在内循环内循环从前到后遍历**。
5. 举例来推导dp数组
我们再来用示例中的例子推导一下:
![377.组合总和Ⅳ](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000625.png)
如果代码运行处的结果不是想要的结果就把dp[i]都打出来,看看和我们推导的一不一样。
以上分析完毕C++代码如下:
```CPP
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];
}
};
```
* 时间复杂度: O(target * n),其中 n 为 nums 的长度
* 空间复杂度: O(target)
C++测试用例有两个数相加超过int的数据所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
但java就不用考虑这个限制java里的int也是四个字节吧也有可能leetcode后台对不同语言的测试数据不一样
## 总结
**求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!**
本题与[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)就是一个鲜明的对比一个是求排列一个是求组合遍历顺序完全不同
如果对遍历顺序没有深度理解的话做这种完全背包的题目会很懵逼即使题目刷过了可能也不太清楚具体是怎么过的
此时大家应该对动态规划中的遍历顺序又有更深的理解了
## 其他语言版本
### Java
```Java
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
}
```
### Python
卡哥版
```python
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0] * (target + 1)
dp[0] = 1
for i in range(1, target + 1): # 遍历背包
for j in range(len(nums)): # 遍历物品
if i - nums[j] >= 0:
dp[i] += dp[i - nums[j]]
return dp[target]
```
优化版
```python
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0] * (target + 1) # 创建动态规划数组,用于存储组合总数
dp[0] = 1 # 初始化背包容量为0时的组合总数为1
for i in range(1, target + 1): # 遍历背包容量
for j in nums: # 遍历物品列表
if i >= j: # 当背包容量大于等于当前物品重量时
dp[i] += dp[i - j] # 更新组合总数
return dp[-1] # 返回背包容量为target时的组合总数
```
### Go
```go
func combinationSum4(nums []int, target int) int {
//定义dp数组
dp := make([]int, target+1)
// 初始化
dp[0] = 1
// 遍历顺序, 先遍历背包,再循环遍历物品
for j:=0;j<=target;j++ {
for i:=0 ;i < len(nums);i++ {
if j >= nums[i] {
dp[j] += dp[j-nums[i]]
}
}
}
return dp[target]
}
```
### Javascript
```javascript
const combinationSum4 = (nums, target) => {
let dp = Array(target + 1).fill(0);
dp[0] = 1;
for(let i = 0; i <= target; i++) {
for(let j = 0; j < nums.length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
};
```
### TypeScript
```typescript
function combinationSum4(nums: number[], target: number): number {
const dp: number[] = new Array(target + 1).fill(0);
dp[0] = 1;
// 遍历背包
for (let i = 1; i <= target; i++) {
// 遍历物品
for (let j = 0, length = nums.length; j < length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
};
```
### Rust:
```Rust
impl Solution {
pub fn combination_sum4(nums: Vec<i32>, target: i32) -> i32 {
let target = target as usize;
let mut dp = vec![0; target + 1];
dp[0] = 1;
for i in 1..=target {
for &n in &nums {
if i >= n as usize {
dp[i] += dp[i - n as usize];
}
}
}
dp[target]
}
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>