这里有 n
门不同的在线课程,按从 1
到 n
编号。给你一个数组 courses
,其中 courses[i] = [durationi, lastDayi]
表示第 i
门课将会 持续 上 durationi
天课,并且必须在不晚于 lastDayi
的时候完成。
你的学期从第 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 天完成它,这已经超出了关闭日期。
输入:courses = [[1,2]] 输出:1
输入: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, 如果求最优解,我们应该把课程a换掉,换成课程c,因为这么做答案不会变小,但是花费时间变小了,ta+tb 我们可以反悔。怎么个反悔法?也就是说,我们要退掉目前答案集合里面最“不好”的一个元素? 何谓不好呢?也就是持续时间最长的。这样一来,放进去的答案截止日期大,持续时间还短(相对于那个最“不好的元素”),我们的答案才会越来越多(贪心).而且换掉一个元素后,我们还要重新排序,找到新的答案集合里面最不好的一个课程,用来下一次遇到更好的课程交换的筹码。 这让我不由得感叹,只有更加优秀,才能不被淘汰啊! 这里我们就要用到优先队列了。 定义:优先级队列 是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。 通俗的说,优先队列就是一种数据结构,用来存放集合,在取出元素的时候,会直接取出最大或者最小的元素。在放入元素的时候,该队列会在内部再次排序,让队头元素永远是最大或者最小,当然,最大或者最小看你自己需求,默认是大根堆,也就是队头永远是最大的元素。 这个结构可以完美的实现以上贪心反悔的过程。 由此,这个题目也就结束了。 再缕缕思路: 为什么最终存放在队列里的答案集合一定是最优的呢?? 首先对于每一个入队的元素来说,加入它可以让答案变得更好。 对于每一个出队的元素来说,是因为有一个更好的元素代替了它,并且其它元素都比他要好。 所以最终遍历完整个课表数组,每一个元素都有入队和出队的选择,每种选择都会让答案更好,所以,最后的答案一定是最优的。 正所谓,抛开时间复杂度谈算法,就是在耍流氓。 时间复杂度:排序O(nlogn)+优先队列操作最多的时间O(nlogn)(单次操作的时间是logn,每个元素最多入队一次,那么就是nlogn),所以总的时间复杂度是O(nlogn). 空间复杂度:O(n)O(n)O(n),即为优先队列需要使用的空间。 关于优先队列的基本用法大家可以去查查资料(博主有点累了). 这里再讲一下小根堆(每次队列弹出最小值)的用法: 如果要用到小根堆,我们可以利用默认优先队列是大根堆的特性,在元素入队的时候加一个负号,这样一来,大根堆就变成小根堆了,取出元素的时候再加一个负号消除影响就好了。 当然,如果有时间也有兴趣最好还是去看看正规小根堆的表示,我这里只是提供了一种比较取巧的方法,节约时间嘛haha。 最后,别忘了给博主点赞哦!举例:
贪心反悔--优先队列(大根堆):
durationi
=10,课程b durationi
=7,现在有一个课程c durationi=8,
什么叫优先队列?
时间复杂度分析:
代码:
int scheduleCourse(vector
知识扩展: