Task Scheduler i Gas Station
Stosować rozumowanie zachłanne w problemie okresu chłodzenia harmonogramu zadań procesora oraz w problemie możliwości przejechania całej trasy między stacjami benzynowymi
Task Scheduler i Gas Station to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Problem Task Scheduler
Task Scheduler (LeetCode 621): dana jest lista zadań procesora (każde oznaczone literą od A do Z) oraz okres oczekiwania n. Należy znaleźć minimalną liczbę przedziałów czasowych procesora potrzebnych do wykonania wszystkich zadań. To samo zadanie musi odczekać co najmniej n przedziałów, zanim będzie można wykonać je ponownie. Dopuszczalne są bezczynne przedziały. Dla zadań ['A','A','A','B','B','B'] i okresu oczekiwania 2 odpowiedź wynosi 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')Wzór zachłanny dla Task Scheduler
Kluczowa obserwacja: całkowity czas wyznacza zadanie występujące najczęściej. Jeśli najczęstsze zadanie występuje f razy, a liczba zadań o częstotliwości f wynosi max_count, czas wykonania wynosi max(len(tasks), (f-1) * (n+1) + max_count). Wzór polega na utworzeniu f-1 bloków o rozmiarze n+1, wypełnieniu ich innymi zadaniami i dodaniu ostatniego cyklu. Jeśli inne zadania wypełnią wszystkie bezczynne miejsca (czyli występuje wiele różnych zadań), wystarczy wykonać wszystkie zadania bez okresów bezczynności.
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)) # 10Dlaczego ten wzór działa
Wyobraźmy sobie harmonogram jako siatkę z n+1 kolumnami (jedno miejsce na zadanie i n miejsc na okres oczekiwania). Najczęstsze zadanie A, występujące f razy, wymaga f wierszy. Między pierwszym a ostatnim wystąpieniem znajduje się f-1 pełnych bloków po n+1 miejsc. Do tego dochodzi ostatni niepełny blok zawierający wszystkie zadania o maksymalnej częstotliwości. Jeśli występuje wystarczająco dużo różnych zadań, wypełniają one wszystkie bezczynne miejsca, a rzeczywista liczba zadań przekracza czas wynikający z układu bloków — należy wybrać większą z tych dwóch wartości.
# 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)}')Alternatywa: symulacja z użyciem kopca
Symulacja oparta na kopcu pozwala uzyskać rzeczywisty harmonogram, a nie tylko jego długość. W każdym kroku wybieramy dostępne zadanie występujące najczęściej (kopiec maksymalny). Po jego wykonaniu stosujemy okres oczekiwania: nie dodajemy zadania ponownie, dopóki nie upłynie n kroków. Do śledzenia zadań w okresie oczekiwania używamy kolejki. Złożoność wynosi O(total_time × log k), gdzie k oznacza liczbę różnych zadań. To rozwiązanie jest poprawne, ale wzór jest szybszy. Warto znać oba podejścia — podczas rozmowy kwalifikacyjnej może pojawić się pytanie o sam harmonogram.
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)) # 8Problem Gas Station
Gas Station (LeetCode 134): jest n stacji benzynowych ułożonych na okręgu. Na stacji i znajduje się gas[i] paliwa, a przejazd do następnej stacji kosztuje cost[i] paliwa. Zaczynając z pustym bakiem, należy znaleźć stację początkową, z której można pokonać cały okrąg. Jeśli taka stacja nie istnieje, należy zwrócić -1. W zadaniu zagwarantowano, że jeśli istnieje prawidłowa odpowiedź, to jest co najwyżej jedna.
# 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 existsRozwiązanie zachłanne dla Gas Station
Algorytm zachłanny: (1) Jeśli całkowita ilość paliwa jest mniejsza niż całkowity koszt, rozwiązanie nie istnieje (należy zwrócić -1). (2) W przeciwnym razie istnieje dokładnie jedno rozwiązanie. Należy znaleźć je w jednym przejściu, śledząc tank (bieżącą ilość paliwa) oraz start (kandydatkę na stację początkową). Jeśli po odwiedzeniu stacji tank < 0, bieżąca wartość start nie pozwala dotrzeć do tej stacji — ustawiamy tank = 0 i start = i + 1. Końcowa wartość start jest odpowiedzią.
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)) # -1Dlaczego zachłanny wybór stacji początkowej jest poprawny
Argument poprawności: jeśli po dotarciu do stacji i ze stacji start zapas paliwa staje się ujemny, żadna stacja między start a i (włącznie) nie może być prawidłową stacją początkową — przy dotarciu do stacji i z każdej z nich byłoby mniej paliwa niż w przypadku rozpoczęcia ze stacji start. Można więc bezpiecznie pominąć wszystkie te stacje i spróbować od i+1. Ponieważ rozwiązanie istnieje (całkowita ilość paliwa ≥ całkowity koszt), końcowa kandydatka start musi być prawidłowa.
# 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)) # TrueBrute force a podejście zachłanne dla Gas Station
Brute force próbuje każdej stacji początkowej i symuluje cały okrąg — złożoność czasowa wynosi O(n²). Zachłanne rozwiązanie wykonujące jedno przejście działa w czasie O(n) i zajmuje O(1) pamięci. Dla tablicy zawierającej 10⁵ stacji oznacza to różnicę między 10¹⁰ a 10⁵ operacji. Kluczową własnością matematyczną umożliwiającą zastosowanie podejścia zachłannego jest to, że jeśli całkowity bilans paliwa jest nieujemny, istnieje prawidłowa stacja początkowa i zawsze jest nią stacja znajdująca się bezpośrednio za ostatnim punktem, w którym suma bieżąca stała się ujemna.
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))Powiązane: Minimum Cost to Complete Trips
Minimum Time to Complete Trips (LeetCode 2187) to problem wyszukiwania binarnego w przestrzeni odpowiedzi. Wykonujemy wyszukiwanie binarne wartości czasu T: w czasie T autobusy o wartościach time[i] wykonują floor(T/time[i]) przejazdów. Jeśli łączna liczba przejazdów ≥ totalTrips, T jest wystarczające. Należy znaleźć najmniejszą taką wartość T. Pokazuje to, że zachłanność można stosować na poziomie meta (wyszukiwać binarnie odpowiedzi), gdy na poziomie obiektów nie istnieje bezpośrednia reguła zachłanna.
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)) # 2Przypadki brzegowe i weryfikacja
Ważne przypadki brzegowe dla obu problemów: Task Scheduler — gdy cooldown n=0, odpowiedź to po prostu len(tasks) (nie są potrzebne żadne okresy bezczynności). Gdy wszystkie zadania są takie same (np. wszystkie to „A”), okresy bezczynności wypełniają ramki dokładnie. Gdy zadań jest wiele różnych typów, liczba okresów bezczynności może wynosić 0 (zadania wypełniają wszystkie ramki). Gas Station — gdy łączna ilość paliwa jest dokładnie równa łącznemu kosztowi, istnieje dokładnie jeden prawidłowy punkt startowy. Gdy pojedyncza stacja ma wystarczającą ilość paliwa na całe okrążenie, to właśnie ona jest odpowiedzią. Zawsze należy zweryfikować rozwiązanie zachłanne na takich zdegenerowanych przypadkach.
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])) # 4Rozpoznawanie wzorców zachłannych
Zarówno Task Scheduler, jak i Gas Station stosują wzorzec zachłanny: (1) identyfikujemy wąskie gardło (najczęściej występujące zadanie / bilans netto paliwa). (2) Podejmujemy decyzję w jednym przejściu, używając zmiennej przechowującej bieżący stan (max_freq, tank). (3) Rozpoczynamy ponownie lub resetujemy stan, gdy ograniczenie zostanie naruszone. Typowe problemy zachłanne, które warto znać, to: Activity Selection, Huffman Coding, Fractional Knapsack, Jump Game, Task Scheduler, Gas Station i Merge Intervals. Każdy z nich można uzasadnić argumentem wymiany albo niezmiennikiem matematycznym.
# 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 -1Szybki test
Proszę sprawdzić swoje zrozumienie zagadnień z kursu Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.
Podsumowanie lekcji
W tej lekcji poznali Państwo: odpowiedź dla Task Scheduler = max(total_tasks, (max_freq-1)*(n+1)+max_count) — wzór wynika z wypełniania siatek opartych na ramkach za pomocą najczęściej występującego zadania, Gas Station korzysta z jednego przejścia, ustawiając start=i+1 za każdym razem, gdy tank staje się ujemny; rozwiązanie jest prawidłowe, gdy łączna ilość paliwa ≥ łączny koszt, a także oba problemy działają w czasie O(n) i używają O(1) pamięci, ponieważ identyfikują niezmiennik matematyczny zamiast wykonywać pełne przeszukiwanie. Następnie zajmiemy się schematem Divide and Conquer i jego zastosowaniami wykraczającymi poza sortowanie przez scalanie.
Często zadawane pytania
Czy lekcja „Task Scheduler i Gas Station” jest bezpłatna?
Tak — pełny tekst „Task Scheduler i Gas Station” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Co nauczysz się w „Task Scheduler i Gas Station”?
Stosować rozumowanie zachłanne w problemie okresu chłodzenia harmonogramu zadań procesora oraz w problemie możliwości przejechania całej trasy między stacjami benzynowymi Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?
Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Task Scheduler i Gas Station”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?
Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście
- Harmonogramowanie i scalanie przedziałów
- Jump Game I i II
- Task Scheduler i Gas Station