题目重点:Uber Senior MLE VO 的任务依赖题:每个任务耗时一单位,可同时执行多个任务。用拓扑排序与最长依赖链求最短完工时间,并检测环。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 任务编号为 0..n−1;边 (u,v) 表示 u 完成后 v 才能启动。不同任务可无限并行,且每任务恰好耗时 1 单位。
- 所有任务都必须完成;n=0 返回 0。重复依赖边按同一关系去重,避免入度与释放次数不一致。
- 若依赖图有环,任务无法全部完成,代码报错;题目未规定错误返回格式。
解题路线
- 建依赖邻接表和每个任务入度;所有入度为 0 的任务在时间 0 同时可开始,完成时刻为 1。
- Kahn 拓扑遍历每条 u→v 时,令 finish[v]=max(finish[v],finish[u]+1);当 v 所有依赖释放后,其 finish 即最早完成时刻。
- 最终答案是 max finish,即 DAG 最长依赖路径的节点数;无限并行时任何调度都不能短于这条关键路径,而 ASAP 并行执行达到该下界。
手动走一遍
四个任务,依赖 0→2、1→2、2→3。0 与 1 可在第 0–1 单位同时做,2 在第 1–2 单位做,3 在第 2–3 单位做;虽然共有四个任务,最短完工时间为 3。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
"""Minimum finish time of unit-duration DAG tasks with unlimited workers."""
from collections import deque
def minimum_makespan(task_count: int, dependencies: list[tuple[int, int]]) -> int:
if task_count < 0:
raise ValueError("task count must be nonnegative")
graph = [set() for _ in range(task_count)]
indegree = [0] * task_count
for before, after in dependencies:
if not (0 <= before < task_count and 0 <= after < task_count):
raise ValueError("invalid task ID")
if after not in graph[before]:
graph[before].add(after)
indegree[after] += 1
ready = deque(i for i, degree in enumerate(indegree) if degree == 0)
finish = [1] * task_count
processed = 0
while ready:
task = ready.popleft()
processed += 1
for next_task in graph[task]:
finish[next_task] = max(finish[next_task], finish[task] + 1)
indegree[next_task] -= 1
if indegree[next_task] == 0:
ready.append(next_task)
if processed != task_count:
raise ValueError("dependency graph has a cycle")
return max(finish, default=0)
复杂度
设 n 个任务、m 条不同依赖。建图和拓扑 DP 为 O(n+m) 时间、O(n+m) 空间;若用集合去重,哈希操作期望 O(1)。
易错点与追问
容易失分的边界
- 把全部任务数当答案会忽略并行;只算拓扑层数但漏掉某节点多个父依赖的最大完成时间也会错。
- 有环时 ready 队列会提前耗尽;必须检查实际处理任务数是否等于 n。
- 本题无限并行,与 q21 Metric Scheduler 高层任务必须串行的资源限制不同,不能直接套同一完工时间公式。
继续深挖
- 若每任务耗时 d[v] 不同,转移改为 finish[v]=max(finish[u])+d[v],仍是 DAG 最长加权路径。
- 若机器数量有限,关键路径只是下界,任务调度会涉及资源竞争;需要明确优化目标和机器约束。
