diff --git a/README.md b/README.md index e5f4f665..81881558 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,8 @@ * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) * [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA) + * [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw) + * [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) (持续更新中....) @@ -317,6 +319,7 @@ |[0052.N皇后II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0052.N皇后II.md) |回溯|困难| **回溯**| |[0053.最大子序和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |简单|**暴力** **贪心** 动态规划 分治| |[0055.跳跃游戏](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |中等| **贪心** 经典题目| +|[0057.插入区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0057.插入区间.md) |数组 |困难| **模拟** 是一道数组难题| |[0059.螺旋矩阵II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0059.螺旋矩阵II.md) |数组 |中等|**模拟**| |[0077.组合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0077.组合.md) |回溯 |中等|**回溯**| |[0078.子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0078.子集.md) |回溯/数组 |中等|**回溯**| @@ -381,12 +384,13 @@ |[0416.分割等和子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0416.分割等和子集.md) |动态规划 |中等|**背包问题/01背包**| |[0429.N叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0429.N叉树的层序遍历.md) |树 |简单|**队列/广度优先搜索**| |[0434.字符串中的单词数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0434.字符串中的单词数.md) |字符串 |简单|**模拟**| +|[0435.无重叠区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0435.无重叠区间.md) |贪心 |中等|**贪心** 经典题目,有点难| |[0450.删除二叉搜索树中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0450.删除二叉搜索树中的节点.md) |树 |中等|**递归**| |[0454.四数相加II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0454.四数相加II.md) |哈希表 |中等| **哈希**| |[0455.分发饼干](https://github.com/youngyangyang04/leetcode/blob/master/problems/0455.分发饼干.md) |贪心 |简单| **贪心**| |[0459.重复的子字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0459.重复的子字符串.md) |字符创 |简单| **KMP**| |[0486.预测赢家](https://github.com/youngyangyang04/leetcode/blob/master/problems/0486.预测赢家.md) |动态规划 |中等| **递归** **记忆递归** **动态规划**| -|[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法**| +|[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法** 这个去重有意思| |[0496.下一个更大元素I](https://github.com/youngyangyang04/leetcode/blob/master/problems/0496.下一个更大元素I.md) |栈 |中等|**单调栈** 入门题目,但是两个数组还是有点绕的| |[0501.二叉搜索树中的众数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0501.二叉搜索树中的众数.md) |二叉树 |简单|**递归/中序遍历**| |[0513.找树左下角的值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0513.找树左下角的值.md) |二叉树 |中等|**递归** **迭代**| @@ -415,6 +419,7 @@ |[0925.长按键入](https://github.com/youngyangyang04/leetcode/blob/master/problems/0925.长按键入.md) |字符串 |简单|**双指针/模拟** 是一道模拟类型的题目| |[0941.有效的山脉数组](https://github.com/youngyangyang04/leetcode/blob/master/problems/0941.有效的山脉数组.md) |数组 |简单|**双指针**| |[0968.监控二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0968.监控二叉树.md) |二叉树 |困难|**贪心** 贪心与二叉树的结合| +|[0973.最接近原点的K个点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0973.最接近原点的K个点.md) |优先级队列 |中等|**优先级队列**| |[0977.有序数组的平方](https://github.com/youngyangyang04/leetcode/blob/master/problems/0977.有序数组的平方.md) |数组 |中等|**双指针** 还是比较巧妙的| |[1002.查找常用字符](https://github.com/youngyangyang04/leetcode/blob/master/problems/1002.查找常用字符.md) |栈 |简单|**栈**| |[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |哈希表 |简单|**哈希表/数组**| diff --git a/pics/491. 递增子序列1.png b/pics/491. 递增子序列1.png new file mode 100644 index 00000000..1addc4c0 Binary files /dev/null and b/pics/491. 递增子序列1.png differ diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md new file mode 100644 index 00000000..31d00fbc --- /dev/null +++ b/problems/0435.无重叠区间.md @@ -0,0 +1,40 @@ + +## 思路 + +这道题目如果真的去模拟去重复区间的行为,是非常麻烦的,还要有删除区间。 + +**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** + +按照右边界排序,那么右边界越小越好,因为右边界越小,留给下一个区间的空间就越大,所以可以从左向右遍历,优先选右边界小的。 + +按照左边界排序,那么就是左边界越大越好,这样就给前一个区间的空间就越大,所以可以从右向左遍历。 + +如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就比较复杂了,这其实也就不是贪心了。 + + + +在每次选择中,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 + + +``` +class Solution { +public: + // 按照区间右边界排序 + static bool cmp (const vector& a, const vector& b) { + return a[1] < b[1]; + } + int eraseOverlapIntervals(vector>& 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; + } +}; +``` diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md index dd9d96b3..8722858b 100644 --- a/problems/0491.递增子序列.md +++ b/problems/0491.递增子序列.md @@ -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]这个数组来举例,抽象为树形结构如图: -在递归的过程中 `if ((subseq.empty() || nums[i] >= subseq.back()) && uset.find(nums[i]) == uset.end())` 这个判断条件一定要想清楚, 如果子序列为空或者nums[i]>=子序列尾部数值,**同时** 这个nums[i] 不能出现过, 因为一旦出现过就 是一个重复的递增子序列了。 + +## 回溯三部曲 + +* 递归函数参数 + +本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。 + +代码如下: + +``` +vector> result; +vector path; +void backtracking(vector& 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,因为要取树上的所有节点 +} +``` + +* 单层搜索逻辑 + + + +在图中可以看出,同层上使用过的元素就不能在使用了,**注意这里和[回溯算法:求子集问题(二)](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 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 uset;` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!** + + +最后整体C++代码如下: ## C++代码 ``` +// 版本一 class Solution { private: -void backtracking(vector& nums, vector>& result, vector& subseq, int startIndex) { - if (subseq.size() > 1) { - result.push_back(subseq); - // 注意这里不要加return,因为要取所有的可能 - } - unordered_set 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> result; + vector path; + void backtracking(vector& nums, int startIndex) { + if (path.size() > 1) { + result.push_back(path); + // 注意这里不要加return,要取树上的节点 + } + unordered_set 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> findSubsequences(vector& nums) { - vector> result; - vector subseq; - backtracking(nums, result, subseq, 0); + result.clear(); + path.clear(); + backtracking(nums, 0); return result; } }; ``` -一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图: +## 优化 - +以上代码用我用了`unordered_set`来记录本层元素是否重复使用。 -改动的地方主要是将去重的逻辑中把 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& nums, vector>& result, vector& 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> result; + vector path; + void backtracking(vector& 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> findSubsequences(vector& nums) { - vector> result; - vector 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宣传一波吧!** + + diff --git a/problems/0973.最接近原点的K个点.md b/problems/0973.最接近原点的K个点.md new file mode 100644 index 00000000..5090614f --- /dev/null +++ b/problems/0973.最接近原点的K个点.md @@ -0,0 +1,83 @@ + +## 思路 + +这道题其实我在讲解队列的时候,就已经讲过了,在[栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng)中,我介绍了一种队列, 优先级队列,其实就是大(小)顶堆。 + +大家有精力的话也可以做做[347.前 K 个高频元素](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng),347是求前k的高频元素,本题呢,其实是求前k的低频元素。 + +所以套路都是一样一样的。 + +有的同学会用快排,其实快排的话把所有元素都排序了,时间复杂度是O(nlogn),而使用优先级队列时间复杂度为O(nlogk),因为只需要维护k个元素有序。 + +然后就是为什么要定义大顶堆呢? + +因为本地要求最小k个数,每次添加堆,都是从顶部把最大的弹出去,然后堆里留下的就是最小的k个数了。 + +C++代码如下: + +``` +// 版本一 +class Solution { +public: + // 大顶堆比较函数 + class mycomparison { + public: + bool operator()(const pair>& lhs, const pair>& rhs) { + return lhs.first < rhs.first; + } + }; + vector> kClosest(vector>& points, int K) { + // 定义一个大顶堆 + priority_queue>, vector>>, mycomparison> pri_que; + for(int i = 0; i < points.size(); i++) { + int x = points[i][0]; + int y = points[i][1]; + pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) + pri_que.push(p); + if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k + pri_que.pop(); + } + } + vector> result(K); // 把队列里元素放入数组 + for (int i = 0; i < K; i++) { + result[i] = pri_que.top().second; + pri_que.pop(); + } + return result; + } +}; +``` + +以上是为了完整的体现出优先级队列的定义以及比较过程。 + +如果要简化一下,就用默认的配置就可以。代码如下: + +``` +// 版本二 +class Solution { +public: + vector> kClosest(vector>& points, int K) { + // 默认大顶堆,按照pair的key排序 + priority_queue>, vector>>> pri_que; + for(int i = 0; i < points.size(); i++) { + int x = points[i][0]; + int y = points[i][1]; + pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) + pri_que.push(p); + if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k + pri_que.pop(); + } + } + vector> result(K); // 把队列里元素放入数组 + for (int i = 0; i < K; i++) { + result[i] = pri_que.top().second; + pri_que.pop(); + } + return result; + } +}; +``` + + +> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! + diff --git a/problems/贪心算法理论基础.md b/problems/贪心算法理论基础.md new file mode 100644 index 00000000..d5afc481 --- /dev/null +++ b/problems/贪心算法理论基础.md @@ -0,0 +1,47 @@ + +期盼通过每个阶段的局部最优选择,从而达到全局最优 + +当前子问题的最优解 + +一个子问题的最优解会是下一个子问题最优解的一部分,重复这个操作直到堆叠出该问题的最优解 + +贪心算法最关键的部分在于贪心策略的选择,贪心选择的意思是对于所求问题的整体最优解可以通过一系列的局部最优选择求得。 + +而必须注意的是,贪心选择必须具备无后效性,也就是某个状态不会影响之前求得的局部最优解。 + + +很多同学做贪心的题目的时候,想不出来是贪心,想知道有没有什么套路可以一看看出来是贪心,说实话贪心算法并没有固定的套路。 + +所以唯一的难点就是如何通过局部最优,推出整体最优。 + +那么如何能看出局部最优是否能退出整体最优呢?有没有什么固定策略呢? + +不好意思,也没有,靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,不过不可行,可能需要动态规划了。 + +那又有同学想手动模拟得出的结论不靠谱,想要严格的数学证明。 + +做了贪心题目的时候大家就会发现,如果啥都要数学证明,就是把简单问题搞复杂了。 + +举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但有这个必要么。 + +虽然这个例子有点极端,但可以表达出我的意思,就是手动模拟一下感觉可以局部最优推出整体最优,那么就试一试贪心。 + + +刷题的时候什么时候真的需要数学推导,例如环形链表2,这道题目不用数学推导一下,就找不出环的其实位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 + +但贪心问题就不必了,模拟一下感觉是这么回事,就迅速试一试。 + +,但毕竟熟能生巧嘛,算法的基本思想总是固定不变的。 + +贪心算法求解步骤 +* 将问题分解为若干个子问题 +* 找出适合的贪心策略 +* 求解每一个子问题的最优解 +* 将局部最优解堆叠成全局最优解 + + +例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿? + +每次拿最大的啊,这就是局部最优,然后可以退出全局最优。 + +但例如是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满, 如果还每次选最大的盒子,一定不行。这时候就需要动态规划。