题目地址:
https://leetcode.com/problems/task-scheduler/
给定一系列任务,以大写字母表示,再给定一个冷却时间,每个任务需要花一个单位的时间完成,并且相同任务之间需要 n n n个单位的冷却时间。要求安排这些任务的执行顺序,使得花费的总时间最小。
直觉上来讲,这个总时间是由出现次数最多的任务决定的。我们可以这样以贪心的方式安排任务:
1、先将每个任务按出现次数由大到小排序,然后依次执行任务,并累加任务执行的时间;
2、如果这样一轮依次执行任务的总时间是小于
n
+
1
n+1
n+1的,并且任务还没全执行完,那么说明需要idle,累加一下idle的时间;如果这样一轮依次执行任务的总时间是大于等于
n
+
1
n+1
n+1的,那说明这一轮任务顺利执行了,不需要idle;
3、把执行完一轮的任务的出现次数减去
1
1
1,然后再重复上述
1
1
1和
2
2
2的操作。
首先证明一下上述算法得到总时间最少。数学归纳法,对任务数进行归纳。当任务数为 1 1 1的时候显然正确。假设当任务数小于 n n n的时候成立,当任务数等于 n n n的时候,先按照上面贪心的方法得到一个安排方案,接着去掉最后的任务,如果最后的任务之前不是idle,由于之前的 n − 1 n-1 n−1个任务的方案是按照上面的贪心法得到的,由归纳假设,那是能得到的耗时最少的方案,所以加上最后一个任务当然也是耗时最少的方案(如果不然,删掉最后一个任务就得到了更优解,与归纳假设矛盾),所以结论成立;如果最后的任务之前是idle,那就说明最后一个任务恰好是全局出现次数最多的任务,设为任务 X X X,由于有idle的限制,所花费的最少时间的方案也必须大于等于 X , . . . , X , . . . , X , . . . , X X,...,X,...,X,...,X X,...,X,...,X,...,X这样的方案的花费时间(否则由鸽笼原理,就会有俩 X X X在idle的时间里产生冲突),而贪心法得到的时间花费恰好就是这个最小值,所以结论也成立。综上,算法正确。
其次考虑如何实现。(除了以下方法之外,还有一种偏数学的办法,参考https://blog.csdn.net/qq_46105170/article/details/109113420)。
法1:堆。由于每次都要找出出现次数最高的任务,所以可以考虑用堆来做。先将每个任务出现了多少次统计一下,然后开一个大顶堆,出现次数多的任务优先出堆,每一轮出堆完成之后,看一下是否需要idle,需要idle的话再累加一下idle的时间。代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
public int leastInterval(char[] tasks, int n) {
if (n == 0) {
return tasks.length;
}
int[] count = new int[26];
for (int i = 0; i < tasks.length; i++) {
count[tasks[i] - 'A']++;
}
PriorityQueue<Character> maxHeap = new PriorityQueue<>((c1, c2) -> -Integer.compare(count[c1 - 'A'], count[c2 - 'A']));
for (int i = 0; i < count.length; i++) {
if (count[i] > 0) {
maxHeap.add((char) ('A' + i));
}
}
int res = 0;
while (!maxHeap.isEmpty()) {
// 记录一下哪些任务还有剩余,方便后面重新入堆
List<Character> list = new ArrayList<>();
int size = maxHeap.size();
for (int i = 0; i < Math.min(size, n + 1); i++) {
char top = maxHeap.poll();
// process掉一个任务
count[top - 'A']--;
// 如果该任务还有剩余,则记录之
if (count[top - 'A'] > 0) {
list.add(top);
}
// 累加process的时间
res++;
}
// 将还没process完的任务重新入堆
for (char ch : list) {
maxHeap.offer(ch);
}
// 如果后面还有任务需要process,并且上一轮出堆的任务需要冷却,则累加idle的时间
if (!maxHeap.isEmpty() && size < n + 1) {
res += n + 1 - size;
}
}
return res;
}
}
时间复杂度 O ( t ) O(t) O(t),其中 t t t指的是最终答案,空间 O ( 1 ) O(1) O(1)。
法2:直接计算idle时间。不妨设任务 A A A出现次数最多,那么由上面的贪心算法,任务安排方式应该是形如 A , . . . , A , . . . , A , . . . A,...,A,...,A,... A,...,A,...,A,...这样,我们先把 A A A后面idle的时间的”空位“数算出来,注意,最后一个 A A A的后面是不必idle的,所以空位数应该是 A A A的数量减去 1 1 1,再乘以 n n n。接着就开始按照贪心算法里的流程填数了。假设 B B B是出现次数第二多的任务(这里允许出现次数和 A A A一样多),想象把 B B B分配到 A A A的后面,如果 B B B出现次数少于 A A A,那么idle的空位就少了 B B B的次数这么多,否则的话,就少了 A A A次数减去 1 1 1这么多。把所有任务都插入进去之后,看一下还剩余的空位的个数,如果此时还剩余负数个空位,说明中间没有idle,置idle为 0 0 0,直接返回任务数;否则返回idle值加上任务数。代码如下:
import java.util.Arrays;
public class Solution {
public int leastInterval(char[] tasks, int n) {
int[] count = new int[26];
for (int i = 0; i < tasks.length; i++) {
count[tasks[i] - 'A']++;
}
// 按count从大到小排序
Arrays.sort(count);
reverse(count);
int idle = (count[0] - 1) * n;
for (int i = 1; i < count.length; i++) {
idle -= Math.min(count[0] - 1, count[i]);
}
idle = Math.max(0, idle);
return idle + tasks.length;
}
private void reverse(int[] nums) {
for (int i = 0, j = nums.length - 1; i < j; i++, j--) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
}
时间复杂度 O ( m ) O(m) O(m), m m m是任务个数。