贪心算法解决任务调度器问题

989 阅读3分钟

正题

621. 任务调度器

给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。

然而,两个 相同种类 的任务之间必须有长度为整数 ****n ****的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的 最短时间 。

示例 1:


输入:tasks = ["A","A","A","B","B","B"], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,
而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 

示例 2:

输入:tasks = ["A","A","A","B","B","B"], n = 0
输出:6
解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0
["A","A","A","B","B","B"]
["A","B","A","B","A","B"]
["B","B","B","A","A","A"]
...
诸如此类

示例 3:

输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
输出:16
解释:一种可能的解决方案是:
     A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A

解析:

先读懂题意,n代表冷却时间,表示相同任务必须间隔n个时间单位后才能再次进行,但是在这段时间里可以执行其他的任务,同样,每个相同的任务都必须间隔n个时间间隔。

贪心原则

假设我们只有一种任务A,有 m 个,冷却时间为 n,那么他所需要的时间是多少呢?

image.png

图示 m = 3,那么需要花费 (3 - 1) * (n + 1) + 1 个时间单位, 即 (m - 1) * (n + 1) + 1

如果我们再任务列表中添加一个任务 B 呢?

image.png

你可以发现它所用时间并没有改变。原因是B占用了原有的待命时间,所以总的时间不会改变。假设 A 是需要执行次数最多的一个,那么我们是不是可以认为,它所需要的最小时间为上面的公式:

(A执行次数 - 1) * (n + 1) + 1

理由很简单,你要执行完所有的A,这么排列已经是最低消费了。那么执行次数最多的最低消费,就是所有任务执行完消费最小的可能性了。真正的结果只会大于或等于这个次数。

所以贪心原则就是: 所需时间只和当前重复次数最高的任务有关 ,结果满足大于等于 (重复最高的任务的执行次数 - 1) * (n + 1) + 1

什么时候会比这个情况大?

我们尝试用动画演示一下:

1.gif

通过动画我们可以看出,当其他任务出现的时候如果和 A 出现频率是一样的,那么次数就会 + 1. A是我们找出的最大出现频率的任务,所以不用考虑比A出现频率大的任务的情况。

假设出现另一个任务C,也和A的次数一样呢?那么就 + 2,所以我们还需要找到出现频率等于最高频率的个数。

尝试实现

首先定义我们所需要的变量:

let obj = {} // key 任务 value 出现次数
let maxCount = 0 // 出现频率最高的次数
let equalCount = 0 // 和最高频相等的次数

求出出现频率最高的任务数量:

for(let index = 0 ; index < tasks.length ; index++) {
        obj[tasks[index]] = obj[tasks[index]] ? obj[tasks[index]] + 1 : 1
    }
    const keys = Object.keys(obj)
    maxCount = obj[keys[0]]
    for (let index = 1 ; index < keys.length ; index++) {
        const key = keys[index]
        if (maxCount < obj[key]) {
            maxCount = obj[key]
        }
    }

修改一下,顺便求出同最高频率出现的次数 equalCount:

for(let index = 0 ; index < tasks.length ; index++) {
        obj[tasks[index]] = obj[tasks[index]] ? obj[tasks[index]] + 1 : 1
    }
    const keys = Object.keys(obj)
    maxCount = obj[keys[0]]
    for (let index = 1 ; index < keys.length ; index++) {
        const key = keys[index]
        if (maxCount < obj[key]) {
            maxCount = obj[key]
            equalCount = 0
        } else if (maxCount === obj[key]) {
            equalCount++
        }
    }

最终 return 公式

return (maxCount - 1) * (n + 1) + 1  + equalCount

信心满满提交!

image.png

发现并没有通过,我们肯定是忽略了某个因素的存在。

查找原因

发现2个明显的问题:

  1. n = 0时,那么花费时间不就是 tasks 的长度吗?
  2. 怎么计算出来的花费时间要比 tasks 的长度要短???

按理说花费时间一定是比任务数量要长的,就算没有冷却时间,那至少也是数组长度个单位的花费时间!

所以当我们计算出的时间比理论最低时间还要少的时候,我们就取 tasks 长度最为最低花费时间。

return Math.max((maxCount - 1) * (n + 1) + 1 + equalCount, tasks.length)

image.png

最终露出了欣慰的笑容。

完整代码:

/**
 * @param {character[]} tasks
 * @param {number} n
 * @return {number}
 */


var leastInterval = function(tasks, n) {
    let obj = {} // key 任务 value 出现次数
    let maxCount = 0 // 出现频率最高的次数
    let equalCount = 0 // 和最高频相等的次数
    for(let index = 0 ; index < tasks.length ; index++) {
        obj[tasks[index]] = obj[tasks[index]] ? obj[tasks[index]] + 1 : 1
    }
    const keys = Object.keys(obj)
    maxCount = obj[keys[0]]
    for (let index = 1 ; index < keys.length ; index++) {
        const key = keys[index]
        if (maxCount < obj[key]) {
            maxCount = obj[key]
            equalCount = 0
        } else if (maxCount === obj[key]) {
            equalCount++
        }
    }
    return Math.max((maxCount - 1) * (n + 1) + 1  + equalCount, tasks.length)
};
// A -> . -> . -> A -> . -> . -> A