Task Scheduler e Gas Station
Applichi il ragionamento greedy al problema del periodo di raffreddamento del CPU task scheduler e al problema di fattibilità circolare delle stazioni di servizio.
Task Scheduler e Gas Station è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.
Problema di Task Scheduler
Task Scheduler (LeetCode 621): dato un elenco di attività della CPU, ciascuna identificata da una lettera da A a Z, e un periodo di cooldown n, trovare il numero minimo di intervalli della CPU necessari per completare tutte le attività. La stessa attività deve attendere almeno n intervalli prima di essere eseguita di nuovo. Sono consentiti intervalli di inattività. Per le attività ['A','A','A','B','B','B'] con cooldown pari a 2, la risposta è 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')Formula greedy per Task Scheduler
L'intuizione chiave è che il tempo totale è determinato dall'attività con frequenza maggiore. Se l'attività più frequente compare f volte e ci sono max_count attività con frequenza f, il tempo è max(len(tasks), (f-1) * (n+1) + max_count). La formula consiste nel creare f-1 blocchi di dimensione n+1, riempirli con le altre attività e aggiungere l'ultimo ciclo. Se le altre attività riempiono tutti gli intervalli di inattività, perché sono numerose e diversificate, è sufficiente eseguire tutte le attività senza intervalli vuoti.
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)) # 10Perché la formula funziona
Si visualizzi la pianificazione come una griglia con n+1 colonne (uno slot per l'attività più n slot di cooldown). L'attività A con frequenza maggiore, pari a f, necessita di f righe. Tra la prima e l'ultima occorrenza ci sono f-1 blocchi completi di n+1 slot. Si aggiunge poi l'ultimo blocco parziale, che contiene tutte le attività con frequenza massima. Se ci sono abbastanza attività diversificate, queste riempiono tutti gli slot di inattività e il numero effettivo di attività supera il tempo calcolato dai blocchi: si sceglie il maggiore tra i due.
# 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 con simulazione basata su heap
Una simulazione basata su heap restituisce la pianificazione effettiva, non solo il conteggio. A ogni passaggio, si sceglie l'attività disponibile con frequenza maggiore (max-heap). Dopo l'esecuzione, si applica il cooldown: l'attività non deve essere reinserita prima di n passaggi. Si utilizza una coda per tenere traccia delle attività in cooldown. Questa soluzione richiede O(total_time × log k), dove k è il numero di attività distinte. Sebbene sia corretta, la formula è più veloce. È opportuno conoscere entrambe: durante un colloquio potrebbe essere richiesto di restituire la pianificazione stessa.
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)) # 8Problema della stazione di servizio
Gas Station (LeetCode 134): ci sono n stazioni di servizio disposte in cerchio. La stazione i contiene gas[i] unità di carburante e il viaggio verso la stazione successiva costa cost[i]. Partendo con il serbatoio vuoto, trovare la stazione da cui iniziare per completare il circuito. Se non esiste una soluzione, restituire -1. Il problema garantisce che, se esiste una soluzione, essa è al massimo una.
# 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 existsSoluzione greedy per Gas Station
Algoritmo greedy: (1) se il carburante totale è < del costo totale, non esiste alcuna soluzione (restituire -1). (2) Altrimenti, esiste esattamente una soluzione. La si trova con un'unica scansione: si tengono traccia di tank (il carburante corrente) e start (la stazione candidata di partenza). Se tank < 0 dopo aver visitato una stazione, la start corrente non può raggiungere quella stazione: si reimposta tank = 0 e si imposta start = i + 1. Il valore finale di start è la risposta.
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)) # -1Perché la partenza greedy è corretta
Argomento di correttezza: se il serbatoio diventa negativo dopo aver raggiunto la stazione i partendo da start, nessuna stazione compresa tra start e i, estremi inclusi, può essere un punto di partenza valido: raggiungerebbe la stazione i con meno carburante rispetto a quello disponibile partendo da start. Quindi è sicuro saltarle tutte e provare i+1. Poiché esiste una soluzione (il carburante totale è ≥ al costo totale), il candidato finale start deve funzionare.
# 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)) # TrueForza bruta e greedy a confronto per Gas Station
La forza bruta prova ogni stazione di partenza e simula l'intero circuito, con complessità temporale O(n²). La soluzione greedy a passaggio singolo richiede tempo O(n) e spazio O(1). Per un array di 10⁵ stazioni, la differenza è tra 10¹⁰ e 10⁵ operazioni. La proprietà matematica che rende possibile la strategia greedy è la seguente: se il carburante netto totale è non negativo, esiste una partenza valida, che corrisponde sempre alla stazione immediatamente successiva all'ultimo punto in cui la somma progressiva è diventata 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))Correlato: Minimum Cost to Complete Trips
Minimum Time to Complete Trips (LeetCode 2187) è un problema di ricerca binaria nello spazio delle risposte. Si esegue la ricerca binaria sul valore del tempo T: dato un tempo T, gli autobus con time[i] completano floor(T/time[i]) viaggi. Se il numero totale di viaggi è ≥ totalTrips, T è sufficiente. Si trova il valore minimo di T che soddisfa questa condizione. Questo dimostra che l'approccio greedy può essere applicato a livello meta (cercando binariamente tra le risposte) quando non esiste una regola greedy diretta a livello degli oggetti.
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)) # 2Casi limite e verifica
Casi limite importanti per entrambi i problemi: Task Scheduler — quando il cooldown n=0, la risposta è semplicemente len(tasks) (non è necessaria alcuna inattività). Quando tutte le attività sono uguali (ad esempio, tutte 'A'), gli slot di inattività vengono riempiti esattamente. Quando le attività hanno molti tipi distinti, gli slot di inattività possono essere 0 (le attività riempiono tutti gli slot). Gas Station — quando il gas totale è esattamente uguale al costo totale, esiste esattamente un punto di partenza valido. Quando una singola stazione ha abbastanza gas per completare l'intero circuito, quella stazione è la risposta. Verifichi sempre la risposta greedy in questi casi degeneri.
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])) # 4Riconoscimento dei pattern greedy
Sia Task Scheduler sia Gas Station seguono il pattern greedy: (1) identificare il collo di bottiglia (l'attività più frequente / il bilancio netto del carburante). (2) Prendere una decisione in un unico passaggio usando una variabile aggiornata (max_freq, tank). (3) Ricominciare o reimpostare il calcolo quando viene violato un vincolo. Problemi greedy comuni da conoscere: Activity Selection, Huffman Coding, Fractional Knapsack, Jump Game, Task Scheduler, Gas Station, Merge Intervals. Ognuno ha una dimostrazione basata su un argomento di scambio o su un invariante matematico.
# 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 -1Verifica rapida
Verifichi la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: la risposta di Task Scheduler = max(total_tasks, (max_freq-1)*(n+1)+max_count) — derivata dal riempimento di griglie basate su slot con l'attività più frequente, Gas Station usa un unico passaggio e reimposta start=i+1 ogni volta che tank diventa negativo; la soluzione è valida quando il gas totale ≥ il costo totale e entrambi i problemi usano tempo O(n) e spazio O(1), individuando un invariante matematico invece di eseguire una ricerca esaustiva. Nella prossima lezione studieremo il modello Divide et impera e le sue applicazioni oltre il merge sort.
Domande Frequenti
La lezione «Task Scheduler e Gas Station» è gratuita?
Sì — il testo completo di «Task Scheduler e Gas Station» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Task Scheduler e Gas Station»?
Applichi il ragionamento greedy al problema del periodo di raffreddamento del CPU task scheduler e al problema di fattibilità circolare delle stazioni di servizio. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare DSA Interview Prep?
Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Task Scheduler e Gas Station»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?
Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Greedy e DP: quando usare ciascuno
- Pianificazione e fusione degli intervalli
- Jump Game I e II
- Task Scheduler e Gas Station