任务调度器与加油站
将贪心思路应用于 CPU 任务调度器的冷却时间问题和环形加油站的可行性问题。
任务调度器与加油站 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。
任务调度器问题
任务调度器(LeetCode 621):给定一个 CPU 任务列表(每个任务标记为 A-Z)和冷却时间 n,求完成所有任务所需的最少 CPU 时间片数。同一任务再次运行前,至少必须等待 n 个时间片。允许有空闲时间片。对于任务 ['A','A','A','B','B','B'],当冷却时间为 2 时,答案是 8:A→B→idle→A→B→idle→A→B。
# Task Scheduler example
tasks = ['A','A','A','B','B','B']
n = 2 # cooldown
# One optimal schedule: A B _ A B _ A B
# Intervals: 1 2 3 4 5 6 7 8 → answer = 8
# Another example: tasks=['A','A','A','B','B','C'] n=2
# A B C A B _ A → 7 intervals
print('Understanding the cooldown constraint')
print('Same task needs n intervals gap between runs')任务调度器的贪心公式
关键洞察是:总时间由出现次数最多的任务决定。如果出现次数最多的任务出现 f 次,且有 max_count 个任务的出现频率为 f,则总时间为 max(len(tasks), (f-1) * (n+1) + max_count)。公式的含义是:创建 f-1 个大小为 n+1 的框架,用其他任务填充它们,然后加上最后一个周期。如果其他任务填满了所有空闲位置(存在许多不同的任务),就直接执行所有任务,不需要空闲时间。
from collections import Counter
def least_interval(tasks, n):
count = Counter(tasks)
max_freq = max(count.values())
# How many tasks have the maximum frequency?
max_count = sum(1 for c in count.values() if c == max_freq)
# Formula: max of total tasks (no idle) or frame-based calculation
frame_time = (max_freq - 1) * (n + 1) + max_count
return max(len(tasks), frame_time)
print(least_interval(['A','A','A','B','B','B'], 2)) # 8
print(least_interval(['A','A','A','B','B','B'], 0)) # 6 (no cooldown)
print(least_interval(['A','A','A','A','B','C'], 3)) # 10为什么该公式有效
可以将调度安排想象成一个有 n+1 列的网格(一个任务位置加上 n 个冷却位置)。出现次数最多的任务 A(出现频率为 f)需要 f 行。在第一次和最后一次出现之间,有 f-1 个完整框架,每个框架包含 n+1 个位置。除此之外,还有包含所有最高频任务的最后一个不完整框架。如果有足够多种类的任务,它们会填满所有空闲位置,实际任务数量就会超过框架所需时间——此时取两者中较大的值。
# Visualise frame structure for AAABBB, n=2
# Frame size = n+1 = 3
# f = 3 (A appears 3 times), max_count = 2 (A and B both appear 3 times)
# Grid:
# [A B _] ← frame 1
# [A B _] ← frame 2
# [A B ] ← last partial frame (max_count=2 cells)
# Total = (3-1)*3 + 2 = 6 + 2 = 8
# If tasks = AAAABBCC, n=2: max_freq=4 (A), max_count=1
# (4-1)*(2+1)+1 = 9+1 = 10
# But len(tasks)=8 < 10, so answer is 10
tasks2 = ['A','A','A','A','B','B','C','C']
from collections import Counter
count = Counter(tasks2)
mf = max(count.values())
mc = sum(1 for c in count.values() if c == mf)
print(f'Frame formula: ({mf}-1)*{2+1}+{mc} = {(mf-1)*(2+1)+mc}')
print(f'Max(len={len(tasks2)}, frame={max(len(tasks2),(mf-1)*3+mc)}) = {max(len(tasks2),(mf-1)*3+mc)}')基于堆的模拟替代方案
基于堆的模拟可以给出实际的调度安排(而不仅仅是数量)。每一步都取出当前可用且出现次数最多的任务(最大堆)。执行后应用冷却规则:直到 n 步之后才重新放回该任务。使用队列跟踪冷却中的任务。该方法的时间复杂度为 O(total_time × log k),其中 k 是不同任务的数量。虽然这种方法是正确的,但公式更快。两种方法都要掌握——面试官可能会要求您给出具体的调度安排。
import heapq
from collections import deque, Counter
def task_scheduler_simulate(tasks, n):
count = Counter(tasks)
heap = [-c for c in count.values()] # max-heap using negation
heapq.heapify(heap)
time = 0
cooldown = deque() # (available_at, neg_count)
while heap or cooldown:
time += 1
if heap:
c = heapq.heappop(heap) + 1 # use one instance
if c < 0: # still has remaining tasks
cooldown.append((time + n, c))
if cooldown and cooldown[0][0] == time:
heapq.heappush(heap, cooldown.popleft()[1])
return time
print(task_scheduler_simulate(['A','A','A','B','B','B'], 2)) # 8加油站问题
加油站(LeetCode 134):有 n 个加油站组成一个环。加油站 i 有 gas[i] 单位的汽油,前往下一站的消耗为 cost[i]。从空油箱出发,找出可以完成环路的起始加油站。如果不存在这样的加油站,则返回 -1。如果存在有效答案,题目保证最多只有一个。
# Example:
gas = [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
# net gain per station: gas[i] - cost[i]
net = [g - c for g, c in zip(gas, cost)]
print('Net gain per station:', net) # [-2, -2, -2, 3, 3]
# Only possible start: station 3 (index 3)
# Tank: 0 +3=3 → 3-1=2 → 2+1=3-2=... let's verify
print('Sum of net:', sum(net)) # 1 > 0 means solution exists加油站的贪心解法
贪心算法:(1)如果总汽油量小于总消耗量,则不存在解(返回 -1)。(2)否则,恰好存在一个解。通过一次遍历找到它:跟踪 tank(当前燃油量)和 start(候选起始加油站)。如果到达某个加油站后 tank < 0,说明当前 start 无法到达该站——将 tank = 0 重置,并将 start = i + 1。最终的 start 即为答案。
def can_complete_circuit(gas, cost):
if sum(gas) < sum(cost):
return -1 # impossible
tank = 0
start = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0:
tank = 0
start = i + 1 # current start failed, try next
return start
gas = [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
print(can_complete_circuit(gas, cost)) # 3
gas2 = [2, 3, 4]
cost2 = [3, 4, 3]
print(can_complete_circuit(gas2, cost2)) # -1为什么贪心起点是正确的
正确性论证:如果从 start 出发到达加油站 i 后油量变为负数,那么从 start 到 i(包括两端)之间的任何加油站都不可能成为有效起点——它们从自身出发到达加油站 i 时所能提供的燃油,都少于从 start 出发时所能提供的燃油。因此,我们可以安全地跳过所有这些加油站,并尝试 i+1。由于解确实存在(总汽油量 ≥ 总消耗量),最终的候选起点 start 必然有效。
# Proof sketch: why start=i+1 is correct after tank<0 at station i
# If we start at station j (start <= j <= i), tank at j is tank_from_start(j)
# After stations start..j: tank_from_j starts at 0, but we've already used gas[start..j-1]
# Starting at j means: tank_at_i = sum(net[j..i]) = sum(net[start..i]) - sum(net[start..j-1])
# Since sum(net[start..i]) < 0 AND sum(net[start..j-1]) >= 0 (no reset before i),
# tank_at_i when starting at j is even more negative → j cannot work either
def verify_gas_solution(gas, cost, start):
tank = 0
n = len(gas)
for i in range(n):
idx = (start + i) % n
tank += gas[idx] - cost[idx]
if tank < 0: return False
return True
print(verify_gas_solution([1,2,3,4,5],[3,4,5,1,2], 3)) # True加油站:暴力法与贪心法
暴力法会尝试每个起始加油站并模拟完整环路——时间复杂度为 O(n²)。贪心的一次遍历解法的时间复杂度为 O(n),空间复杂度为 O(1)。对于包含 10⁵ 个加油站的数组,差异是 10¹⁰ 次操作与 10⁵ 次操作。使贪心方法可行的关键数学性质是:如果总净燃油量非负,就一定存在有效起点,而且它总是运行总和最后一次变为负数的位置之后的那个加油站。
def brute_force_gas(gas, cost):
n = len(gas)
for start in range(n):
tank = 0
valid = True
for i in range(n):
idx = (start + i) % n
tank += gas[idx] - cost[idx]
if tank < 0: valid = False; break
if valid: return start
return -1
def greedy_gas(gas, cost):
if sum(gas) < sum(cost): return -1
tank = start = 0
for i, (g, c) in enumerate(zip(gas, cost)):
tank += g - c
if tank < 0: tank = 0; start = i + 1
return start
gas = [1,2,3,4,5]; cost = [3,4,5,1,2]
print('Brute:', brute_force_gas(gas,cost), '== Greedy:', greedy_gas(gas,cost))相关:完成行程的最小成本
完成行程的最短时间(LeetCode 2187)是一个在答案空间上进行二分查找的问题。对时间值 T 进行二分查找:给定时间 T,使用 time[i] 的公交车可完成 floor(T/time[i]) 次行程。如果总行程数 ≥ totalTrips,则 T 足够。请找出满足条件的最小 T。这说明,当对象层面不存在直接的贪心规则时,贪心也可以应用在 the 元层面(对答案进行二分查找)。
def minimum_time(time, total_trips):
def can_complete(t):
return sum(t // bus for bus in time) >= total_trips
lo, hi = 1, min(time) * total_trips # upper bound
while lo < hi:
mid = (lo + hi) // 2
if can_complete(mid):
hi = mid
else:
lo = mid + 1
return lo
print(minimum_time([1, 2, 3], 5)) # 3 (3/1=3 + 3/2=1 + 3/3=1 = 5)
print(minimum_time([2], 1)) # 2边界情况与验证
两个问题的重要边界情况:任务调度器 — 当冷却时间 n=0 时,答案就是任务数量(无需空闲)。当所有任务都相同(例如全是 'A')时,空闲时隙会被恰好填满。当任务类型很多且彼此不同时,空闲时隙可能为 0(任务填满所有时间帧)。加油站 — 当总油量恰好等于总消耗时,恰好存在一个有效起点。当某个加油站有足够的油量完成整个环路时,该加油站就是答案。请始终在这些退化情况下验证您的贪心答案。
from collections import Counter
def least_interval(tasks, n):
if n == 0: return len(tasks) # no cooldown
cnt = Counter(tasks)
mf = max(cnt.values())
mc = sum(1 for c in cnt.values() if c == mf)
return max(len(tasks), (mf-1)*(n+1)+mc)
# Edge cases for task scheduler
print(least_interval(['A','A','A'], 2)) # 7: A _ _ A _ _ A
print(least_interval(['A','A','B','B'], 0)) # 4: no idle
print(least_interval(['A','B','C','D'], 3)) # 4: all diff, no idle needed
# Edge case for gas station
def gas_station(gas, cost):
if sum(gas) < sum(cost): return -1
tank = start = 0
for i,(g,c) in enumerate(zip(gas,cost)):
tank += g-c
if tank < 0: tank=0; start=i+1
return start
print(gas_station([5,1,2,3,4],[4,4,1,5,1])) # 4贪心模式识别
任务调度器和加油站都遵循 the 贪心模式:(1) 确定瓶颈(出现频率最高的任务/净燃油余额)。(2) 使用一个持续更新的变量(最大频率、油箱)进行单次遍历决策。(3) 当约束被违反时重新开始或重置。常见的贪心问题包括:活动选择、霍夫曼编码、分数背包、跳跃游戏、任务调度器、加油站、合并区间。每个问题都可以通过交换论证或数学不变量进行证明。
# Greedy pattern summary
# Task Scheduler:
# Bottleneck: max frequency task
# Formula: max(total_tasks, (max_freq-1)*(n+1)+max_count)
# O(n) time, O(1) space
# Gas Station:
# Bottleneck: running sum of (gas-cost) going negative
# Reset start when tank < 0, valid if total sum >= 0
# O(n) time, O(1) space
# Both avoid the need for DP by using a clever single-pass insight
from collections import Counter
def combined_demo(tasks, n, gas, cost):
ti = max(len(tasks), (max(Counter(tasks).values())-1)*(n+1) +
sum(1 for c in Counter(tasks).values() if c==max(Counter(tasks).values())))
tank = start = 0
gs = sum(g-c for g,c in zip(gas,cost)) >= 0
return ti, start if gs else -1快速检查
测试您对本课“数据结构与算法——编程面试准备”概念的理解。
课程回顾
在本课中,您学到了:任务调度器的答案 = max(任务总数, (最大频率-1)*(n+1)+最大计数) — 由使用出现频率最高的任务填充基于时间帧的网格推导而来,加油站使用单次遍历,每当油箱余额变为负数时就将起点重置为 i+1;当总油量 ≥ 总消耗时答案有效,以及两个问题都通过识别数学不变量而不是穷举搜索,使用 O(n) 时间和 O(1) 空间。接下来我们将学习分治法模板及其在归并排序之外的应用。
常见问题解答
「任务调度器与加油站」课时是免费的吗?
是的 — 「任务调度器与加油站」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。
「任务调度器与加油站」这节课中我会学到什么?
将贪心思路应用于 CPU 任务调度器的冷却时间问题和环形加油站的可行性问题。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Coding Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「任务调度器与加油站」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Coding Interview Prep 课中编写并运行代码吗?
能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 贪心算法与 DP:何时使用哪一种
- 区间调度与合并
- 跳跃游戏 I 与 II
- 任务调度器与加油站