笔记摘自:labuladong 的算法小抄
目录
贪心算法
什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。
比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到 多项式级别 的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到 线性级别 的。
什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。注意哦,这是一种特殊性质,其实只有一部分问题拥有这个性质。
比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。
然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文 动态规划解决博弈问题。
区间问题
所谓区间问题,就是线段问题,让你合并所有线段、找出线段的交集等等。主要有两个技巧:
1、排序。常见的排序方法就是按照区间起点排序,或者先按照起点升序排序,若起点相同,则按照终点降序排序。当然,如果你非要按照终点排序,无非对称操作,本质都是一样的。
2、画图。就是说不要偷懒,勤动手,两个区间的相对位置到底有几种可能,不同的相对位置我们的代码应该怎么去处理。
第一个场景
- 假设现在只有一个会议室,还有若干会议,你如何将尽可能多的会议安排到这个会议室里?
这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 [start, end],表示开始和结束的时间,请问你今天最多能参加几个活动呢?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。
- 这种题的本质就是让你算出这些区间中最多有几个互不相交的区间。例如:力扣 435 无重叠区间
- 需要将这些会议(区间)按结束时间(右端点)排序,然后进行处理。
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size()==0){
return 0;
}
sort(intervals.begin(),intervals.end(),[](vector<int> &a,vector<int> &b){
return a[1] < b[1];
});
int count = 1;
int end = intervals[0][1];
for(vector<int> &tmp : intervals){
if(tmp[0]>=end){
count++;
end = tmp[1];
}
}
return intervals.size()-count;
}
第二个场景
-
给你若干较短的视频片段,和一个较长的视频片段,请你从较短的片段中尽可能少地挑出一些片段,拼接出较长的这个片段。
-
例如:力扣 1024 视频拼接
3. 我能知道他肯定按起点排序,但是怎么往后继续搜索不清楚。这个题解是用了两个循环,在和 curEnd 作比较的同时不断更新 nxtEnd 。当确定有一个一定会被选择之后,就可以选出下一个会被选择的视频:
class Solution {
public:
int videoStitching(vector<vector<int>>& clips, int time) {
sort(clips.begin(),clips.end(),[](const vector<int> &a,const vector<int> &b){
if(a[0]==b[0]){
return a[1]>b[1];
}
return a[0]<b[0];
});
int i = 0, num = clips.size();
int curEnd = 0, nxtEnd = 0;
int res = 0;
while( i<num && clips[i][0]<=curEnd ){
while( i<num && clips[i][0]<=curEnd ){
nxtEnd = max(nxtEnd,clips[i][1]);
i++;
}
res++;
curEnd = nxtEnd;
if(nxtEnd>=time){
return res;
}
}
return -1;
}
};
第三个场景
-
给你若干区间,其中可能有些区间比较短,被其他区间完全覆盖住了,请你删除这些被覆盖的区间。
3. 看完第二个场景后不难写出这道题的解。
int removeCoveredIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),[](const vector<int> &a,const vector<int> &b){
if(a[0]==b[0]){
return a[1]>b[1];
}
return a[0]<b[0];
});
int i = 0, num = intervals.size();
int start = 0, end = 0;
int count = 0;
while(i<num && intervals[i][0]>=start){
while(i<num && intervals[i][0]>=start){
if(intervals[i][1]<=end){
count++;
}else{
start = intervals[i][0];
end = intervals[i][1];
}
i++;
}
}
return num-count;
}
- 但是这道题没有必要用两个循环,因为这里没有两个层次的概念。用一个for循环就好了,但是第二个场景不能用一个for循环,因为你要对 res 进行 +1 操作。这道题排序后相邻区间无外乎以下三种情况。
public int removeCoveredIntervals(int[][] intvs) {
// 按照起点升序排列,起点相同时降序排列
Arrays.sort(intvs, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
// 记录合并区间的起点和终点
int left = intvs[0][0];
int right = intvs[0][1];
int res = 0;
for (int i = 1; i < intvs.length; i++) {
int[] intv = intvs[i];
// 情况一,找到覆盖区间
if (left <= intv[0] && right >= intv[1]) { // 这个判断里 left <= intv[0] 可以删
res++;
}
// 情况二,找到相交区间,合并
if (intv[0]<=right && intv[1]>=right) {
// left = intv[0]; 这里没有必要对left进行修改
right = intv[1];
}
// 情况三,完全不相交,更新起点和终点
if (intv[0]>right) {
left = intv[0];
right = intv[1];
}
}
return intvs.length - res;
}
- 写这个代码的时候想到了一件事,就是情况1定要写在情况2前面,否则的话right可能更新小了。
第四个场景
-
给你若干区间,请你将所有有重叠部分的区间进行合并。
-
例如:力扣 56 合并区间
3. 下面是我一开始写的代码,但是不对。问题会出现最后一个或几个区间是没有加入的。因为 push_back 的前提是后面有一个区间满足 if 条件的判断。如果想让这段代码成立的话,可以在排序后往intervals里再追加一个最大的区间。
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),[](const vector<int> &a,const vector<int> &b){
if(a[0]==b[0]){
return a[1]>b[1];
}
return a[0]<b[0];
});
int num = intervals.size();
int left = intervals[0][0],right=intervals[0][1];
vector<vector<int>> res;
for(int i=1;i<num;i++){
vector<int> tmp = intervals[i];
if(tmp[1]<=right){
continue;
}
if(tmp[0]<=right && tmp[1]>=right){
right = tmp[1];
}
if(tmp[0]>right){
vector<int> t{left,right};
res.push_back(t);
left = tmp[0];
right = tmp[1];
}
}
return res;
}
- 正确的这么写,这里面的引用用的真好。(这几个场景的代码需要好好看看)
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
// 按区间的 start 升序排列
sort(intervals.begin(), intervals.end(), [](auto& a, auto& b){
return a[0] < b[0];
});
res.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
auto& curr = intervals[i];
// res 中最后一个元素的引用
auto& last = res.back();
if (curr[0] <= last[1]) {
last[1] = max(last[1], curr[1]);
} else {
// 处理下一个待合并区间
res.push_back(curr);
}
}
return res;
}
第五个场景
-
有两个部门同时预约了同一个会议室的若干时间段,请你计算会议室的冲突时段。
3. 我的代码就比较麻烦了,因为涉及到了排序。
vector<vector<int>> intervalIntersection(vector<vector<int>>& firstList, vector<vector<int>>& secondList) {
vector<vector<int>> res;
if(firstList.size()==0 || secondList.size()==0){
return res;
}
firstList.insert(firstList.end(),secondList.begin(),secondList.end());
sort(firstList.begin(),firstList.end(),[](vector<int> &a,vector<int> &b){
if(a[0]==b[0]){
return a[1]>b[1];
}
return a[0]<b[0];
});
int left = firstList[0][0],right = firstList[0][1];
for(int i=1;i<firstList.size();i++){
vector<int> tmp = firstList[i];
if(tmp[0]<=right){
if(tmp[1]<=right){
res.push_back(vector({tmp[0],tmp[1]}));
}else{
res.push_back(vector({tmp[0],right}));
left = tmp[0];
right = tmp[1];
}
}else{
left = tmp[0];
right = tmp[1];
}
}
return res;
}
- 事实上题目中已经给了信息,两个区间列表是已经排好序的,我们用两个指针就可以。
while i < len(A) and j < len(B):
# ...
j += 1
i += 1
下面我们就要看什么情况下会产生交集了。
也就是在 b[1] <= a[2] 并且 a[1] <= b[2] 的时候了。下面思考重叠区间怎么看,不管是哪种情况都是 [ max(a[0],b[0]) , min(a[1],b[1]) ] 。
这里解释一下为什么是并且,因为还有两种情况是没有交集的。以下两种情况,b[1] > a[2] || a[1] > b[2] ,根据命题的否定,就能到存在交集是什么情况。
后面就是 i 和 j 怎么动了,也就是什么时候 +1 , 1 和 4 是 j++ ,2 和 3 是 i++ ,其实就是谁的区间尾巴短谁去换下一个。代码如下:
vector<vector<int>> intervalIntersection(vector<vector<int>>& firstList, vector<vector<int>>& secondList) {
vector<vector<int>> res;
if(firstList.size()==0 || secondList.size()==0){
return res;
}
int i = 0, j = 0;
while(i<firstList.size() && j<secondList.size()){
auto a = firstList[i];
auto b = secondList[j];
if(b[0]<=a[1] && b[1]>=a[0]){
res.push_back( vector( { max(a[0],b[0]) , min(a[1],b[1]) } ) );
}
if(a[1]>b[1]){
j++;
}else{
i++;
}
}
return res;
}
第六个场景
假设现在只有一个会议室,还有若干会议,如何安排会议才能使这个会议室的闲置时间最少?(不同于第一个场景)
这个问题需要动动脑筋,说白了这就是个 0-1 背包问题的变形:
会议室可以看做一个背包,每个会议可以看做一个物品,物品的价值就是会议的时长,请问你如何选择物品(会议)才能最大化背包中的价值(会议室的使用时长)?
当然,这里背包的约束不是一个最大重量,而是各个物品(会议)不能互相冲突。把各个会议按照结束时间进行排序,然后参考前文 0-1 背包问题详解 的思路即可解决。
第七个场景
-
给你输入若干形如 [begin, end] 的区间,代表若干会议的开始时间和结束时间,请你计算至少需要申请多少间会议室。「 本质:计算同一时刻 最多 有几个区间重叠 」
-
例如:力扣 253 会议室II
-
首先这道题可以用 差分数组 的方法去做,也就是平凡给一段区间进行加减法的操作,最后在差分数组返回的结果数组中找最大值就好了。差分数组技巧可以在 O(1) 时间对整个区间的元素进行加减,所以可以拿来解决这道题。
-
这道题的另一种做法:结合下面这张图,假想有一条带着计数器的线,在时间线上从左至右进行扫描,每遇到红色的点,计数器 count 加一,每遇到绿色的点,计数器 count 减一,这样一来,每个时刻有多少个会议在同时进行,就是计数器 count 的值,count 的最大值,就是需要申请的会议室数量。
- 这样做的话,需要使用到双指针了。
int minMeetingRooms(vector<vector<int>>& meetings) {
int n = meetings.size();
vector<int> begin(n);
vector<int> end(n);
for(int i = 0; i < n; i++) {
begin[i] = meetings[i][0];
end[i] = meetings[i][1];
}
sort(begin.begin(), begin.end());
sort(end.begin(), end.end());
// 扫描过程中的计数器
int count = 0;
// 双指针技巧
int res = 0, i = 0, j = 0;
while (i < n && j < n) {
if (begin[i] < end[j]) { // 这个判断应该不能写等于号
// 扫描到一个红点
count++;
i++;
} else {
// 扫描到一个绿点
count--;
j++;
}
// 记录扫描过程中的最大值
res = max(res, count);
}
return res;
}