diff --git a/README.md b/README.md index 6276c49b..55af7c35 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,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) |数组 |中等| **贪心** 经典题目| +|[0056.合并区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0056.合并区间.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) |回溯 |中等|**回溯**| diff --git a/pics/56.合并区间.png b/pics/56.合并区间.png new file mode 100644 index 00000000..ff905c72 Binary files /dev/null and b/pics/56.合并区间.png differ diff --git a/problems/0056.合并区间.md b/problems/0056.合并区间.md new file mode 100644 index 00000000..2d891c36 --- /dev/null +++ b/problems/0056.合并区间.md @@ -0,0 +1,55 @@ +## 题目链接 +https://leetcode-cn.com/problems/merge-intervals/ + +## 思路 +这道题目看起来就是一道模拟类的题,但其实是一道贪心题目! + +按照左区间排序之后,每次合并都取最大的右区间,这样就可以合并更多的区间了。 + +那有同学问了,这不是废话么? 当然要取最大的右区间啊。 + +**是的,一想就是这么个道理,但它就是贪心的思想,局部最优推导出整体最优**。 + +这也就是为什么很多同学刷题的时候都没有发现自己用了贪心。 + +合并思路:如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]起始位置 < intervals[i - 1]终止位置,则一定有重复,需要合并。 + +如图所示: + + + +C++代码如下: + +``` +class Solution { +public: + // 按照区间左边界排序 + static bool cmp (const vector& a, const vector& b) { + return a[0] < b[0]; + } + + vector> merge(vector>& intervals) { + vector> result; + if (intervals.size() == 0) return result; + sort(intervals.begin(), intervals.end(), cmp); + bool flag = false; // 标记最后一个区间有没有合并 + int length = intervals.size(); + + for (int i = 1; i < length; i++) { + int start = intervals[i - 1][0]; + int end = intervals[i - 1][1]; + while (i < length && intervals[i][0] <= end) { // 合并区间 + end = max(end, intervals[i][1]); + if (i == length - 1) flag = true; // 最后一个区间也合并了 + i++; + } + result.push_back({start, end}); + } + // 如果最后一个区间没有合并,将其加入result + if (flag == false) { + result.push_back({intervals[length - 1][0], intervals[length - 1][1]}); + } + return result; + } +}; +``` diff --git a/problems/贪心算法理论基础.md b/problems/贪心算法理论基础.md index a8aeee56..22d7abb9 100644 --- a/problems/贪心算法理论基础.md +++ b/problems/贪心算法理论基础.md @@ -19,13 +19,22 @@ 那么如何能看出局部最优是否能退出整体最优呢?有没有什么固定策略呢? -不好意思,也没有,靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,不过不可行,可能需要动态规划。 +不好意思,也没有,靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。 -那又有同学认为手动模拟得出的结论不靠谱,想要严格的数学证明。 +验证可不可以用贪心最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。 + +那又有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。 + +数学证明一般可以是 + +* 数学归纳法 +* 反证法 看教课书上讲解贪心真的是一堆公式,估计连看都不想看。 -所以做了贪心题目的时候大家就会发现,如果啥都要数学证明,就是把简单问题搞复杂了。 +所以做了贪心题目的时候大家就会发现,如果啥都要数学证明一下,就是把简单问题搞复杂了。 + +面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说就行。 举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但没必要。 @@ -33,11 +42,14 @@ 例如刚刚举的拿钞票的例子,就是模拟一下每次那做大的,最后就能拿到最多的钱,这还要数学证明的话,是不是感觉有点怪怪的。 -刷题的时候什么时候真的需要数学推导呢? +所以这也是为什么有的通过AC(accept)了一些贪心的题目,但都不知道自己用了贪心算法,因为贪心有时候就是常识性的推导,所以会认为本就应该这么做! + +那么刷题的时候什么时候真的需要数学推导呢? 例如环形链表2,这道题目不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 -但贪心问题就不必了,模拟一下感觉是这么回事,就迅速试一试。 +**但贪心问题就不必了,模拟一下感觉是这么回事,就迅速试一试**。 + 贪心算法求解步骤: @@ -54,3 +66,10 @@ 贪心算法最关键的部分在于贪心策略的选择,贪心选择的意思是对于所求问题的整体最优解可以通过一系列的局部最优选择求得。 而必须注意的是,贪心选择必须具备无后效性,也就是某个状态不会影响之前求得的局部最优解。 + + +贪心算法的应用 +对数据压缩编码的霍夫曼编码(Huffman Coding) +求最小生成树的 Prim 算法和 Kruskal 算法 +求单源最短路径的Dijkstra算法 +