LeetCode56.合并区间
1.思路
思路和 用最少数量的弓箭引爆气球 一样,按照左边界从小到大排序后,for 从 1 开始遍历,先记录上一个区间的左边界 l 和右边界 r ,然后如果下一个的左边界小于等于上一个的右边界(即 intervals[i][0] <= intervals[i - 1][1]),说明这两个区间是重合的,然后使用 while 循环判断后面是否还有区间和当前区间重合,更新右边界,最后存入答案集(ans.push_back({l,r}))。
cpp
class Solution {
public:
static bool cmp(vector<int>& a,vector<int>& b){
return a[0]<b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),cmp);
vector<vector<int>>ans;
for(int i=1;i<=intervals.size();i++){
int l=intervals[i-1][0];
int r=intervals[i-1][1];
while(i<intervals.size() && intervals[i][0]<=intervals[i-1][1]){
intervals[i][1]=max(intervals[i][1],intervals[i-1][1]);
r=intervals[i][1];
i++;
}
ans.push_back({l,r});
}
return ans;
}
};
注:
这里 for 循环要遍历到 intervals.size() 停止,因为是从 1 开始遍历的,每次存的是前一个合并的区间,要想把最后一个区间也存入数组,必须遍历到原数组末尾的下一位;
因为要遍历到 intervals.size() ,所以第一个前提就是 i<intervals.size() ,然后才能接着判断是否存在连续的重叠区间;
while 里最后的 i++ :因为实际处理的是 i 前面的重叠区间,所以找到右边界后需要 i++ ,这样才不会重复放入 i 处的区间。
优化
为了避免不断寻找右边界,可以直接对 ans 的末尾修改。只需用 ans.back()[1] 来和 intervals[i][0] 来做比较,判断是否有重叠区间,有的话就修改 ans.back()[1] 为最大的右边界;没有重叠区间就直接放进 ans 里就行。
cpp
class Solution {
public:
static bool cmp(vector<int>& a,vector<int>& b){
return a[0]<b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),cmp);
vector<vector<int>>ans;
if(intervals.size()==0) return ans;
ans.push_back(intervals[0]);
for(int i=1;i<intervals.size();i++){
if(intervals[i][0] <= ans.back()[1]){
ans.back()[1]=max(ans.back()[1],intervals[i][1]);
}
else{
ans.push_back(intervals[i]);
}
}
return ans;
}
};
2.复杂度分析
时间复杂度:O(nlog n)
空间复杂度:O(logn)
3.思考
按照 用最少数量的弓箭引爆气球 的思路,这道题还是很好想的,固定左边界,寻找不重叠的最大右边界;第二种方法属实震惊到我了,我只想到了更新右边界,没想到可以先把区间放入数组种,如果有重叠区间,再更新数组末位的值就行了。对容器的使用更深一步理解了。
4.Reference:56. 合并区间 | 代码随想录
LeetCode738.单调递增的数字
1.思路
这道题需要想清楚个例,例如 98 ,一旦出现 a[i-1] > a[i] 的情况,首先让 a[i-1]--,a[i]='9',这样下来最接近 98 的单调递增数字就是 89 。
那么是怎样遍历呢?这里必须为从后往前遍历,举个例子 323,从前往后遍历得到的是329,从后往前则是299 (332->329->299) ;
在遍历遇到 a[i-1] > a[i] 时,不能直接就给 a[i] 赋值为 '9',举个例子 100,如果直接赋值的话,最终结果就是 90 ,这与正确答案 99 不同,所以不能在遍历时直接修改,而是需要用 index 存下标,用来标记从哪个位置开始赋值 '9',最后通过 for 循环来赋值。
cpp
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string a = to_string(n);
int index=a.size();
for(int i=a.size()-1;i>0;i--){
if(a[i-1]>a[i]){
a[i-1]--;
index=i;
}
}
for(int i=index;i<a.size();i++){
a[i]='9';
}
return stoi(a);
}
};
2.复杂度分析
时间复杂度:O(n)
空间复杂度:O(n)
3.思考
本题难点在于要想清楚个例,一旦出现 a[i-1] > a[i] 的情况,首先让 a[i-1]--,a[i]='9' ;
还要想好遍历的顺序,从前往后和从后往前遍历是截然不同的;
最后就是不能直接在遍历时给 a[i] 赋值'9',而是先通过 index 存下标,最后 for 循环来赋值。
4.Reference:738.单调递增的数字 | 代码随想录
LeetCode968.监控二叉树
1.思路
难难难!
这里需要用到递归,先考虑每个节点的状态:
0:该节点无覆盖
1:本节点有摄像头
2:本节点有覆盖
依旧递归三部曲:
-
返回 int(代表该节点的状态),传入节点,定义一个全局变量 res 存答案;
-
遇到空节点返回 2(空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了);
-
后序遍历,先收集左右孩子的状态,再分情况讨论:
情况1:左右节点都有覆盖
return 0 ,此时中间节点就是无覆盖的状态了。
情况2:左右节点至少有一个无覆盖的情况
有一个孩子没有覆盖,父节点就应该放摄像头。此时摄像头的数量要加一,并且return 1,代 表中间节点放摄像头。
情况3:左右节点至少有一个有摄像头
return 2 ,代表父节点是覆盖的状态。
最后判断根节点是否是情况1,如果是的话,就单独让 res++;
cpp
class Solution {
public:
int res=0;
int traversal(TreeNode* node){
if(node==NULL) return 2;
int left = traversal(node->left);
int right = traversal(node->right);
// 情况1
// 左右节点都有覆盖
if(left==2 && right==2){
return 0;
}
// 情况2
// left == 0 && right == 0 左右节点无覆盖
// left == 1 && right == 0 左节点有摄像头,右节点无覆盖
// left == 0 && right == 1 左节点有无覆盖,右节点摄像头
// left == 0 && right == 2 左节点无覆盖,右节点覆盖
// left == 2 && right == 0 左节点覆盖,右节点无覆盖
if(left==0 || right==0){
res++;
return 1;
}
// 情况3
// left == 1 && right == 2 左节点有摄像头,右节点有覆盖
// left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// left == 1 && right == 1 左右节点都有摄像头
if(left==1 || right==1){
return 2;
}
return -1;
}
int minCameraCover(TreeNode* root) {
// 情况4 root 无覆盖
if(traversal(root)==0) res++;
return res;
}
};
2.复杂度分析
时间复杂度:O(n)
空间复杂度:O(n)
3.思考
本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。
在二叉树上进行状态推导,难度上了一个台阶了。这道题目是名副其实的hard,难难难!
4.Reference:968.监控二叉树 | 代码随想录
贪心算法总结
一、理论
- 核心逻辑:无需严格数学证明,手动模拟无反例、能自圆其说,即可尝试贪心思路。
- 判定标准:若能找到明确的 "局部最优选择",且该选择可累积成 "全局最优结果",则适合用贪心。
- 学习关键:不纠结 "是否为纯贪心题",重点掌握解题思路,结合题目特点灵活应用。
二、刷题
- 刷题顺序:不严格按 "简单到困难",而是简单与困难交错,整体呈阶梯式上升。
- 与回溯法差异:回溯需严格按框架顺序刷题(前后题目有因果关系),贪心无此限制。
- 额外要求:需理解所用编程语言的内部机制(如 C++ 中 list 与 vector 的效率差异),才能写出高效代码。
三、题型分类与要点
1. 简单题(常识类)
- 特点:思路贴近常识,重点是明确 "局部最优" 与 "全局最优" 的对应关系。
- 代表题目:分发饼干、K 次取反后最大化的数组和、柠檬水找零。
2. 中等题(巧思类)
- 特点:仅靠常识难以突破,需挖掘题目隐藏的 "最优逻辑"。
- 代表题目:摆动序列、单调递增的数字。
3. 股票问题(贪心应用类)
- 特点:动态规划是专长,但贪心可实现更简洁的解法。
- 代表题目:买卖股票的最佳时机 II、买卖股票的最佳时机含手续费(后者贪心较绕,可结合动规理解)。
4. 双维度权衡问题(优先级类)
- 特点:两个维度相互影响,需先固定一个维度排序,再处理另一个维度,避免顾此失彼。
- 代表题目:分发糖果、根据身高重建队列。
5. 区间问题(高频重点类)
- 特点:围绕区间的覆盖、去重、合并等操作,贪心思路集中且典型。
- 代表题目:跳跃游戏、跳跃游戏 II、用最少数量的箭引爆气球、无重叠区间、划分字母区间、合并区间。
6. 其他难题(综合类)
- 特点:需结合其他知识点(如二叉树、数组操作),贪心思路隐蔽,需反复练习。
- 代表题目:最大子序和(贪心比动规更优)、加油站、监控二叉树。
四、学习建议
- 多刷重复题:难题(如区间问题、监控二叉树)需反复练习,强化贪心 "感觉"。
- 聚焦局部最优:解题时先明确 "当前步的最优选择是什么",再验证是否能推导全局最优。
- 结合其他算法:部分题目(如股票、最大子序和)可对比贪心与动规的解法,加深理解。