0Pricing
DSA Interview Prep · Aula

Escalonador de tarefas e posto de combustível

Aplique o raciocínio guloso ao problema do período de resfriamento do escalonador de tarefas da CPU e ao problema de viabilidade de postos de combustível em um circuito.

Escalonador de tarefas e posto de combustível é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

Problema do Agendador de Tarefas

Agendador de Tarefas (LeetCode 621): dada uma lista de tarefas da CPU (cada uma identificada por A-Z) e um período de espera n, encontre o número mínimo de intervalos da CPU necessários para concluir todas as tarefas. A mesma tarefa deve esperar pelo menos n intervalos antes de ser executada novamente. Intervalos ociosos são permitidos. Para as tarefas ['A','A','A','B','B','B'] com período de espera 2, a resposta é 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órmula Gulosa para o Agendador de Tarefas

A ideia principal é que o tempo total é determinado pela tarefa mais frequente. Se a tarefa mais frequente aparecer f vezes, com contagem max_count (número de tarefas com frequência f), o tempo será max(len(tasks), (f-1) * (n+1) + max_count). A fórmula consiste em criar f-1 blocos de tamanho n+1, preenchê-los com outras tarefas e adicionar o último ciclo. Se as outras tarefas preencherem todos os intervalos ociosos (muitas tarefas diferentes), basta executar todas as tarefas sem tempo ocioso.

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

Por que a Fórmula Funciona

Visualize o cronograma como uma grade com n+1 colunas (um espaço para tarefa + n espaços de espera). A tarefa mais frequente A (frequência f) precisa de f linhas. Entre a primeira e a última ocorrência, há f-1 blocos completos de n+1 espaços. Além disso, há o último bloco parcial, que contém todas as tarefas com frequência máxima. Se houver tarefas diferentes suficientes, elas preencherão todos os espaços ociosos, e a quantidade real de tarefas excederá o tempo dos blocos — escolha o maior dos dois.

# 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)}')

Alternativa de Simulação com Monte

Uma simulação baseada em um monte fornece o cronograma real (não apenas a contagem). A cada etapa, escolha a tarefa disponível mais frequente (monte máximo). Após executá-la, aplique o período de espera: não a reinsira até n etapas depois. Use uma fila para acompanhar as tarefas em espera. Isso é executado em O(tempo total × log k), em que k é o número de tarefas distintas. Embora esteja correta, a fórmula é mais rápida. Conheça ambas — os entrevistadores podem pedir o próprio cronograma.

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

Problema do Posto de Combustível

Posto de Combustível (LeetCode 134): há n postos de combustível dispostos em círculo. O posto i tem gas[i] de gasolina, e o custo para viajar até o posto seguinte é cost[i]. Começando com o tanque vazio, encontre o posto inicial a partir do qual é possível completar o circuito. Se não existir um posto assim, retorne -1. O problema garante que, se existir uma resposta válida, haverá no máximo uma.

# 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

Solução Gulosa para o Posto de Combustível

Algoritmo guloso: (1) Se a gasolina total < custo total, não existe solução (retorne -1). (2) Caso contrário, existe exatamente uma solução. Encontre-a com uma única passagem: acompanhe tank (combustível atual) e start (posto inicial candidato). Se tank < 0 depois de visitar um posto, o start atual não consegue chegar a esse posto — redefina tank = 0 e defina start = i + 1. O start final é a resposta.

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

Por que o Início Guloso está Correto

Argumento de correção: se o tanque ficar negativo depois de chegar ao posto i partindo de start, nenhum posto entre start e i (inclusive) poderá ser um ponto de partida válido — todos terão menos combustível ao chegar ao posto i do que haveria partindo de start. Portanto, podemos ignorar todos esses postos com segurança e tentar i+1. Como existe uma solução (a gasolina total ≥ o custo total), o candidato final start deve funcionar.

# 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

Força Bruta versus Abordagem Gulosa para o Posto de Combustível

