0Pricing
DSA Interview Prep · Урок

Планировщик задач и заправочная станция

Применяйте жадное рассуждение к задаче о периоде охлаждения планировщика задач CPU и к задаче о возможности проехать по круговому маршруту заправочных станций

«Планировщик задач и заправочная станция» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA 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) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Планировщик задач и заправочная станция»?

Применяйте жадное рассуждение к задаче о периоде охлаждения планировщика задач CPU и к задаче о возможности проехать по круговому маршруту заправочных станций Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Планировщик задач и заправочная станция»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Жадные алгоритмы и DP: когда что использовать
  2. Планирование и объединение интервалов
  3. Игра с прыжками I и II
  4. Планировщик задач и заправочная станция
← Назад к DSA Interview Prep