forked from youngyangyang04/leetcode-master
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f6bb091
commit 604fd07
Showing
6 changed files
with
348 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
|
||
## 思路 | ||
|
||
这道题目如果真的去模拟去重复区间的行为,是非常麻烦的,还要有删除区间。 | ||
|
||
**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** | ||
|
||
按照右边界排序,那么右边界越小越好,因为右边界越小,留给下一个区间的空间就越大,所以可以从左向右遍历,优先选右边界小的。 | ||
|
||
按照左边界排序,那么就是左边界越大越好,这样就给前一个区间的空间就越大,所以可以从右向左遍历。 | ||
|
||
如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就比较复杂了,这其实也就不是贪心了。 | ||
|
||
|
||
|
||
在每次选择中,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 | ||
|
||
|
||
``` | ||
class Solution { | ||
public: | ||
// 按照区间右边界排序 | ||
static bool cmp (const vector<int>& a, const vector<int>& b) { | ||
return a[1] < b[1]; | ||
} | ||
int eraseOverlapIntervals(vector<vector<int>>& intervals) { | ||
if (intervals.size() == 0) return 0; | ||
sort(intervals.begin(), intervals.end(), cmp); | ||
int count = 1; // 记录非交叉区间的个数 | ||
int end = intervals[0][1]; | ||
for (int i = 1; i < intervals.size(); i++) { | ||
if (end <= intervals[i][0]) { | ||
end = intervals[i][1]; | ||
count++; | ||
} | ||
} | ||
return intervals.size() - count; | ||
} | ||
}; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,211 @@ | ||
## 题目地址 | ||
|
||
## 思路 | ||
> 和子集问题有点像,但又处处是陷阱 | ||
这道题可以说是深度优先搜索,也可以说是回溯法,其实我更倾向于说它用回溯法,因为本题和[90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)非常像,差别就是[90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)可以通过排序,在加一个标记数组来达到去重的目的。 | ||
# 491.递增子序列 | ||
|
||
去重复的逻辑,关键在于子序列的末尾,如果子序列的末尾重复出现一个元素,那么该序列就是重复的了,如图所示: | ||
题目链接:https://leetcode-cn.com/problems/increasing-subsequences/ | ||
|
||
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。 | ||
|
||
示例: | ||
|
||
输入: [4, 6, 7, 7] | ||
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]] | ||
|
||
说明: | ||
* 给定数组的长度不会超过15。 | ||
* 数组中的整数范围是 [-100,100]。 | ||
* 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。 | ||
|
||
|
||
# 思路 | ||
|
||
这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。 | ||
|
||
这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)。 | ||
|
||
就是因为太像了,更要注意差别所在,要不就掉坑里了! | ||
|
||
在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中我们是通过排序,再加一个标记数组来达到去重的目的。 | ||
|
||
而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。 | ||
|
||
**所以不能使用之前的去重逻辑!** | ||
|
||
本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。 | ||
|
||
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图: | ||
|
||
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div> | ||
|
||
|
||
## 回溯三部曲 | ||
|
||
* 递归函数参数 | ||
|
||
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。 | ||
|
||
代码如下: | ||
|
||
``` | ||
vector<vector<int>> result; | ||
vector<int> path; | ||
void backtracking(vector<int>& nums, int startIndex) | ||
``` | ||
|
||
* 终止条件 | ||
|
||
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。 | ||
|
||
但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下: | ||
|
||
``` | ||
if (path.size() > 1) { | ||
result.push_back(path); | ||
// 注意这里不要加return,因为要取树上的所有节点 | ||
} | ||
``` | ||
|
||
* 单层搜索逻辑 | ||
|
||
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div> | ||
|
||
在递归的过程中 `if ((subseq.empty() || nums[i] >= subseq.back()) && uset.find(nums[i]) == uset.end())` 这个判断条件一定要想清楚, 如果子序列为空或者nums[i]>=子序列尾部数值,**同时** 这个nums[i] 不能出现过, 因为一旦出现过就 是一个重复的递增子序列了。 | ||
在图中可以看出,同层上使用过的元素就不能在使用了,**注意这里和[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中去重的区别**。 | ||
|
||
**本题只要同层重复使用元素,递增子序列就会重复**,而[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中是排序之后看相邻元素是否重复使用。 | ||
|
||
|
||
还有一种情况就是如果选取的元素小于子序列最后一个元素,那么就不能是递增的,所以也要pass掉。 | ||
|
||
那么去重的逻辑代码如下: | ||
|
||
``` | ||
if ((!path.empty() && nums[i] < path.back()) | ||
|| uset.find(nums[i]) != uset.end()) { | ||
continue; | ||
} | ||
``` | ||
判断`nums[i] < path.back()`之前一定要判断path是否为空,所以是`!path.empty() && nums[i] < path.back()`。 | ||
|
||
`uset.find(nums[i]) != uset.end()`判断nums[i]在本层是否使用过。 | ||
|
||
那么单层搜索代码如下: | ||
|
||
``` | ||
unordered_set<int> uset; // 使用set来对本层元素进行去重 | ||
for (int i = startIndex; i < nums.size(); i++) { | ||
if ((!path.empty() && nums[i] < path.back()) | ||
|| uset.find(nums[i]) != uset.end()) { | ||
continue; | ||
} | ||
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 | ||
path.push_back(nums[i]); | ||
backtracking(nums, i + 1); | ||
path.pop_back(); | ||
} | ||
``` | ||
|
||
**对于已经习惯写回溯的同学,看到递归函数上面的`uset.insert(nums[i]);`,下面却没有对应的pop之类的操作,应该很不习惯吧,哈哈** | ||
|
||
**这也是需要注意的点,`unordered_set<int> uset;` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!** | ||
|
||
|
||
最后整体C++代码如下: | ||
|
||
## C++代码 | ||
|
||
``` | ||
// 版本一 | ||
class Solution { | ||
private: | ||
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& subseq, int startIndex) { | ||
if (subseq.size() > 1) { | ||
result.push_back(subseq); | ||
// 注意这里不要加return,因为要取所有的可能 | ||
} | ||
unordered_set<int> uset; // 使用set来对尾部元素进行去重 | ||
for (int i = startIndex; i < nums.size(); i++) { | ||
if ((subseq.empty() || nums[i] >= subseq.back()) | ||
&& uset.find(nums[i]) == uset.end()) { | ||
subseq.push_back(nums[i]); | ||
backtracking(nums, result, subseq, i + 1); | ||
subseq.pop_back(); | ||
uset.insert(nums[i]);//在回溯的时候,记录这个元素用过了,后面不能再用了 | ||
vector<vector<int>> result; | ||
vector<int> path; | ||
void backtracking(vector<int>& nums, int startIndex) { | ||
if (path.size() > 1) { | ||
result.push_back(path); | ||
// 注意这里不要加return,要取树上的节点 | ||
} | ||
unordered_set<int> uset; // 使用set对本层元素进行去重 | ||
for (int i = startIndex; i < nums.size(); i++) { | ||
if ((!path.empty() && nums[i] < path.back()) | ||
|| uset.find(nums[i]) != uset.end()) { | ||
continue; | ||
} | ||
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 | ||
path.push_back(nums[i]); | ||
backtracking(nums, i + 1); | ||
path.pop_back(); | ||
} | ||
} | ||
} | ||
public: | ||
vector<vector<int>> findSubsequences(vector<int>& nums) { | ||
vector<vector<int>> result; | ||
vector<int> subseq; | ||
backtracking(nums, result, subseq, 0); | ||
result.clear(); | ||
path.clear(); | ||
backtracking(nums, 0); | ||
return result; | ||
} | ||
}; | ||
``` | ||
|
||
一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图: | ||
## 优化 | ||
|
||
<img src='../pics/491. 递增子序列2.png' width=600> </img></div> | ||
以上代码用我用了`unordered_set<int>`来记录本层元素是否重复使用。 | ||
|
||
改动的地方主要是将去重的逻辑中把 unordered_set 改成了 数组。 | ||
**其实用数组来做哈希,效率就高了很多**。 | ||
|
||
用数组替换unordered_set 确实可以快很多,unordered_set底层符号表也是哈希表,理论上不应该差多少。 | ||
注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。 | ||
|
||
估计程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)费了些时间。 | ||
程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。 | ||
|
||
用数组来做哈希,效率就高了很多,再加上题目中也说了,数值范围[-100,100],所以用数组正合适。 | ||
|
||
**这个事实告诉我们,使用哈希法的时候,条件允许的话,能用数组尽量用数组。** | ||
|
||
优化后的代码如下: | ||
那么优化后的代码如下: | ||
|
||
``` | ||
// 版本二 | ||
class Solution { | ||
private: | ||
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& subseq, int startIndex) { | ||
if (subseq.size() > 1) { | ||
result.push_back(subseq); | ||
// 注意这里不要加return,因为要取所有的可能 | ||
} | ||
int hash[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] | ||
for (int i = startIndex; i < nums.size(); i++) { | ||
if ((subseq.empty() || nums[i] >= subseq.back()) | ||
&& hash[nums[i] + 100] == 0) { | ||
subseq.push_back(nums[i]); | ||
backtracking(nums, result, subseq, i + 1); | ||
subseq.pop_back(); | ||
hash[nums[i]+100] = 1; | ||
vector<vector<int>> result; | ||
vector<int> path; | ||
void backtracking(vector<int>& nums, int startIndex) { | ||
if (path.size() > 1) { | ||
result.push_back(path); | ||
} | ||
int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] | ||
for (int i = startIndex; i < nums.size(); i++) { | ||
if ((!path.empty() && nums[i] < path.back()) | ||
|| used[nums[i] + 100] == 1) { | ||
continue; | ||
} | ||
used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了 | ||
path.push_back(nums[i]); | ||
backtracking(nums, i + 1); | ||
path.pop_back(); | ||
} | ||
} | ||
} | ||
public: | ||
vector<vector<int>> findSubsequences(vector<int>& nums) { | ||
vector<vector<int>> result; | ||
vector<int> subseq; | ||
backtracking(nums, result, subseq, 0); | ||
result.clear(); | ||
path.clear(); | ||
backtracking(nums, 0); | ||
return result; | ||
} | ||
}; | ||
``` | ||
|
||
这份代码在leetcode上提交,要比版本一耗时要好的多。 | ||
|
||
**所以正如在[哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg)中说的那样,数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如何数值范围小的话能用数组尽量用数组**。 | ||
|
||
|
||
|
||
# 总结 | ||
|
||
本题题解清一色都说是深度优先搜索,但我更倾向于说它用回溯法,而且本题我也是完全使用回溯法的逻辑来分析的。 | ||
|
||
相信大家在本题中处处都能看到是[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)的身影,但处处又都是陷阱。 | ||
|
||
**对于养成思维定式或者套模板套嗨了的同学,这道题起到了很好的警醒作用。更重要的是拓展了大家的思路!** | ||
|
||
**就酱,如果感觉「代码随想录」很干货,就帮Carl宣传一波吧!** | ||
|
||
|
Oops, something went wrong.