LeetCode 第 621 题:任务调度器(Task Scheduler)
字数 2177 2025-10-26 03:46:54

好的,我们这次来详细讲解 LeetCode 第 621 题:任务调度器(Task Scheduler)
虽然题目名称在已讲列表里出现过,但可能之前的讲解不够系统,我们这次把它拆解得非常细致,确保你能透彻掌握。


1. 题目描述

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

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

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

示例 1:

输入:tasks = ["A","A","A","B","B","B"], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
      (索引 0 1 2    3    4 5    6    7)

示例 2:

输入:tasks = ["A","A","A","B","B","B"], n = 0
输出:6
解释:这种情况下冷却时间为 0,直接按顺序做 A A A B B B 即可。

示例 3:

输入:tasks = ["A","A","A","A","A","B","B","B","B","B","C","C","C","C","C","D","D","D"], n = 1
输出:18

约束条件:

  • 任务数量范围 [1, 10^4]
  • tasks[i] 是大写英文字母
  • n 的范围 [0, 100]

2. 问题理解与抽象

我们先理解一下规则:

  • 相同任务之间必须间隔至少 n 个单位时间。
  • 在这 n 个间隔时间里,可以执行其他不同的任务,或者 CPU 待命。
  • 目标是总时间最短。

直觉:出现次数最多的任务会制约整个调度,因为它的执行间隔必须满足冷却要求。


3. 思路推导(循序渐进)

3.1 特殊情况

如果 n = 0,没有冷却时间,总时间就是任务总数 len(tasks)

3.2 一般情况分析

假设任务种类有 k 种,统计每个任务出现的次数,找到出现次数最多的任务,设其次数为 maxFreq

第一步:安排出现次数最多的任务

我们把出现次数最多的任务称为 A,它需要执行 maxFreq 次。
执行第 1 个 A 后,必须等 n 个单位时间才能执行下一个 A。
因此,两个 A 之间至少有 n 个其他任务/待命。

那么,最后一个 A 执行完后,后面不需要再等冷却。
所以 A 本身占用的时间片是:

  • 第一次执行:时间点 0
  • 第二次执行:时间点 0 + (n+1)
  • 第三次执行:时间点 0 + 2*(n+1)
  • ...
  • maxFreq 次执行:时间点 0 + (maxFreq-1)*(n+1)

所以,至少需要的时间是:(maxFreq - 1) * (n + 1) + 1
(最后一个任务执行完就结束,所以最后一个时间段只占 1 个单位)

第二步:安排其他任务

在 A 的冷却间隔中,可以插入其他任务。
如果还有任务次数和 A 一样多(比如 B 也出现 maxFreq 次),那么最后一个 B 必须紧接在最后一个 A 后面(或并行安排),这样会多占用一个时间单位。

因此,如果有 countMax 个任务出现次数都是 maxFreq,那么最终公式修正为:

min_time = (maxFreq - 1) * (n + 1) + countMax

其中 countMax 是出现次数等于 maxFreq 的任务种类数。

第三步:考虑任务很多,冷却间隔被填满的情况

有可能任务种类非常多,多到冷却间隔可以被完全填满,并且还有任务剩下,这时候总时间实际上就是任务总数,因为不需要额外待命时间。

所以最终答案是:

max( len(tasks), (maxFreq - 1) * (n + 1) + countMax )

4. 举例验证

示例 1

tasks = ["A","A","A","B","B","B"], n = 2

  • 统计频率:A 出现 3 次,B 出现 3 次,maxFreq = 3, countMax = 2
  • 计算:(3-1)(2+1) + 2 = 23 + 2 = 8
  • 任务总数 6,max(6,8) = 8 ✅

示例 2

n = 0

  • 公式:(3-1)(0+1) + 2 = 21 + 2 = 4
  • 但任务总数 6,max(6,4) = 6 ✅

示例 3

tasks = [A×5, B×5, C×5, D×3], n=1

  • maxFreq = 5, countMax = 3(A,B,C 都是 5 次)
  • 公式:(5-1)(1+1) + 3 = 42 + 3 = 11
  • 但任务总数 5+5+5+3=18,max(18,11)=18 ✅

5. 算法步骤

  1. 统计每个任务的频率。
  2. 找到最大频率 maxFreq
  3. 统计有多少个任务频率等于 maxFreq,记为 countMax
  4. 计算 result = max( len(tasks), (maxFreq - 1) * (n + 1) + countMax )
  5. 返回 result

6. 代码实现(Python)

def leastInterval(tasks, n):
    from collections import Counter
    
    freq = list(Counter(tasks).values())
    maxFreq = max(freq)
    countMax = freq.count(maxFreq)
    
    return max(len(tasks), (maxFreq - 1) * (n + 1) + countMax)

7. 复杂度分析

  • 时间复杂度:O(N),N 是任务数量,统计频率和找最大值都是 O(N)。
  • 空间复杂度:O(1),因为任务是大写字母,最多 26 种,freq 列表长度最多 26。

8. 为什么这个公式有效?

