Skip to content

Commit 64a885d

Browse files
Update
1 parent a15a13e commit 64a885d

9 files changed

+225
-47
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@
135135
* [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA)
136136
* [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)
137137
* [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A)
138+
* [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw)
139+
* [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)
138140

139141
(持续更新中....)
140142

@@ -303,6 +305,7 @@
303305
|[0039.组合总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md) |数组/回溯 |中等| **回溯**|
304306
|[0040.组合总和II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0040.组合总和II.md) |数组/回溯 |中等| **回溯**|
305307
|[0042.接雨水](https://github.com/youngyangyang04/leetcode/blob/master/problems/0042.接雨水.md) |数组/栈/双指针 |困难| **双指针** **单调栈** **动态规划**|
308+
|[0045.跳跃游戏II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0045.跳跃游戏II.md) |贪心 |困难| **贪心**|
306309
|[0046.全排列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0046.全排列.md) |回溯|中等| **回溯**|
307310
|[0047.全排列II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0047.全排列II.md) |回溯|中等| **回溯**|
308311
|[0051.N皇后](https://github.com/youngyangyang04/leetcode/blob/master/problems/0051.N皇后.md) |回溯|困难| **回溯**|

pics/39.组合总和1.png

-3.57 KB
Loading

pics/40.组合总和II.png

77.7 KB
Loading

pics/40.组合总和II1.png

289 KB
Loading

pics/45.跳跃游戏II.png

40.1 KB
Loading

problems/0039.组合总和.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,6 @@ private:
177177
vector<vector<int>> result;
178178
vector<int> path;
179179
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
180-
if (sum > target) {
181-
return;
182-
}
183180
if (sum == target) {
184181
result.push_back(path);
185182
return;

problems/0040.组合总和II.md

Lines changed: 144 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,173 @@
1-
# 第40题. 组合总和
1+
> 这篇可以说是全网把组合问题如何去重,讲的最清晰的了!
2+
3+
4+
# 40.组合总和II
5+
6+
题目链接:https://leetcode-cn.com/problems/combination-sum-ii/
7+
28
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
39

410
candidates 中的每个数字在每个组合中只能使用一次。
511

6-
说明:
12+
说明:
13+
所有数字(包括目标数)都是正整数。
14+
解集不能包含重复的组合。 
715

8-
所有数字(包括目标数)都是正整数。
9-
解集不能包含重复的组合。 
16+
示例 1:
17+
输入: candidates = [10,1,2,7,6,1,5], target = 8,
18+
所求解集为:
19+
[
20+
[1, 7],
21+
[1, 2, 5],
22+
[2, 6],
23+
[1, 1, 6]
24+
]
25+
26+
示例 2:
27+
输入: candidates = [2,5,2,1,2], target = 5,
28+
所求解集为:
29+
[
30+
  [1,2,2],
31+
  [5]
32+
]
1033

11-
示例 1:
34+
# 思路
1235

13-
输入: candidates = [10,1,2,7,6,1,5], target = 8,
14-
所求解集为:
15-
[
16-
[1, 7],
17-
[1, 2, 5],
18-
[2, 6],
19-
[1, 1, 6]
20-
]
36+
这道题目和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)如下区别:
2137

22-
示例 2:
38+
1. 本题candidates 中的每个数字在每个组合中只能使用一次。
39+
2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是无重复元素的数组candidates
2340

24-
输入: candidates = [2,5,2,1,2], target = 5,
25-
所求解集为:
26-
[
27-
  [1,2,2],
28-
  [5]
29-
]
41+
最后本题和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求一样,解集不能包含重复的组合。
3042

31-
# 思想
43+
**本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合**
3244

33-
这道题目和[0039.组合总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md) 区别就是要去重。
45+
一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时!
3446

35-
很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
47+
所以要在搜索的过程中就去掉重复组合。
3648

37-
这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
49+
很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
3850

51+
这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
3952

4053
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。**
4154

42-
所以要明确我们要去重的是同一树层上的“使用过”。
55+
那么问题来了,我们是要同一树层上使用过,还是统一树枝上使用过呢?
56+
57+
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
58+
59+
**所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重**
4360

4461
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)
4562

46-
选择过程如图所示
63+
选择过程树形结构如图所示
4764

4865
<img src='../pics/40.组合总和II.png' width=600> </img></div>
4966

67+
可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。
68+
69+
## 回溯三部曲
70+
71+
* **递归函数参数**
72+
73+
[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。
74+
75+
这个集合去重的重任就是used来完成的。
76+
77+
代码如下:
5078

51-
理解了“同一树枝使用过”和“同一树层使用过” 之后,我们在拉看如下代码实现,关键地方已经注释,大家应该就理解了
79+
```
80+
vector<vector<int>> result; // 存放组合集合
81+
vector<int> path; // 符合条件的组合
82+
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
83+
```
84+
85+
* **递归终止条件**
86+
87+
[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)相同,终止条件为 `sum > target``sum == target`
5288

53-
# C++代码
89+
代码如下:
90+
91+
```
92+
if (sum > target) { // 这个条件其实可以省略
93+
return;
94+
}
95+
if (sum == target) {
96+
result.push_back(path);
97+
return;
98+
}
99+
```
100+
101+
`sum > target` 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。
102+
103+
* **单层搜索的逻辑**
104+
105+
这里与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)最大的不同就是要去重了。
106+
107+
前面我们提到:要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。
108+
109+
**如果`candidates[i] == candidates[i - 1]` 并且 `used[i - 1] == false`,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]**
110+
111+
此时for循环里就应该做continue的操作。
112+
113+
这块比较抽象,如图:
114+
115+
<img src='../pics/40.组合总和II1.png' width=600> </img></div>
116+
117+
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
118+
119+
* used[i - 1] == true,说明同一树支candidates[i - 1]使用过
120+
* used[i - 1] == false,说明同一树层candidates[i - 1]使用过
121+
122+
**这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!**
123+
124+
那么单层搜索的逻辑代码如下:
125+
126+
```
127+
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
128+
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
129+
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
130+
// 要对同一树层使用过的元素进行跳过
131+
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
132+
continue;
133+
}
134+
sum += candidates[i];
135+
path.push_back(candidates[i]);
136+
used[i] = true;
137+
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
138+
used[i] = false;
139+
sum -= candidates[i];
140+
path.pop_back();
141+
}
142+
```
143+
144+
**注意sum + candidates[i] <= target为剪枝操作,在[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)有讲解过!**
145+
146+
## C++代码
147+
148+
回溯三部曲分析完了,整体C++代码如下:
54149

55150
```
56151
class Solution {
57152
private:
58153
vector<vector<int>> result;
59154
vector<int> path;
60155
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
61-
if (sum > target) {
62-
return;
63-
}
64156
if (sum == target) {
65157
result.push_back(path);
66158
return;
67159
}
68-
69-
// 每个组合中只能使用一次 所以用 startindex
70-
// 给定一个数组 candidates 默认有重复项,解集不能包含重复的组合。 所以使用if这一套
71-
for (int i = startIndex; i < candidates.size(); i++) {
72-
// 这里理解used[i - 1]非常重要
73-
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
160+
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
161+
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
74162
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
75-
// 而我们要对同一树层使用过的元素进行跳过
76-
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
163+
// 要对同一树层使用过的元素进行跳过
164+
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
77165
continue;
78166
}
79167
sum += candidates[i];
80168
path.push_back(candidates[i]);
81169
used[i] = true;
82-
backtracking(candidates, target, sum, i + 1, used); // 关键点在这里,不用i+1了
170+
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
83171
used[i] = false;
84172
sum -= candidates[i];
85173
path.pop_back();
@@ -89,11 +177,25 @@ private:
89177
public:
90178
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
91179
vector<bool> used(candidates.size(), false);
92-
// 首先把给candidates排序,让其相同的元素都挨在一起。
180+
path.clear();
181+
result.clear();
182+
// 首先把给candidates排序,让其相同的元素都挨在一起。
93183
sort(candidates.begin(), candidates.end());
94184
backtracking(candidates, target, 0, 0, used);
95185
return result;
96-
97186
}
98187
};
188+
99189
```
190+
191+
# 总结
192+
193+
本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)难度提升了不少。
194+
195+
**关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可**
196+
197+
所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,**就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!**
198+
199+
**就酱,如果感觉「代码随想录」诚意满满,就帮Carl宣传一波吧,感谢啦!**
200+
201+

