力扣630.课程表

题目描述:

这里有 n 门不同的在线课程,按从 1 到 n 编号。给你一个数组 courses ,其中 courses[i] = [durationi, lastDayi] 表示第 i 门课将会 持续 上 durationi 天课,并且必须在不晚于 lastDayi 的时候完成。

你的学期从第 1 天开始。且不能同时修读两门及两门以上的课程。

返回你最多可以修读的课程数目。

示例 1:

输入:courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]]
输出:3
解释:
这里一共有 4 门课程,但是你最多可以修 3 门:
首先,修第 1 门课,耗费 100 天,在第 100 天完成,在第 101 天开始下门课。
第二,修第 3 门课,耗费 1000 天,在第 1100 天完成,在第 1101 天开始下门课程。
第三,修第 2 门课,耗时 200 天,在第 1300 天完成。
第 4 门课现在不能修,因为将会在第 3300 天完成它,这已经超出了关闭日期。

示例 2:

输入:courses = [[1,2]]
输出:1

示例 3:

输入:courses = [[3,2],[4,3]]
输出:0

提示:

  • 1 <= courses.length <= 104
  • 1 <= durationi, lastDayi <= 104

前言:

这个题在力扣里面难度是困难,又是求选取集合的最值问题,这种问题一看就是用动态规划或者贪心之类的去做。一般刷题经验告诉我,能贪心就贪心,不能贪心再去考虑动态规划,除非是那种非常裸的动态规划的板子题,不然还是看能不能贪(或许这也是一种贪心思维hhh),毕竟推动规的公式要花很多时间(对于我这种菜鸟来说)。然而这题目也确实可以用贪心去做,只不过要用优先队列。

思路:

首先,如果持续时间>截止日期,不用考虑。

既然是贪心,怎么贪呢?是以课程的 持续时间durationi 还是以截止日期 lastDayi为贪心的目标呢?换句话来说,就是考虑哪些因素能让我们选更多的课呢?当然是看截止日期lastDay嘛!最终课程能上多少,取决于截止日期。

那么先考虑lastday大的还是小的呢?

这里遇到以下情况:有课程a,durationi=d1,lastday=t1,课程b,durationi=d2,lastday=t2.

假设t1

有两种方案(贪心思想,都拿):

方案一:先拿a再拿b.需满足:t+d1<=t1且t+d1+d2<=t2.

方案二:先拿b再拿a.需满足:t+d2<=t2且t+d2+d1<=t1.

现在我们再假设,方案一不成立。

也就是说 t+d1>t1 或者 t+d1+d2>t2.

很明显,如果我们方案一不能选的话,方案二一定不成立。如果说t+d2+d1<=t1满足,那么t+d2+d1<=t1也满足。

举例:

当这两门课是课程a(2,3) , b(5,100)(第一个元素是持续时间,第二个是截至日期),当前时间t=0。这个时候我们应该先上课程a,再去看看课程b能不能上。

小结:如果两门课无论按什么样的顺序上都不会超出相应的截止日期,那自然皆大欢喜。否者,我们只能优先考虑截止日期小的。

贪心反悔--优先队列(大根堆):

那么如果我们现在遇到了课程b,当前时间t+d2>t2,也就是说,就目前答案集合已经容不下课程b了,我们直接放弃吗?如果这个时候课程b的持续上课时间非常短呢?也就是说,此时课程b的截止日期比现在答案集合里面的都大,而且持续时间很小(小于答案集合里面某些元素),对于整个答案集合来说,其实课程b比目前答案集合里面的一些元素要“优秀”!

举例:我们已经拿了课程a,durationi =10,课程b durationi =7,现在有一个课程c durationi=8,

如果求最优解,我们应该把课程a换掉,换成课程c,因为这么做答案不会变小,但是花费时间变小了,ta+tb

我们可以反悔。怎么个反悔法?也就是说,我们要退掉目前答案集合里面最“不好”的一个元素?

何谓不好呢?也就是持续时间最长的。这样一来,放进去的答案截止日期大,持续时间还短(相对于那个最“不好的元素”),我们的答案才会越来越多(贪心).而且换掉一个元素后,我们还要重新排序,找到新的答案集合里面最不好的一个课程,用来下一次遇到更好的课程交换的筹码。

这让我不由得感叹,只有更加优秀,才能不被淘汰啊!

这里我们就要用到优先队列了。

什么叫优先队列?

定义:优先级队列 是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。

通俗的说,优先队列就是一种数据结构,用来存放集合,在取出元素的时候,会直接取出最大或者最小的元素。在放入元素的时候,该队列会在内部再次排序,让队头元素永远是最大或者最小,当然,最大或者最小看你自己需求,默认是大根堆,也就是队头永远是最大的元素。

这个结构可以完美的实现以上贪心反悔的过程。

由此,这个题目也就结束了。

再缕缕思路:

为什么最终存放在队列里的答案集合一定是最优的呢??

首先对于每一个入队的元素来说,加入它可以让答案变得更好。

对于每一个出队的元素来说,是因为有一个更好的元素代替了它,并且其它元素都比他要好。

所以最终遍历完整个课表数组,每一个元素都有入队和出队的选择,每种选择都会让答案更好,所以,最后的答案一定是最优的。

时间复杂度分析:

正所谓,抛开时间复杂度谈算法,就是在耍流氓。

时间复杂度:排序O(nlogn)+优先队列操作最多的时间O(nlogn)(单次操作的时间是logn,每个元素最多入队一次,那么就是nlogn),所以总的时间复杂度是O(nlogn).

空间复杂度:O(n)O(n)O(n),即为优先队列需要使用的空间。

代码:

  int scheduleCourse(vector>& courses) {
        for(auto &it:courses){
            swap(it[0],it[1]);//这里我交换了一下[durationi, lastDayi]的位置,因为sort
        }//排序默认是以第一个元素从小到大排序,当然,重新写个比较剂也一样。
         sort(courses.begin(),courses.end());
         priority_queue q;//默认是大根堆
         int time=0;//当前时间
         int ans=0;//答案
         for(auto it:courses){//遍历排好序的课程表
             int x=it[0];//截止日期
             int y=it[1];//持续时间
             if(time+y<=x){//目前时间能放下该课程
                 time+=y;//更新时间
                 ans++;
                 q.push(y);//入队
             }else{
                 if(!q.empty()&&q.top()>y){//队列不为空,目前遍历到的课程比较优秀
                     time=time-q.top()+y;//消除影响
                     q.pop();
                     q.push(y);
                 }
             }
         }
         return ans;
    }

知识扩展:

关于优先队列的基本用法大家可以去查查资料(博主有点累了).

这里再讲一下小根堆(每次队列弹出最小值)的用法:

如果要用到小根堆,我们可以利用默认优先队列是大根堆的特性,在元素入队的时候加一个负号,这样一来,大根堆就变成小根堆了,取出元素的时候再加一个负号消除影响就好了。

当然,如果有时间也有兴趣最好还是去看看正规小根堆的表示,我这里只是提供了一种比较取巧的方法,节约时间嘛haha。

最后,别忘了给博主点赞哦!

你可能感兴趣的:(力扣刷题,leetcode,算法,职场和发展,贪心算法,数据结构)