【力扣时间】【630】【困难】课程表 III

冬泳怪鸽有句话说得好。
无论遇到什么样的困难,都不要怕,微笑着面对它,消除恐惧的最好办法就是面对恐惧,后面省略……奥利给!

1、康康题

题目在这
我和各位算法水平一般的朋友一样,看到困难题时都是一哆嗦的。
但题目越难,攻克后的成就感也越大。
就算这次败了,我们吸取他人的思路,下次或许就会成功。

总之干了!奥利给!

2、审题

今天的题目描述倒是很清晰,但奈何读完后是一脸蒙蔽的。
总之,还是先来分析下我们能从题目和示例中获取的信息:

  1. 课程从第1天开始,不再从0计数
  2. 每天仅能选修1门课,且选修后必须连续读完,这意味着你修读的课程开始结束时间不能重合,并且不能中途安插其他课程。
  3. courses 是无序的,且数量为1 <= courses.length <= 10^4
  4. 根据示例,会有无效的干扰数据(即课程持续时间比结课日期要长,就算从第一天开始修读也无法完成)
  5. 返回仅仅是可以选修的课程数量,不用在意具体哪些课程。

然后,就没有了……

从题目里仅得到了一些边界数据和特殊数据,仍然没有详细的思路产生。
总之还是硬着头皮上吧。

3、思路

于是我又拿起了小本本写写画画。
再次建议各位活用手边的工具,越是难的题目,小本本越能给予帮助。

我们应该能够很轻易的就发觉这是一个贪心问题要你说,人家题目标签上就写好了贪心
而且是反悔贪心,类似大部分背包问题。
但不同于固定容量的背包,课程的截止日期各不相同。

辣么回到贪心上来。
我们首先要明确以下信息:

  1. 如何确认课程的优先级
  2. 什么情况下要选择这门课程?
  3. 什么情况下又要择优而丢弃已选的课程?

顺着上述三点,让我们来慢慢展开思路。

3.1、如何确认课程的优先级

贪心算法的要点在于从每步得到的局部最优解扩大到全局的最优解。
于是优先级是算法中很重要的概念。

由于题解要求的仅仅是课程的数量,因而重点不在于特定的课程上。
我们应尽可能多的选修课程,故要注意的便是课程的时长截止时间

此时,我们不妨带入到自身生活中来考虑。
在现实中,我们选择有时限的任务来完成时,是不是往往会优先完成deadline最近的任务呢?
社畜的压力开始大起来了……

于是,我们可以很自然的推断出,我们在选取时,应优先选取最早截止的课程。

对原始数组,按课程的截止时间正序排列!

3.2、什么情况下要选择这门课程

由于我们应尽可能的多选择课程,于是,这个问题的真正题面应该是——什么情况才不能选择此课程

实际上答案在审题中就有列出:

  1. 课程无法同时选修。于是,如果某一时刻已经排上了其他课程后,无法再排入这个课程。
  2. 课程有截止时间,超过截止时间则无法选修。(由于选修后必须连续修完,所以可以看成开始时间必须在截止时间-课程长度之前
  3. 课程安排本身就无法选修。(详见题目中的示例3)

所以综上,我们的条件语句似乎已经形成。

3.3、什么情况下要择优而丢弃已选的课程

重中之重!
面临择优,我们又得回到了优先级问题上了

无论长课程,还是短课程,选修后的计数都是1。
于是,我们又可以很快得出取舍:在排除其他条件下,我们会尽可能地选取时长较短的课程。

再次带入现实。
假设你很多个拥有deadline的任务都面临到期,为了不被boss教训,你肯定得尽可能多的完成它们(假设任务没有重要性区分)。辣么,在优先选择deadline较近的任务后,你会不会再去先完成耗时短的任务呢?
社畜的血压开始高起来了……

于是,在择优问题上我们再次得出结论:

在可选取的情况下,优先选择耗时较短的课程。

4、开工!

public int scheduleCourse(int[][] courses) {
        //按照截止日期从小到大排序
        Arrays.sort(courses, Comparator.comparingInt(o -> o[1]));

        //优先级队列按照课程耗时倒排
        PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o2[0] - o1[0]);

        int sumT = 0;
        for (int[] course : courses) {
            //如果某课程根本无法完成,则直接排除
            if (course[1] - course[0] < 0) {
                continue;
            }

            if (sumT + course[0] <= course[1]) {
                queue.offer(course);
                sumT += course[0];
            } else {
                //队列头部为耗时最高的课程,判断此课程与当前课程的耗时
                int[] head = queue.peek();
                if (head[0] > course[0]) {
                    //如果头部的耗时高于当前课程耗时,则将该课程排除
                    queue.poll();
                    sumT -= head[0];

                    //加入当前课程
                    queue.offer(course);
                    sumT += course[0];
                }
            }
        }

        return queue.size();
    }

