Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 604fd07

Browse files
Update
1 parent f6bb091 commit 604fd07

File tree

6 files changed

+348
-50
lines changed

6 files changed

+348
-50
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@
142142
* [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)
143143
* [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA)
144144
* [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)
145+
* [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw)
146+
* [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)
145147

146148
(持续更新中....)
147149

@@ -317,6 +319,7 @@
317319
|[0052.N皇后II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0052.N皇后II.md) |回溯|困难| **回溯**|
318320
|[0053.最大子序和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |简单|**暴力** **贪心** 动态规划 分治|
319321
|[0055.跳跃游戏](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |中等| **贪心** 经典题目|
322+
|[0057.插入区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0057.插入区间.md) |数组 |困难| **模拟** 是一道数组难题|
320323
|[0059.螺旋矩阵II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0059.螺旋矩阵II.md) |数组 |中等|**模拟**|
321324
|[0077.组合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0077.组合.md) |回溯 |中等|**回溯**|
322325
|[0078.子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0078.子集.md) |回溯/数组 |中等|**回溯**|
@@ -381,12 +384,13 @@
381384
|[0416.分割等和子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0416.分割等和子集.md) |动态规划 |中等|**背包问题/01背包**|
382385
|[0429.N叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0429.N叉树的层序遍历.md) ||简单|**队列/广度优先搜索**|
383386
|[0434.字符串中的单词数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0434.字符串中的单词数.md) |字符串 |简单|**模拟**|
387+
|[0435.无重叠区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0435.无重叠区间.md) |贪心 |中等|**贪心** 经典题目,有点难|
384388
|[0450.删除二叉搜索树中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0450.删除二叉搜索树中的节点.md) ||中等|**递归**|
385389
|[0454.四数相加II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0454.四数相加II.md) |哈希表 |中等| **哈希**|
386390
|[0455.分发饼干](https://github.com/youngyangyang04/leetcode/blob/master/problems/0455.分发饼干.md) |贪心 |简单| **贪心**|
387391
|[0459.重复的子字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0459.重复的子字符串.md) |字符创 |简单| **KMP**|
388392
|[0486.预测赢家](https://github.com/youngyangyang04/leetcode/blob/master/problems/0486.预测赢家.md) |动态规划 |中等| **递归** **记忆递归** **动态规划**|
389-
|[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法**|
393+
|[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法** 这个去重有意思|
390394
|[0496.下一个更大元素I](https://github.com/youngyangyang04/leetcode/blob/master/problems/0496.下一个更大元素I.md) ||中等|**单调栈** 入门题目,但是两个数组还是有点绕的|
391395
|[0501.二叉搜索树中的众数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0501.二叉搜索树中的众数.md) |二叉树 |简单|**递归/中序遍历**|
392396
|[0513.找树左下角的值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0513.找树左下角的值.md) |二叉树 |中等|**递归** **迭代**|
@@ -415,6 +419,7 @@
415419
|[0925.长按键入](https://github.com/youngyangyang04/leetcode/blob/master/problems/0925.长按键入.md) |字符串 |简单|**双指针/模拟** 是一道模拟类型的题目|
416420
|[0941.有效的山脉数组](https://github.com/youngyangyang04/leetcode/blob/master/problems/0941.有效的山脉数组.md) |数组 |简单|**双指针**|
417421
|[0968.监控二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0968.监控二叉树.md) |二叉树 |困难|**贪心** 贪心与二叉树的结合|
422+
|[0973.最接近原点的K个点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0973.最接近原点的K个点.md) |优先级队列 |中等|**优先级队列**|
418423
|[0977.有序数组的平方](https://github.com/youngyangyang04/leetcode/blob/master/problems/0977.有序数组的平方.md) |数组 |中等|**双指针** 还是比较巧妙的|
419424
|[1002.查找常用字符](https://github.com/youngyangyang04/leetcode/blob/master/problems/1002.查找常用字符.md) ||简单|****|
420425
|[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |哈希表 |简单|**哈希表/数组**|

pics/491. 递增子序列1.png

281 KB
Loading

problems/0435.无重叠区间.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
## 思路
3+
4+
这道题目如果真的去模拟去重复区间的行为,是非常麻烦的,还要有删除区间。
5+
6+
**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?**
7+
8+
按照右边界排序,那么右边界越小越好,因为右边界越小,留给下一个区间的空间就越大,所以可以从左向右遍历,优先选右边界小的。
9+
10+
按照左边界排序,那么就是左边界越大越好,这样就给前一个区间的空间就越大,所以可以从右向左遍历。
11+
12+
如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就比较复杂了,这其实也就不是贪心了。
13+
14+
15+
16+
在每次选择中,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。
17+
18+
19+
```
20+
class Solution {
21+
public:
22+
// 按照区间右边界排序
23+
static bool cmp (const vector<int>& a, const vector<int>& b) {
24+
return a[1] < b[1];
25+
}
26+
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
27+
if (intervals.size() == 0) return 0;
28+
sort(intervals.begin(), intervals.end(), cmp);
29+
int count = 1; // 记录非交叉区间的个数
30+
int end = intervals[0][1];
31+
for (int i = 1; i < intervals.size(); i++) {
32+
if (end <= intervals[i][0]) {
33+
end = intervals[i][1];
34+
count++;
35+
}
36+
}
37+
return intervals.size() - count;
38+
}
39+
};
40+
```

problems/0491.递增子序列.md

Lines changed: 172 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,211 @@
11
## 题目地址
22

3-
## 思路
3+
> 和子集问题有点像,但又处处是陷阱
44
5-
这道题可以说是深度优先搜索,也可以说是回溯法,其实我更倾向于说它用回溯法,因为本题和[90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)非常像,差别就是[90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)可以通过排序,在加一个标记数组来达到去重的目的。
5+
# 491.递增子序列
66

7-
去重复的逻辑,关键在于子序列的末尾,如果子序列的末尾重复出现一个元素,那么该序列就是重复的了,如图所示:
7+
题目链接:https://leetcode-cn.com/problems/increasing-subsequences/
8+
9+
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
10+
11+
示例:
12+
13+
输入: [4, 6, 7, 7]
14+
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
15+
16+
说明:
17+
* 给定数组的长度不会超过15。
18+
* 数组中的整数范围是 [-100,100]
19+
* 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
20+
21+
22+
# 思路
23+
24+
这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。
25+
26+
这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)
27+
28+
就是因为太像了,更要注意差别所在,要不就掉坑里了!
29+
30+
[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中我们是通过排序,再加一个标记数组来达到去重的目的。
31+
32+
而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。
33+
34+
**所以不能使用之前的去重逻辑!**
35+
36+
本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。
37+
38+
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:
39+
40+
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div>
41+
42+
43+
## 回溯三部曲
44+
45+
* 递归函数参数
46+
47+
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
48+
49+
代码如下:
50+
51+
```
52+
vector<vector<int>> result;
53+
vector<int> path;
54+
void backtracking(vector<int>& nums, int startIndex)
55+
```
56+
57+
* 终止条件
58+
59+
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
60+
61+
但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下:
62+
63+
```
64+
if (path.size() > 1) {
65+
result.push_back(path);
66+
// 注意这里不要加return,因为要取树上的所有节点
67+
}
68+
```
69+
70+
* 单层搜索逻辑
871

972
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div>
1073

11-
在递归的过程中 `if ((subseq.empty() || nums[i] >= subseq.back()) && uset.find(nums[i]) == uset.end())` 这个判断条件一定要想清楚, 如果子序列为空或者nums[i]>=子序列尾部数值,**同时** 这个nums[i] 不能出现过, 因为一旦出现过就 是一个重复的递增子序列了。
74+
在图中可以看出,同层上使用过的元素就不能在使用了,**注意这里和[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中去重的区别**
75+
76+
**本题只要同层重复使用元素,递增子序列就会重复**,而[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中是排序之后看相邻元素是否重复使用。
77+
78+
79+
还有一种情况就是如果选取的元素小于子序列最后一个元素,那么就不能是递增的,所以也要pass掉。
80+
81+
那么去重的逻辑代码如下:
82+
83+
```
84+
if ((!path.empty() && nums[i] < path.back())
85+
|| uset.find(nums[i]) != uset.end()) {
86+
continue;
87+
}
88+
```
89+
判断`nums[i] < path.back()`之前一定要判断path是否为空,所以是`!path.empty() && nums[i] < path.back()`
90+
91+
`uset.find(nums[i]) != uset.end()`判断nums[i]在本层是否使用过。
92+
93+
那么单层搜索代码如下:
94+
95+
```
96+
unordered_set<int> uset; // 使用set来对本层元素进行去重
97+
for (int i = startIndex; i < nums.size(); i++) {
98+
if ((!path.empty() && nums[i] < path.back())
99+
|| uset.find(nums[i]) != uset.end()) {
100+
continue;
101+
}
102+
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
103+
path.push_back(nums[i]);
104+
backtracking(nums, i + 1);
105+
path.pop_back();
106+
}
107+
```
108+
109+
**对于已经习惯写回溯的同学,看到递归函数上面的`uset.insert(nums[i]);`,下面却没有对应的pop之类的操作,应该很不习惯吧,哈哈**
110+
111+
**这也是需要注意的点,`unordered_set<int> uset;` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!**
112+
113+
114+
最后整体C++代码如下:
12115

13116
## C++代码
14117

15118
```
119+
// 版本一
16120
class Solution {
17121
private:
18-
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& subseq, int startIndex) {
19-
if (subseq.size() > 1) {
20-
result.push_back(subseq);
21-
// 注意这里不要加return,因为要取所有的可能
22-
}
23-
unordered_set<int> uset; // 使用set来对尾部元素进行去重
24-
for (int i = startIndex; i < nums.size(); i++) {
25-
if ((subseq.empty() || nums[i] >= subseq.back())
26-
&& uset.find(nums[i]) == uset.end()) {
27-
subseq.push_back(nums[i]);
28-
backtracking(nums, result, subseq, i + 1);
29-
subseq.pop_back();
30-
uset.insert(nums[i]);//在回溯的时候,记录这个元素用过了,后面不能再用了
122+
vector<vector<int>> result;
123+
vector<int> path;
124+
void backtracking(vector<int>& nums, int startIndex) {
125+
if (path.size() > 1) {
126+
result.push_back(path);
127+
// 注意这里不要加return,要取树上的节点
128+
}
129+
unordered_set<int> uset; // 使用set对本层元素进行去重
130+
for (int i = startIndex; i < nums.size(); i++) {
131+
if ((!path.empty() && nums[i] < path.back())
132+
|| uset.find(nums[i]) != uset.end()) {
133+
continue;
134+
}
135+
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
136+
path.push_back(nums[i]);
137+
backtracking(nums, i + 1);
138+
path.pop_back();
31139
}
32140
}
33-
}
34141
public:
35142
vector<vector<int>> findSubsequences(vector<int>& nums) {
36-
vector<vector<int>> result;
37-
vector<int> subseq;
38-
backtracking(nums, result, subseq, 0);
143+
result.clear();
144+
path.clear();
145+
backtracking(nums, 0);
39146
return result;
40147
}
41148
};
42149
```
43150

44-
一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图:
151+
## 优化
45152

46-
<img src='../pics/491. 递增子序列2.png' width=600> </img></div>
153+
以上代码用我用了`unordered_set<int>`来记录本层元素是否重复使用。
47154

48-
改动的地方主要是将去重的逻辑中把 unordered_set 改成了 数组。
155+
**其实用数组来做哈希,效率就高了很多**
49156

50-
用数组替换unordered_set 确实可以快很多,unordered_set底层符号表也是哈希表,理论上不应该差多少
157+
注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希
51158

52-
估计程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)费了些时间
159+
程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的
53160

54-
用数组来做哈希,效率就高了很多,再加上题目中也说了,数值范围[-100,100],所以用数组正合适。
55-
56-
**这个事实告诉我们,使用哈希法的时候,条件允许的话,能用数组尽量用数组。**
57-
58-
优化后的代码如下:
161+
那么优化后的代码如下:
59162

60163
```
164+
// 版本二
61165
class Solution {
62166
private:
63-
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& subseq, int startIndex) {
64-
if (subseq.size() > 1) {
65-
result.push_back(subseq);
66-
// 注意这里不要加return,因为要取所有的可能
67-
}
68-
int hash[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
69-
for (int i = startIndex; i < nums.size(); i++) {
70-
if ((subseq.empty() || nums[i] >= subseq.back())
71-
&& hash[nums[i] + 100] == 0) {
72-
subseq.push_back(nums[i]);
73-
backtracking(nums, result, subseq, i + 1);
74-
subseq.pop_back();
75-
hash[nums[i]+100] = 1;
167+
vector<vector<int>> result;
168+
vector<int> path;
169+
void backtracking(vector<int>& nums, int startIndex) {
170+
if (path.size() > 1) {
171+
result.push_back(path);
172+
}
173+
int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
174+
for (int i = startIndex; i < nums.size(); i++) {
175+
if ((!path.empty() && nums[i] < path.back())
176+
|| used[nums[i] + 100] == 1) {
177+
continue;
178+
}
179+
used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了
180+
path.push_back(nums[i]);
181+
backtracking(nums, i + 1);
182+
path.pop_back();
76183
}
77184
}
78-
}
79185
public:
80186
vector<vector<int>> findSubsequences(vector<int>& nums) {
81-
vector<vector<int>> result;
82-
vector<int> subseq;
83-
backtracking(nums, result, subseq, 0);
187+
result.clear();
188+
path.clear();
189+
backtracking(nums, 0);
84190
return result;
85191
}
86192
};
87-
88193
```
194+
195+
这份代码在leetcode上提交,要比版本一耗时要好的多。
196+
197+
**所以正如在[哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg)中说的那样,数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如何数值范围小的话能用数组尽量用数组**
198+
199+
200+
201+
# 总结
202+
203+
本题题解清一色都说是深度优先搜索,但我更倾向于说它用回溯法,而且本题我也是完全使用回溯法的逻辑来分析的。
204+
205+
相信大家在本题中处处都能看到是[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)的身影,但处处又都是陷阱。
206+
207+
**对于养成思维定式或者套模板套嗨了的同学,这道题起到了很好的警醒作用。更重要的是拓展了大家的思路!**
208+
209+
**就酱,如果感觉「代码随想录」很干货,就帮Carl宣传一波吧!**
210+
211+

0 commit comments

Comments
 (0)