270 lines
8.1 KiB
Markdown
270 lines
8.1 KiB
Markdown
<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。
|
||
|
||
# 算法公开课
|
||
|
||
**《代码随想录》算法视频公开课:[装满背包有几种方法?求排列数?| LeetCode:377.组合总和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数组
|
||
|
||
我们再来用示例中的例子推导一下:
|
||
|
||

|
||
|
||
如果代码运行处的结果不是想要的结果,就把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];
|
||
}
|
||
};
|
||
|
||
```
|
||
|
||
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, target):
|
||
dp = [0] * (target + 1)
|
||
dp[0] = 1
|
||
|
||
for i in range(1, target+1):
|
||
for j in nums:
|
||
if i >= j:
|
||
dp[i] += dp[i - j]
|
||
|
||
return dp[-1]
|
||
```
|
||
|
||
|
||
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 mut dp = vec![0; target as usize + 1];
|
||
dp[0] = 1;
|
||
for i in 1..=target as usize {
|
||
for &j in nums.iter() {
|
||
if i as i32 >= j {
|
||
dp[i] += dp[i- j as usize];
|
||
}
|
||
}
|
||
}
|
||
return dp[target as usize];
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
<p align="center">
|
||
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
|
||
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
|
||
</a>
|