题目大意:提供k个任务,这些任务没有依赖关系(即可以任意调度)。CPU完成一个任务需要耗时一个时间片段,当执行完一个任务后,相同的任务必须在n个时间片段才能得以执行。请问CPU通过调度最快能在多少时间片段内完成所有任务。
有难度的一道题目。我借鉴了https://discuss.leetcode.com/topic/92852/concise-java-solution-o-n-time-o-26-space的思路。下面按照我个人理解进行说明:
首先我们将任务进行分组,相同的任务分入一组。对于T类任务,我们记N(T)为任务T所在分组包含的任务数,读作T的提交数。这里记m为所有类型任务中最大提交数,s为提交数为m的任务类型的总数目。
最终结果r显然不可能小于任务总数k,并且也肯定不会小于(m-1)*(n+1)+s。这里稍微说明下这个公式的含义。首先我们取任意一个提交数为最大提交数的任务类型T,令其在第一个时间片段中执行第一次,那么显然最后一次必定是在(m-1)*(n+1)时间片段之后执行的,当其在(m-1)*(n+1)+1时间片段执行了最后一次,那么T类型任务占用的时间片必定为1,n+2,2n+3,...,(m-1)*(n+1)+1,其是紧密排列的。而对应的要完成所有提交数为m的s个任务类型,我们最少也需要(m-1)*(n+1)+s个时间片段。
因此如果我们能找到一个有效的调度方式使得CPU能在max(k, (m-1)*(n+1)+s)的时间片段内完成所有任务,那么这个调度方式必定是最优的(因为不可能存在更快的调度方式)。接下来说明如何利用这些时间片段完成所有任务。
接下来描述一个构造性的算法:首先我们对将被执行的任务分隔为m个分块,用序号0,...,m-1来标志。每个任务块的前s个任务均为提交数为m的任务。对于其余未被调度的任务,我们将其按照任务种类进行排序(保证同类任务连续被调度),之后遍历所有剩余未被调度的任务,对于第i个任务,我们将其分配在第i%(m-1)的任务分块中,并追加到该任务分块中的后续第一个时间片段中。接着对于所有包含少于n+1个任务的任务块,我们在尾部追加空事件(IDLE),直到其长度刚好达到n+1。最后将这些分块按编号顺序连接起来,得到的就是整个调度序列。
我们先说明这个调度算法得到的调度序列是有效的,即每个同种类任务的执行的时间片段都最少相隔n。显然对于提交数为m的任务,是满足这个约束的,接下来只需要考虑所有提交数少于m的任务。假设两个F类任务违反了约束,其必定是被调度到了同一任务块中,假设二者在排序序列中的下标为i和j,由i%(m-1)=j%(m-1)可知i-j=z(m-1),而i不等于j,故|i-j|>=m-1,即F种类的任务提交数最少有m个,这与前提相悖,故所有提交数少于m的任务都是不冲突的。
再说明调度算法所导出的结果的长度为max(k, (m-1)*(n+1)+s)。考虑两种情况,一种是任务块长度不足需要我们补充IDLE,这时候的总共花费的时间片段为(m-1)*(n+1)+s。另外一种就是任务块长度足够甚至超出,此时由于没有添加IDLE,整个导出的序列都是由有效任务组成的,此时导出序列的长度为k。这两种情况下导出序列的上界均为max(k, (m-1)*(n+1)+s),而前面也已经说明过了max(k, (m-1)*(n+1)+s)是最终结果的下界,故最终结果必然为max(k, (m-1)*(n+1)+s)。
由于我们并真正对任务进行调度,而只是求max(k, (m-1)*(n+1)+s),其中求s和m的时间复杂度为O(n),故总的时间复杂度为O(n),而要保存每个任务类型的提交数,需要O(26)的空间复杂度。
最后提供代码:
1 class Solution { 2 public int leastInterval(char[] tasks, int n) { 3 int[] counter = new int[26]; 4 int taskNum = tasks.length; 5 for(int i = 0; i < taskNum; i++) 6 { 7 counter[tasks[i] - 'A']++; 8 } 9 int max = 0; 10 int same = 0; 11 for(int i = 0; i < 26; i++) 12 { 13 if(counter[i] > max) 14 { 15 max = counter[i]; 16 same = 1; 17 } 18 else if(counter[i] == max) 19 { 20 same++; 21 } 22 } 23 return Math.max((max - 1) * (n + 1) + same, taskNum); 24 } 25 }