记录一道算法题
任务调度器
621. 任务调度器 - 力扣(LeetCode) (leetcode-cn.com)
要求:
1. 每个任务执行一个单位时间
2. 同一个任务相隔 n 个单位
3. 求最短时间
先举些例子模拟一下。摸索一下流程,下面会提供两个解法,一个是暴力求解,一个是运用了规律。
-
["A","A","A","B","B","B","C","C"], n = 2
如果要执行的话就是 A -> B -> C -> A -> B -> C -> A -> B, A和下一个A之间至少间隔两个任务。 -
如果是 ["A","A","A","B","B","B"], n = 2
执行顺序就是 A -> B -> 空 -> A -> B -> 空 -> A -> B -
如果是 ["A","A","A","B","B","B","C","C", "D", "E"], n = 2
执行顺序就是 A -> B -> C -> D -> A -> B -> C -> E -> A -> B
基本上是从次数高到次数低依次执行。
首先先算次数吧
function leastInterval(tasks, n) {
const countObj = tasks.reduce((total, curr) => {
if (total[curr]) {
total[curr]++
} else {
total[curr] = 1
}
}, {})
// 因为是优先用掉次数多的,所以排序一下方便后续操作
const arr = Object.values(countObj).sort((a, b) => b - a)
}
假设是 ["A","A","A","B","B","B","C","C"], n = 2, 这时已经获取到了各个种类数量的数组 [3, 3, 2], 接下来观察 n 和次数的关系。转化成立体图的话如下;
A B
A B C
A B C
我们会发现任务的周期是 n + 1, 这里就是3个一个周期。接下来可以写一个循环进行处理。
// arr = [3, 3, 2]
let count = 0
for(let i = 0; i < n + 1; n++) {
arr[i]--
count++
}
这样数组的前3个任务就完成了记录,但是还需要考虑像上面 最后是 A -> B, 只执行两个任务,所以这个循环还需要加上条件判断。需要有值才可以计数。(注意个细节,先计数再减)
// arr = [1, 1, 0]
let count = 0
for(let i = 0; i < n + 1; n++) {
if (arr[i]) {
count++
}
arr[i]--
}
当种类多起来的时候,我们需要让它顺序执行。[3, 2, 1, 1, 1], n = 2, 很明显需要用一个循环再包一层,那循环的终止条件是什么。
let count = 0
while(?) {
for(let i = 0; i < n + 1; n++) {
if (arr[i]) {
count++
}
arr[i]--
}
}
A B
A B
A A C
任务是有间隔的,在间隔是如果不执行其他任务的话,那就是要等待
所以至少是 最高次数的 A 执行完。任务调度才结束。所以当 A 的次数
是 0 的时候,就可以终止了。
同时因为是每次处理前3个,所以还需要在每个周期结束时进行重新排序。
因为是暴力解法,所以就不考虑性能了。
但是有一个小坑,[1, 0, 0] 时,我们会发现arr只计算了一次,实际上需要3次,
而且不能用arr[0], 因为当[1,1,0] 时只计算一次.
let count = 0
while(arr[0]) {
for(let i = 0; i < n + 1; n++) {
if (arr.filter(_ => _ > 0).length) {
count++
}
arr[i]--
}
arr = arr.sort((a, b) => b - a)
}
这样次数就收集好了。暴力解法完整的代码如下
function leastInterval(tasks, n) {
const obj = tasks.reduce((total, curr) => {
if (total[curr]) {
total[curr]++
} else {
total[curr] = 1
}
return total
}, {})
let arr = Object.values(obj).sort((a, b) => b - a)
// 获取最大次数
const max = arr[0]
// 后面的 n 都是用 n+1 所以这里重新赋值
n = n + 1
// 需要保证长度大于等于 n
// 手动填充等待任务
if (arr.length < n) {
while (arr.length !== n) {
arr.push(max - 1)
}
}
let count = 0
while (arr[0]) {
for (let i = 0; i < n; i++) {
if (arr.filter(_ => _ > 0).length) {
count++
}
arr[i]--
}
arr = arr.sort((a, b) => b - a)
}
return count
}
其实规律已经基本上出来了。
-
当有等待任务的时候,至少执行 (max - 1) * n 次,然后加上最高次数的个数
A B A B C A B C 可以分成 A B -----下面是必定的个数 A B C A B C -
当然也可能没有等待任务,我们会用其他任务进行填充
A A B A B C D E 可以变成 A E ----依然可以拆 A B D A B C -
有可能填充之后长度超出了 n + 1
A E F A B D H A B C G 这时候,其实是任务不需要等待, 相同任务之间可以隔着 n+1 或以上数量的任务 也就是刚好等于任务的总数。 tasks.length
所以是填充了之后分为 (max - 1) * n + maxCount 或者 任务总数(maxCount是指数量等于max的个数。)
前者会缺少一些填充后多余出来的任务,所以是取两者中的最大值作为结果返回。完整代码如下:
function leastInterval(tasks, n) {
const obj = tasks.reduce((total, curr) => {
if (total[curr]) {
total[curr]++
} else {
total[curr] = 1
}
return total
}, {})
const arr = Object.values(obj).sort((a, b) => b - a)
const max = arr[0]
n = n + 1
// 上面都是一样的, 下面和暴力解法比起来简洁了很多。
let maxCount = 0
for (; maxCount < n; maxCount++) {
if (arr[maxCount] !== max) {
break
}
}
return Math.max((max - 1) * n + maxCount, tasks.length)
}
结束