Планировщик задач и заправочная станция
Применяйте жадное рассуждение к задаче о периоде охлаждения планировщика задач CPU и к задаче о возможности проехать по круговому маршруту заправочных станций
«Планировщик задач и заправочная станция» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения 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, заполните их другими задачами и выполните операцию add для последнего цикла. Если другие задачи полностью заполняют все интервалы простоя, то есть задач достаточно много и они разнообразны, просто выполните все задачи без простоя.
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Почему жадный выбор начальной станции корректен
Аргумент корректности: если при достижении станции i из start запас топлива становится отрицательным, ни одна станция между 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. Это показывает, что жадный подход применяется на метауровне (при двоичном поиске по ответам), когда на уровне объектов не существует прямого жадного правила.
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»), интервалы простоя заполняются полностью. Если существует много разных типов задач, интервалы простоя могут отсутствовать: задачи заполняют все временные рамки. газовая станция — если общее количество топлива в точности равно общей стоимости, существует ровно одна допустимая начальная станция. Если на одной станции достаточно топлива для прохождения всего маршрута, ответом будет эта станция. Всегда проверяйте жадный ответ на таких вырожденных случаях.
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Распознавание жадного шаблона
И планировщик задач, и газовая станция следуют жадному шаблону: (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Быстрая проверка
Проверьте своё понимание концепций «Структуры данных и алгоритмы — подготовка к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке Вы узнали: ответ планировщика задач = максимум(общее_число_задач, (макс_частота-1)*(n+1)+макс_число) — формула выводится заполнением таблиц временных рамок наиболее часто встречающейся задачей, для газовой станции используется один проход: начальная станция сбрасывается в i+1 всякий раз, когда запас топлива становится отрицательным; решение существует, если общее количество топлива ≥ общей стоимости, а также обе задачи используют время O(n) и память O(1), поскольку вместо полного перебора выявляется математический инвариант. Далее мы изучим шаблон «разделяй и властвуй» и его применения, выходящие за рамки сортировки слиянием.
Часто задаваемые вопросы
Урок «Планировщик задач и заправочная станция» бесплатный?
Да — полный текст урока «Планировщик задач и заправочная станция» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Планировщик задач и заправочная станция»?
Применяйте жадное рассуждение к задаче о периоде охлаждения планировщика задач CPU и к задаче о возможности проехать по круговому маршруту заправочных станций Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Планировщик задач и заправочная станция»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Жадные алгоритмы и DP: когда что использовать
- Планирование и объединение интервалов
- Игра с прыжками I и II
- Планировщик задач и заправочная станция