提交成功的第一版,依旧是原汁原味的思路。
尝试优化了一些后发现对耗时影响并不大,于是就放弃了。

5、解读

//按照截止日期从小到大排序
Arrays.sort(courses, Comparator.comparingInt(o -> o[1]));

在3.1中提到,优先选择快要截止的课程,于是我们对原始数组进行排序。

我初始化了一个优先级队列,队列的元素就是课程本身(int[])。

此处和官解以及部分大牛的解法不同,他们的优先级队列的元素仅为课程的时长(int)。
我想了下,觉得很对。不过由于数组是引用传递,对耗时和空间的影响都不大。

//优先级队列按照课程耗时倒排
PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o2[0] - o1[0]);

然后我们循环课程,并以sumT记录当前已选课程的总时长,同时也是下一课程的开始时间。(回头看看3.2)

这一块的思路来源于3.2,但作用实则不大,后续的条件语句足以过滤。

//如果某课程根本无法完成,则直接排除
 if (course[1] - course[0] < 0) {
     continue;
 }

同样如3.2所说,如果课程的情况满足,则尽可能选择该课程。
在选择后将课程放入优先级队列,可以以此来获得所选课程中耗时最长的课程。
同时更新课程的总时长。

if (sumT + course[0] <= course[1]) {
	queue.offer(course);
    sumT += course[0];
}

下面,则是我们的核心择优思路了。
但其实就如3.3所写,我们在当前课程无法选修的情况下,抛弃已选的时长最长的课程。

else {
    //队列头部为耗时最高的课程,判断此课程与当前课程的耗时
    int[] head = queue.peek();
    if (head[0] > course[0]) {
        //如果头部的耗时高于当前课程耗时,则将该课程排除
        queue.poll();
        sumT -= head[0];

        //加入当前课程
        queue.offer(course);
        sumT += course[0];
    }
}

由于在排序后的原始数组中,截止时间晚的课程会排列在后面。所以在遍历中,当前课程比已入队的课程的截止时间只长不短。于是如果队列中最长的修读时长比当前课程时长要长的话,不用担心去除该课程并选修当前课程后,会超过当前课程的截止时间。

于是,最后留在队列中的课程,就是我们选修的所有课程。

	return queue.size();

6、提交

在这里插入图片描述
坏消息是耗时排名略低。
更坏的消息时,我没有更好的思路来优化时长。

7、咀嚼

第一遍对原始数组的排序,我用的是java的api(快排),时间复杂度为O(logN)
之后,遍历原始数组,并将元素放入优先级队列,每次入队列时会有一次排序,时间复杂度也是O(logN),于是这一过程是O(N*logN).
所以,整体的时间复杂度为O(N*logN)

使用了一个优先级队列,空间复杂度为O(N)

鉴于内存消耗排名挺高,我猜测是有什么牛逼的空间换时间的思路。
或者,可以省略优先级队列的排序?

8、他人的智慧

很遗憾的是,我没有从官解和其他大牛的题解里看到时间排名很靠前的题解。
而我的解法也与大多数的解法相似。

但是官解从数学的角度上分析了一大串,把我看得一愣一愣的。
我当初咋就没有考虑这么多,果然是瞎猫碰上了死耗子嘛?

9、总结

第一次写困难题的博客。
也不知道能不能把我脑筋里的这点水倒清楚。
只希望日后自己看这内容时,能够还原出原本的思路吧。

今天的困难题还是挺有压迫的,好在顺着思路还是能够缕清。
我对于困难题的要求是15分钟内能有大致的思路今天超时了

如果你不幸在面试中遇到了困难题,并且完全没有接触过的话,别指望面试官会给你时间让你慢慢思索(面试过程的高压也很难让人放松头脑打开思路)。

所以还是那句话,注意时间!保证速度
怎么做到呢?
如果你没有天才的头脑,那就只有靠勤奋的手了。
多刷题吧!

共勉共勉!奥利给!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值