贪心问题之区间调度

笔记摘自:labuladong 的算法小抄

目录

贪心算法

什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。

比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到 多项式级别 的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到 线性级别 的。

什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。注意哦,这是一种特殊性质,其实只有一部分问题拥有这个性质。

比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。

然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文 动态规划解决博弈问题。

区间问题

所谓区间问题,就是线段问题,让你合并所有线段、找出线段的交集等等。主要有两个技巧:

1、排序。常见的排序方法就是按照区间起点排序,或者先按照起点升序排序,若起点相同,则按照终点降序排序。当然,如果你非要按照终点排序,无非对称操作,本质都是一样的。

2、画图。就是说不要偷懒,勤动手,两个区间的相对位置到底有几种可能,不同的相对位置我们的代码应该怎么去处理。

第一个场景

  1. 假设现在只有一个会议室,还有若干会议,你如何将尽可能多的会议安排到这个会议室里?

这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 [start, end],表示开始和结束的时间,请问你今天最多能参加几个活动呢?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。

  1. 这种题的本质就是让你算出这些区间中最多有几个互不相交的区间。例如:力扣 435 无重叠区间

在这里插入图片描述

  1. 需要将这些会议(区间)按结束时间(右端点)排序,然后进行处理。
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;
}

第二个场景

  1. 给你若干较短的视频片段,和一个较长的视频片段,请你从较短的片段中尽可能少地挑出一些片段,拼接出较长的这个片段。

  2. 例如:力扣 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;
    }
};

第三个场景

  1. 给你若干区间,其中可能有些区间比较短,被其他区间完全覆盖住了,请你删除这些被覆盖的区间。

  2. 例如:力扣 1288 删除被覆盖区间

在这里插入图片描述
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;
    }
  1. 但是这道题没有必要用两个循环,因为这里没有两个层次的概念。用一个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. 写这个代码的时候想到了一件事,就是情况1定要写在情况2前面,否则的话right可能更新小了。

第四个场景

  1. 给你若干区间,请你将所有有重叠部分的区间进行合并。

  2. 例如:力扣 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;
}
  1. 正确的这么写,这里面的引用用的真好。(这几个场景的代码需要好好看看)
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;
}

第五个场景

  1. 有两个部门同时预约了同一个会议室的若干时间段,请你计算会议室的冲突时段。

  2. 例如:力扣 986 区间列表的交集

在这里插入图片描述
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;
    }
  1. 事实上题目中已经给了信息,两个区间列表是已经排好序的,我们用两个指针就可以。
 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 背包问题详解 的思路即可解决。

第七个场景

  1. 给你输入若干形如 [begin, end] 的区间,代表若干会议的开始时间和结束时间,请你计算至少需要申请多少间会议室。「 本质:计算同一时刻 最多 有几个区间重叠 」

  2. 例如:力扣 253 会议室II

  3. 首先这道题可以用 差分数组 的方法去做,也就是平凡给一段区间进行加减法的操作,最后在差分数组返回的结果数组中找最大值就好了。差分数组技巧可以在 O(1) 时间对整个区间的元素进行加减,所以可以拿来解决这道题。

  4. 这道题的另一种做法:结合下面这张图,假想有一条带着计数器的线,在时间线上从左至右进行扫描,每遇到红色的点,计数器 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;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-月光光-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值