题目描述:
这里有 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<t2,当前时间为t.
有两种方案(贪心思想,都拿):
方案一:先拿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<t2也满足。
举例:
当这两门课是课程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<tc+tb(跟上面的持续时间相对应).也就意味着 有多出来的时间去上其他课程(对于答案更加有利,就好像我吃一个更小的糖果,胃里面就会有更多的空间去装其他的糖果)。
我们可以反悔。怎么个反悔法?也就是说,我们要退掉目前答案集合里面最“不好”的一个元素?
何谓不好呢?也就是持续时间最长的。这样一来,放进去的答案截止日期大,持续时间还短(相对于那个最“不好的元素”),我们的答案才会越来越多(贪心).而且换掉一个元素后,我们还要重新排序,找到新的答案集合里面最不好的一个课程,用来下一次遇到更好的课程交换的筹码。
这让我不由得感叹,只有更加优秀,才能不被淘汰啊!
这里我们就要用到优先队列了。
什么叫优先队列?
定义:优先级队列 是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。
通俗的说,优先队列就是一种数据结构,用来存放集合,在取出元素的时候,会直接取出最大或者最小的元素。在放入元素的时候,该队列会在内部再次排序,让队头元素永远是最大或者最小,当然,最大或者最小看你自己需求,默认是大根堆,也就是队头永远是最大的元素。
这个结构可以完美的实现以上贪心反悔的过程。
由此,这个题目也就结束了。
再缕缕思路:
为什么最终存放在队列里的答案集合一定是最优的呢??
首先对于每一个入队的元素来说,加入它可以让答案变得更好。
对于每一个出队的元素来说,是因为有一个更好的元素代替了它,并且其它元素都比他要好。
所以最终遍历完整个课表数组,每一个元素都有入队和出队的选择,每种选择都会让答案更好,所以,最后的答案一定是最优的。
时间复杂度分析:
正所谓,抛开时间复杂度谈算法,就是在耍流氓。
时间复杂度:排序O(nlogn)+优先队列操作最多的时间O(nlogn)(单次操作的时间是logn,每个元素最多入队一次,那么就是nlogn),所以总的时间复杂度是O(nlogn).
空间复杂度:O(n)O(n)O(n),即为优先队列需要使用的空间。
代码:
int scheduleCourse(vector<vector<int>>& courses) { for(auto &it:courses){ swap(it[0],it[1]);//这里我交换了一下[durationi, lastDayi]的位置,因为sort }//排序默认是以第一个元素从小到大排序,当然,重新写个比较剂也一样。 sort(courses.begin(),courses.end()); priority_queue<int> 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。
最后,别忘了给博主点赞哦!