A força bruta testa cada posto inicial e simula o circuito completo — tempo O(n²). A solução gulosa de uma única passagem usa tempo O(n) e espaço O(1). Para um vetor com 10⁵ postos, a diferença é de 10¹⁰ operações contra 10⁵. A principal propriedade matemática que permite a abordagem gulosa é: se o combustível líquido total for não negativo, existe um início válido, que será sempre o posto imediatamente depois do último ponto em que a soma acumulada ficou negativa.

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))

Relacionado: Custo Mínimo para Completar Viagens

Tempo Mínimo para Completar Viagens (LeetCode 2187) é um problema de busca binária no espaço de respostas. Você faz uma busca binária sobre o valor de tempo T: dado o tempo T, os ônibus com time[i] completam floor(T/time[i]) viagens. Se o total de viagens ≥ totalTrips, T é suficiente. Encontre o menor T que satisfaça essa condição. Isso mostra que a estratégia gulosa pode ser aplicada no metanível (fazendo uma busca binária sobre as respostas) quando não existe uma regra gulosa direta no nível dos objetos.

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

Casos Extremos e Verificação

Casos extremos importantes para ambos os problemas: Agendador de Tarefas — quando o intervalo de espera n=0, a resposta é simplesmente o número de tarefas (não é necessário nenhum período ocioso). Quando todas as tarefas são iguais (por exemplo, todas são 'A'), os intervalos ociosos são preenchidos exatamente. Quando as tarefas têm muitos tipos distintos, pode haver 0 intervalos ociosos (as tarefas preenchem todos os quadros). Posto de Gasolina — quando a quantidade total de combustível é exatamente igual ao custo total, existe exatamente um início válido. Quando um único posto tem combustível suficiente para todo o circuito, esse posto é a resposta. Verifique sempre sua resposta obtida pelo algoritmo guloso nesses casos degenerados.

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

Reconhecimento de Padrões Gulosos

Tanto o Agendador de Tarefas quanto o Posto de Gasolina seguem o padrão guloso: (1) identifique o gargalo (a tarefa mais frequente / o saldo líquido de combustível); (2) tome uma decisão em uma única passagem usando uma variável acumulada (max_freq, tank); (3) reinicie ou redefina quando uma restrição for violada. Problemas gulosos comuns que você deve conhecer: Seleção de Atividades, Codificação de Huffman, Mochila Fracionária, Jogo de Saltos, Agendador de Tarefas, Posto de Gasolina e Mesclagem de Intervalos. Cada um tem uma prova por argumento de troca ou por um invariante matemático.

# 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

Verificação Rápida

Teste sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da Lição

Nesta lição, você aprendeu: resposta do Agendador de Tarefas = max(total_tasks, (max_freq-1)*(n+1)+max_count) — derivada do preenchimento de grades baseadas em quadros com a tarefa mais frequente, o Posto de Gasolina usa uma única passagem, redefinindo start=i+1 sempre que tank fica negativo, e é válido quando o combustível total ≥ o custo total e ambos os problemas usam tempo O(n) e espaço O(1), identificando um invariante matemático em vez de fazer uma busca exaustiva. A seguir, estudaremos o modelo de Divisão e Conquista e suas aplicações além da ordenação por intercalação.

Perguntas Frequentes

A aula “Escalonador de tarefas e posto de combustível” é grátis?

Sim — o texto completo de “Escalonador de tarefas e posto de combustível” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Escalonador de tarefas e posto de combustível”?

Aplique o raciocínio guloso ao problema do período de resfriamento do escalonador de tarefas da CPU e ao problema de viabilidade de postos de combustível em um circuito. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Escalonador de tarefas e posto de combustível”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Algoritmos gulosos vs DP: quando usar cada um
  2. Escalonamento e fusão de intervalos
  3. Jogo dos saltos I e II
  4. Escalonador de tarefas e posto de combustível
← Voltar para DSA Interview Prep