작업 스케줄러와 주유소
CPU 작업 스케줄러의 냉각 기간 문제와 순환 주유소의 가능성 문제에 그리디 사고를 적용합니다.
작업 스케줄러와 주유소은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
작업 스케줄러 문제
작업 스케줄러(LeetCode 621): CPU 작업 목록(각 작업은 A~Z 중 하나로 표시됨)과 대기 시간 n이 주어질 때, 모든 작업을 완료하는 데 필요한 CPU 구간의 최소 개수를 구합니다. 같은 작업을 다시 실행하려면 최소 n개의 구간을 기다려야 합니다. 유휴 구간은 허용됩니다. 대기 시간이 2이고 작업이 ['A','A','A','B','B','B']라면 답은 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번 나타나고 빈도가 f인 작업의 수가 max_count라면, 전체 시간은 max(len(tasks), (f-1) * (n+1) + max_count)입니다. 공식은 크기가 n+1인 틀을 f-1개 만들고 다른 작업으로 채운 다음 마지막 순환 구간을 더하는 방식입니다. 다른 작업이 모든 유휴 칸을 채울 수 있다면(서로 다른 작업이 많은 경우), 유휴 시간 없이 모든 작업을 바로 실행하면 됩니다.
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개). 빈도가 가장 높은 작업은 빈도가 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(전체 소요 시간 × 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탐욕적으로 시작점을 정해도 올바른 이유
정확성을 다음과 같이 증명할 수 있습니다. start에서 주유소 i에 도달한 뒤 연료가 음수가 되었다고 합시다. 그러면 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'일 때)는 유휴 슬롯이 정확히 채워집니다. 작업 유형이 매우 다양하면 작업이 모든 프레임을 채워 유휴 슬롯이 0일 수 있습니다. 주유소 — 전체 연료와 전체 비용이 정확히 같으면 유효한 시작 지점은 정확히 하나입니다. 한 주유소에 전체 순환 경로를 완주할 수 있을 만큼의 연료가 있으면 그 주유소가 답입니다. 항상 이러한 퇴화 사례에서 탐욕적 답을 검증하세요.
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 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“작업 스케줄러와 주유소”에서 뭘 배우나요?
CPU 작업 스케줄러의 냉각 기간 문제와 순환 주유소의 가능성 문제에 그리디 사고를 적용합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“작업 스케줄러와 주유소” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 그리디와 DP: 각각 언제 사용할까
- 구간 스케줄링과 병합
- 점프 게임 I과 II
- 작업 스케줄러와 주유소