problems/0045.跳跃游戏II.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
## 题目链接
2+
https://leetcode-cn.com/problems/jump-game-ii/
3+
4+
## 思路
5+
6+
本题相对于[0055.跳跃游戏](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md)还是难了不少。
7+
8+
本题要计算最大步数,那么就要想清楚什么时候步数加一?
9+
10+
**这里需要统计两个距离,当前可移动距离和下一步最远距离**
11+
12+
如果移动范围超过当前可移动距离,那么就必须再走一步来达到增加可移动距离的目的。
13+
14+
如图:
15+
16+
<img src='../pics/45.跳跃游戏II.png' width=600> </img></div>
17+
18+
### 方法一
19+
20+
这里还是有个特殊情况需要考虑,如果当前可移动距离的终点就是是集合终点,那么就不用增加步数了,因为不能再往后走了。
21+
22+
详情可看代码(详细注释)
23+
24+
```
25+
// 版本一
26+
class Solution {
27+
public:
28+
int jump(vector<int>& nums) {
29+
if (nums.size() == 1) return 0;
30+
int curDistance = 0; // 当前可移动距离
31+
int ans = 0; // 记录走的最大步数
32+
int nextDistance = 0; // 下一步最远距离
33+
for (int i = 0; i < nums.size(); i++) {
34+
nextDistance = max(nums[i] + i, nextDistance);
35+
if (i == curDistance) { // 遇到当前可移动距离的终点
36+
if (curDistance != nums.size() - 1) { // 如果当前可移动距离的终点不是集合终点
37+
ans++; // 需要走下一步
38+
curDistance = nextDistance; // 更新下一步最远距离的范围
39+
if (nextDistance >= nums.size() - 1) break; // 下一步最远距离已经可以达到终点,结束循环
40+
} else break; // 当前可移动距离的终点是集合终点
41+
}
42+
}
43+
return ans;
44+
}
45+
};
46+
```
47+
48+
### 方法二
49+
50+
依然是贪心,思路和方法一差不多,代码可以简洁一些。
51+
52+
在方法一种,处理 当前可移动距离的终点 是不是集合终点 来判断ans是否要做相应的加一操作。
53+
54+
其实可以用 for循环遍历的时候i < nums.size() - 1,这样就是默认最后一步,一定是可以到终点的。
55+
56+
代码如下:
57+
58+
```
59+
60+
class Solution {
61+
public:
62+
int jump(vector<int>& nums) {
63+
int curDistance = 0;
64+
int ans = 0; // 记录走的最大步数,初始为0
65+
int nextDistance = 0; // 每走一步获得的跳跃范围
66+
for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1
67+
nextDistance = max(nums[i] + i, nextDistance);
68+
if (i == curDistance) { // 遇到本次跳跃范围的终点
69+
curDistance = nextDistance;
70+
ans++;
71+
}
72+
}
73+
return ans;
74+
}
75+
};
76+
```

problems/0349.两个数组的交集.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ https://leetcode-cn.com/problems/intersection-of-two-arrays/
2222

2323
这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。
2424

25-
可以发现,貌似用数组做哈希表可以解决这道题目,把nums1的元素,映射到哈希数组的下表上,然后在遍历nums2的时候,判断是否出现过就可以了。
25+
那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)[0383.赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ)
2626

27-
但是要注意,**使用数据来做哈希的题目,都限制了数值的大小,例如[哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)题目中只有小写字母,或者数值大小在[0- 10000] 之内等等**
27+
但是要注意,**使用数据来做哈希的题目,都限制了数值的大小。**
2828

2929
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
3030

0 commit comments

Comments
 (0)