任务调度器 图解题解
这道题到底在问什么
- 输入
- tasks=["A","A","A","B","B","B"], n=2
- 输出
- 8 (A B idle A B idle A B)
- 输入
- tasks=[A×6,B×6,C×4,D×2,E×2], n=2
- 输出
- 20
最优解:为什么这么做
一句话答案:LeetCode 621 任务调度器有 O(N) 的公式解:设最高频任务出现 maxCount 次、并列最高频的有 maxTasks 个,最短总时长 = max((maxCount - 1) × (n + 1) + maxTasks, 任务总数)。前者是最高频任务撑起的骨架,后者兜底不需要空转的情况;空间 O(1)。
这道题的约束到底卡在哪
任务不限执行顺序、每个占一个时间单位,唯一的约束是同种任务两次执行之间至少隔 n 个单位;轮到没任务可干时只能插 idle 待命,而 idle 同样计入总时长。所以题目本质是排布问题:怎么安排才能让被迫空转的格子最少。直觉上,出现次数最多的任务是麻烦制造者——它和自己的冷却冲突次数最多,别的任务反而好办。
为什么最高频任务决定了骨架
盯住出现次数最多的任务,设它出现 maxCount 次。相邻两次之间至少隔 n 格,连它自己占的那一格算上,一个最小重复周期就是 n + 1 格——这也回答了「周期为什么是 n + 1 而不是 n」。前 maxCount - 1 次执行各自撑起一个完整周期,共 (maxCount - 1) × (n + 1) 格;最后一次执行完不必再等冷却,此时有几个任务并列最高频,末尾就各占一格,再加 maxTasks。这两截拼起来就是骨架长度,其余低频任务只负责往周期的空隙里填。
为什么还要和任务总数取较大值
骨架公式隐含一个假设:周期里的空隙填不满,需要 idle 补位。但任务种类足够多时,空隙全被占满之后还有剩余任务——把它们插进各个周期也不违反间隔约束,因为周期被撑得更宽,同种任务的间距只会更充裕。这种情况下一格 idle 都不需要,总时长就等于任务总数本身,骨架公式反而算少了。所以最终答案是 max(骨架, len(tasks)),漏掉这一步兜底是本题最常见的错。
贪心每轮挑剩余最多的为什么对
若用模拟来验证:每一轮的 n + 1 格里,从剩余次数最多的任务开始挑着放,放不满就 idle。优先消耗高频任务,是为了防止它们攒到最后挤成一团、只能靠大量空转硬隔开——先把最难安置的摊开,容易的自然见缝插针。用最大堆模拟这个贪心过程,答案与公式一致,公式正是对该过程的数学闭式。以示例 A、B 各 6 次,C 4 次,D、E 各 2 次,n 取 2 为例:骨架 (6 - 1) × 3 + 2 得 17,任务总数 20,取大为 20。
复杂度与容易翻车的细节
统计频次只需扫一遍任务表,时间 O(N);任务种类至多 26 个字母,计数数组是常数空间 O(1)。三个细节最容易错:并列最高频的任务必须全部计入 maxTasks,只算一个会把末轮算短;周期长度是 n + 1 不是 n;忘了与任务总数取 max。另外公式解只给最短时长、不输出具体排班,若面试官追问执行序列,再补最大堆逐轮模拟即可。
▶ 动画逐步走查(共 42 步)——想跟着动画一帧帧对照就展开
- 3本例最高频是 A、B 各 6 次(并列 2 个)。骨架 = (6-1)×3+2 = 17;任务总数 = 20;取大 = 20。下面用「最大堆」一格格把它排出来。
- 4第 1 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 6 次,放下后它要等至少 2 格才能再放。
- 5落子:第 1 周期第 1 格放 任务 A,时间走到 1。绿色撑起骨架。
- 6第 1 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 6 次,放下后它要等至少 2 格才能再放。
- 7落子:第 1 周期第 2 格放 任务 B,时间走到 2。绿色撑起骨架。
- 8第 1 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 C(它来填空隙):现在 C 还剩 4 次,放下后它要等至少 2 格才能再放。
- 9落子:第 1 周期第 3 格放 任务 C,时间走到 3。橘色填补空隙,不浪费时间。
- 10第 2 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 5 次,放下后它要等至少 2 格才能再放。
- 11落子:第 2 周期第 1 格放 任务 A,时间走到 4。绿色撑起骨架。
- 12第 2 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 5 次,放下后它要等至少 2 格才能再放。
- 13落子:第 2 周期第 2 格放 任务 B,时间走到 5。绿色撑起骨架。
- 14第 2 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 C(它来填空隙):现在 C 还剩 3 次,放下后它要等至少 2 格才能再放。
- 15落子:第 2 周期第 3 格放 任务 C,时间走到 6。橘色填补空隙,不浪费时间。
- 16第 3 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 4 次,放下后它要等至少 2 格才能再放。
- 17落子:第 3 周期第 1 格放 任务 A,时间走到 7。绿色撑起骨架。
- 18第 3 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 4 次,放下后它要等至少 2 格才能再放。
- 19落子:第 3 周期第 2 格放 任务 B,时间走到 8。绿色撑起骨架。
- 20第 3 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 C(它来填空隙):现在 C 还剩 2 次,放下后它要等至少 2 格才能再放。
- 21落子:第 3 周期第 3 格放 任务 C,时间走到 9。橘色填补空隙,不浪费时间。
- 22第 4 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 3 次,放下后它要等至少 2 格才能再放。
- 23落子:第 4 周期第 1 格放 任务 A,时间走到 10。绿色撑起骨架。
- 24第 4 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 3 次,放下后它要等至少 2 格才能再放。
- 25落子:第 4 周期第 2 格放 任务 B,时间走到 11。绿色撑起骨架。
- 26第 4 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 D(它来填空隙):现在 D 还剩 2 次,放下后它要等至少 2 格才能再放。
- 27落子:第 4 周期第 3 格放 任务 D,时间走到 12。橘色填补空隙,不浪费时间。
- 28第 5 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 2 次,放下后它要等至少 2 格才能再放。
- 29落子:第 5 周期第 1 格放 任务 A,时间走到 13。绿色撑起骨架。
- 30第 5 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 2 次,放下后它要等至少 2 格才能再放。
- 31落子:第 5 周期第 2 格放 任务 B,时间走到 14。绿色撑起骨架。
- 32第 5 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 E(它来填空隙):现在 E 还剩 2 次,放下后它要等至少 2 格才能再放。
- 33落子:第 5 周期第 3 格放 任务 E,时间走到 15。橘色填补空隙,不浪费时间。
- 34第 6 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 A(它是最高频,撑骨架):现在 A 还剩 1 次,放下后它要等至少 2 格才能再放。
- 35落子:第 6 周期第 1 格放 任务 A,时间走到 16。绿色撑起骨架。
- 36第 6 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 B(它是最高频,撑骨架):现在 B 还剩 1 次,放下后它要等至少 2 格才能再放。
- 37落子:第 6 周期第 2 格放 任务 B,时间走到 17。绿色撑起骨架。
- 38第 6 周期 · 第 3 格:本轮挑剩余次数最多的任务,轮到放 C(它来填空隙):现在 C 还剩 1 次,放下后它要等至少 2 格才能再放。
- 39落子:第 6 周期第 3 格放 任务 C,时间走到 18。橘色填补空隙,不浪费时间。
- 40第 7 周期 · 第 1 格:本轮挑剩余次数最多的任务,轮到放 D(它来填空隙):现在 D 还剩 1 次,放下后它要等至少 2 格才能再放。
- 41落子:第 7 周期第 1 格放 任务 D,时间走到 19。橘色填补空隙,不浪费时间。
- 42第 7 周期 · 第 2 格:本轮挑剩余次数最多的任务,轮到放 E(它来填空隙):现在 E 还剩 1 次,放下后它要等至少 2 格才能再放。
- 43落子:第 7 周期第 2 格放 任务 E,时间走到 20。橘色填补空隙,不浪费时间。
- 44全部排完!一共用了 20 个时间单位 = 答案 20。绿色(最高频)撑骨架、橘色填空隙、灰色是不得不待命的 idle。
⚠️ 容易写错的地方
✗ 错:忘了和「任务总数」取大
✓ 对:max(骨架, len(tasks))
任务种类很多时空隙被填满、根本不需要 idle,骨架公式会算少,必须兜底成总数
✗ 错:只数最高频的一个任务
✓ 对:要数所有并列最高频的个数 maxTasks
末轮里每个并列最高频任务都各占一格,少数会算短
✗ 错:周期长度写成 n
✓ 对:周期长度是 n+1
同一任务两次之间隔 n 格,连同它自己这一格正好是 n+1 一个周期
完整代码(Python / C++ / Java)
Python
from collections import Counter
def leastInterval(tasks, n):
cnt = Counter(tasks)
maxCount = max(cnt.values())
# 有多少个任务并列最高频
maxTasks = sum(1 for v in cnt.values() if v == maxCount)
# 骨架长度 与 任务总数 取较大者
return max((maxCount - 1) * (n + 1) + maxTasks, len(tasks))C++
class Solution {
public:
int leastInterval(vector<char>& tasks, int n) {
int cnt[26] = {0};
for (char c : tasks) cnt[c - 'A']++;
int maxCount = *max_element(cnt, cnt + 26);
int maxTasks = count(cnt, cnt + 26, maxCount);
int skeleton = (maxCount - 1) * (n + 1) + maxTasks;
return max(skeleton, (int)tasks.size());
}
};Java
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] cnt = new int[26];
for (char c : tasks) cnt[c - 'A']++;
int maxCount = 0;
for (int v : cnt) maxCount = Math.max(maxCount, v);
int maxTasks = 0;
for (int v : cnt) if (v == maxCount) maxTasks++;
int skeleton = (maxCount - 1) * (n + 1) + maxTasks;
return Math.max(skeleton, tasks.length);
}
}复杂度
时间
O(N)
N=任务总数,统计频次一遍;26 个字母的扫描是常数
空间
O(1)
只用大小 26 的计数数组,与输入规模无关
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 任务调度器 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么是 (n+1) 一个周期,而不是 n?+
同一个任务两次执行之间要隔 n 个「别的」时间单位,加上它自己占的这一格,一个最小重复周期就是 n+1。最高频任务每个周期开头放一次,正好满足间隔。
什么时候答案等于任务总数、根本不用 idle?+
当任务种类足够多、把每个周期的空隙都填满时。比如有大量不同任务,空格全被别的任务占了,没有空闲需要待命,总时间就等于任务个数。所以要 max(骨架, 总数) 兜底。
最大堆模拟和公式法是什么关系?+
两者答案一致。最大堆是「每轮取剩余最多的若干任务、不够补 idle」的贪心过程,能直观看到骨架怎么撑起来;公式是对这个过程的数学闭式,O(N) 直接出结果。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 任务调度器 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。