核心思想是桶思想

  • 想象把执行时间分成多个桶,每个桶容量为 n+1(一个任务 + n 个冷却位)。
  • 桶的数量由最高频任务决定:有 maxFreq 个任务,就形成 maxFreq 个桶?不,其实是 maxFreq - 1 个完整桶(每个桶 n+1 时间),加上最后一个桶只装最高频任务(长度 countMax)。
  • 如果任务数很多,桶装不下,说明不需要待命,总时间就是任务数。

这样讲解是否清晰?有没有哪一步需要我再展开说明?

好的,我们这次来详细讲解 LeetCode 第 621 题:任务调度器(Task Scheduler) 。 虽然题目名称在已讲列表里出现过,但可能之前的讲解不够系统,我们这次把它拆解得非常细致,确保你能透彻掌握。 1. 题目描述 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表,其中每个字母表示一种不同种类的任务。 任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。 在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。 然而,两个相同种类的任务之间必须有长度为整数 n 的冷却时间, 因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 你需要计算完成所有任务所需要的 最短时间 。 示例 1: 示例 2: 示例 3: 约束条件: 任务数量范围 [1, 10^4] tasks[i] 是大写英文字母 n 的范围 [0, 100] 2. 问题理解与抽象 我们先理解一下规则: 相同任务之间必须间隔至少 n 个单位时间。 在这 n 个间隔时间里,可以执行其他不同的任务,或者 CPU 待命。 目标是总时间最短。 直觉 :出现次数最多的任务会制约整个调度,因为它的执行间隔必须满足冷却要求。 3. 思路推导(循序渐进) 3.1 特殊情况 如果 n = 0 ,没有冷却时间,总时间就是任务总数 len(tasks) 。 3.2 一般情况分析 假设任务种类有 k 种,统计每个任务出现的次数,找到出现次数最多的任务,设其次数为 maxFreq 。 第一步:安排出现次数最多的任务 我们把出现次数最多的任务称为 A,它需要执行 maxFreq 次。 执行第 1 个 A 后,必须等 n 个单位时间才能执行下一个 A。 因此,两个 A 之间至少有 n 个其他任务/待命。 那么,最后一个 A 执行完后,后面不需要再等冷却。 所以 A 本身占用的时间片是: 第一次执行:时间点 0 第二次执行:时间点 0 + (n+1) 第三次执行:时间点 0 + 2* (n+1) ... 第 maxFreq 次执行:时间点 0 + (maxFreq-1)* (n+1) 所以, 至少 需要的时间是: (maxFreq - 1) * (n + 1) + 1 (最后一个任务执行完就结束,所以最后一个时间段只占 1 个单位) 第二步:安排其他任务 在 A 的冷却间隔中,可以插入其他任务。 如果还有任务次数和 A 一样多(比如 B 也出现 maxFreq 次),那么最后一个 B 必须紧接在最后一个 A 后面(或并行安排),这样会多占用一个时间单位。 因此,如果有 countMax 个任务出现次数都是 maxFreq ,那么最终公式修正为: 其中 countMax 是出现次数等于 maxFreq 的任务种类数。 第三步:考虑任务很多,冷却间隔被填满的情况 有可能任务种类非常多,多到冷却间隔可以被完全填满,并且还有任务剩下,这时候总时间实际上就是任务总数,因为不需要额外待命时间。 所以最终答案是: 4. 举例验证 示例 1 tasks = [ "A","A","A","B","B","B" ], n = 2 统计频率:A 出现 3 次,B 出现 3 次,maxFreq = 3, countMax = 2 计算:(3-1) (2+1) + 2 = 2 3 + 2 = 8 任务总数 6,max(6,8) = 8 ✅ 示例 2 n = 0 公式:(3-1) (0+1) + 2 = 2 1 + 2 = 4 但任务总数 6,max(6,4) = 6 ✅ 示例 3 tasks = [ A×5, B×5, C×5, D×3 ], n=1 maxFreq = 5, countMax = 3(A,B,C 都是 5 次) 公式:(5-1) (1+1) + 3 = 4 2 + 3 = 11 但任务总数 5+5+5+3=18,max(18,11)=18 ✅ 5. 算法步骤 统计每个任务的频率。 找到最大频率 maxFreq 。 统计有多少个任务频率等于 maxFreq ,记为 countMax 。 计算 result = max( len(tasks), (maxFreq - 1) * (n + 1) + countMax ) 。 返回 result 。 6. 代码实现(Python) 7. 复杂度分析 时间复杂度:O(N),N 是任务数量,统计频率和找最大值都是 O(N)。 空间复杂度:O(1),因为任务是大写字母,最多 26 种,freq 列表长度最多 26。 8. 为什么这个公式有效? 核心思想是 桶思想 : 想象把执行时间分成多个桶,每个桶容量为 n+1(一个任务 + n 个冷却位)。 桶的数量由最高频任务决定:有 maxFreq 个任务,就形成 maxFreq 个桶?不,其实是 maxFreq - 1 个完整桶(每个桶 n+1 时间),加上最后一个桶只装最高频任务(长度 countMax)。 如果任务数很多,桶装不下,说明不需要待命,总时间就是任务数。 这样讲解是否清晰?有没有哪一步需要我再展开说明?