점프 게임 I과 II
DP 없이 그리디한 범위 확장 방식으로 도달 가능 여부와 최소 점프 횟수를 구합니다.
점프 게임 I과 II은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
점프 게임 I: 끝까지 도달할 수 있을까요?
점프 게임 I(LeetCode 55): nums[i]가 인덱스 i에서 이동할 수 있는 최대 점프 길이인 배열이 주어질 때, 인덱스 0에서 시작하여 마지막 인덱스에 도달할 수 있는지 판별합니다. [2, 3, 1, 1, 4]에서는 끝까지 도달할 수 있습니다(2만큼 점프하여 3으로 이동한 다음, 3에서 끝까지 갈 수 있습니다). [3, 2, 1, 0, 4]에서는 도달할 수 없습니다(항상 점프 길이가 0인 0에 도착하게 됩니다). 탐욕적 해법은 O(n)에 실행됩니다.
# Can you reach the last index?
nums1 = [2, 3, 1, 1, 4] # True: 0→1→4 or 0→2→3→4
nums2 = [3, 2, 1, 0, 4] # False: always land on index 3 (value 0)
# At index 3 (value 0): no matter how you get here,
# you can't jump further to reach index 4
print('nums1 last index:', len(nums1)-1)
print('nums2 index 3 jump value:', nums2[3]) # 0 = stuck탐욕법: 최대 도달 범위 추적
점프 게임 I의 핵심적인 탐욕적 관찰은 지금까지 도달할 수 있는 가장 먼 인덱스인 max_reach를 유지하는 것입니다. 각 인덱스 i에서 max_reach = max(max_reach, i + nums[i])로 갱신합니다. 어느 순간이든 i > max_reach이면 현재 인덱스에는 도달할 수 없으므로 거짓을 반환합니다. 마지막 인덱스에 도달하거나 지나치면 참을 반환합니다. DP나 역추적은 필요하지 않습니다.
def can_jump(nums):
max_reach = 0
for i, jump in enumerate(nums):
if i > max_reach: # can't reach index i
return False
max_reach = max(max_reach, i + jump)
if max_reach >= len(nums) - 1:
return True # early exit
return True
print(can_jump([2, 3, 1, 1, 4])) # True
print(can_jump([3, 2, 1, 0, 4])) # False
print(can_jump([0])) # True (already at last index)
print(can_jump([1, 0, 0])) # False점프 게임 I 추적
[3, 2, 1, 0, 4]를 추적해 보겠습니다. i=0, 점프=3, max_reach=3입니다. i=1, 점프=2, max_reach=max(3,3)=3입니다. i=2, 점프=1, max_reach=max(3,3)=3입니다. i=3, 점프=0, max_reach=max(3,3)=3입니다. i=4에서는 i=4 > max_reach=3이므로 거짓을 반환합니다. 알고리즘은 인덱스 4에 도달할 수 없다는 사실을 정확히 판별합니다. 인덱스 3의 0 때문에 max_reach가 3으로 제한되므로 인덱스 0에서 시작하는 모든 경로가 막힙니다.
def can_jump_trace(nums):
max_reach = 0
for i, jump in enumerate(nums):
print(f'i={i}, jump={jump}, max_reach before={max_reach}', end='')
if i > max_reach:
print(' → UNREACHABLE')
return False
max_reach = max(max_reach, i + jump)
print(f' → max_reach={max_reach}')
return True
print('Result:', can_jump_trace([3, 2, 1, 0, 4]))점프 게임 II: 최소 점프 횟수
점프 게임 II(LeetCode 45)는 마지막 인덱스에 도달하는 데 필요한 최소 점프 횟수를 묻습니다(항상 도달할 수 있다고 가정합니다). 탐욕적 접근법은 범위 확장 전략을 사용합니다. 현재 점프에서 도달할 수 있는 가장 먼 위치인 curr_end와 다음 점프에서 도달할 수 있는 가장 먼 위치인 farthest를 유지합니다. 현재 점프의 범위를 모두 사용하면 반드시 점프해야 하므로 jumps를 증가시키고 curr_end = farthest로 설정합니다.
def jump(nums):
n = len(nums)
if n == 1: return 0 # already at destination
jumps = 0
curr_end = 0 # end of current jump's range
farthest = 0 # farthest reachable in next jump
for i in range(n - 1): # don't jump from last index
farthest = max(farthest, i + nums[i])
if i == curr_end: # exhausted current jump range
jumps += 1
curr_end = farthest
if curr_end >= n - 1: break
return jumps
print(jump([2, 3, 1, 1, 4])) # 2 (0→1→4)
print(jump([2, 3, 0, 1, 4])) # 2 (0→1→4)
print(jump([1, 2, 1, 1, 1])) # 3점프 게임 II 시각화
점프 게임 II를 대기열 오버헤드 없이 레벨별로 진행하는 BFS 방식이라고 생각해 보세요. 각 점프가 BFS의 한 레벨에 해당합니다. curr_end는 현재 레벨의 경계이고, farthest는 다음 레벨에서 도달할 수 있는 최대 인덱스입니다. 현재 레벨을 모두 순회하여 i가 현재 레벨의 끝에 도달하면 다음 레벨의 경계를 정한 것이므로 점프 횟수를 증가시켜야 합니다. 이는 암시적 그래프에서 수행하는 BFS이며 시간 복잡도는 O(n), 공간 복잡도는 O(1)입니다.
def jump_traced(nums):
n = len(nums)
jumps = curr_end = farthest = 0
for i in range(n - 1):
farthest = max(farthest, i + nums[i])
print(f'i={i}: farthest={farthest}, curr_end={curr_end}')
if i == curr_end:
jumps += 1
curr_end = farthest
print(f' → JUMP #{jumps}, new range ends at {curr_end}')
if curr_end >= n - 1: break
return jumps
print('Min jumps:', jump_traced([2, 3, 1, 1, 4]))점프 게임 II에서 탐욕법이 올바른 이유
항상 가장 먼 곳까지 확장하는 탐욕법이 어떻게 최소 점프 횟수를 보장할까요? 교환 논증으로 설명할 수 있습니다. 최적해가 가장 먼 지점까지 도달하지 않는 점프를 한다고 가정해 보겠습니다. 그 점프를 가장 먼 지점인 farthest까지 확장해도 비용은 늘어나지 않습니다. 여전히 한 번의 점프이기 때문입니다. 각 점프에서 가능한 최대 범위를 선택하면 필요한 점프 횟수가 최소가 됩니다. 각 점프에서 더 짧은 범위를 선택하는 해법은 이보다 나을 수 없으며, 같은 거리를 이동하려면 더 많은 점프가 필요합니다.
# Correctness verification: compare to BFS
from collections import deque
def jump_bfs(nums):
n = len(nums)
if n == 1: return 0
visited = [False] * n
visited[0] = True
queue = deque([0])
level = 0
while queue:
level += 1
for _ in range(len(queue)):
pos = queue.popleft()
for j in range(1, nums[pos] + 1):
nxt = pos + j
if nxt >= n - 1: return level
if not visited[nxt]:
visited[nxt] = True
queue.append(nxt)
return -1
# Both should give same results
for nums in [[2,3,1,1,4],[2,3,0,1,4],[1,2,1,1,1]]:
print(jump(nums), '==', jump_bfs(nums))점프 게임 II의 DP 대안
DP 해법에서는 dp[i]를 인덱스 i에 도달하는 데 필요한 최소 점프 횟수로 정의합니다. 각 위치 j에서 도달할 수 있는 모든 위치를 다음과 같이 갱신합니다. dp[j+k] = min(dp[j+k], dp[j]+1)이며, k의 범위는 1..nums[j]입니다. 이 방법의 시간 복잡도는 O(n × max_jump), 공간 복잡도는 O(n)으로 O(n)인 탐욕적 해법보다 훨씬 느립니다. 여기서는 탐욕법이 더 우수하며, 탐욕법이 내부 반복을 어떻게 없애는지 비교하기 위해 DP를 함께 보여 드립니다.
def jump_dp(nums):
n = len(nums)
dp = [float('inf')] * n
dp[0] = 0
for j in range(n):
for k in range(1, nums[j] + 1):
if j + k < n:
dp[j+k] = min(dp[j+k], dp[j] + 1)
return dp[n-1]
print(jump_dp([2, 3, 1, 1, 4])) # 2
print(jump_dp([1, 2, 1, 1, 1])) # 3
# Greedy is O(n), DP is O(n * max_jump)
# For large inputs with big jump values, greedy is much faster점프 게임 III: 인덱스 0에 도달하기
점프 게임 III(LeetCode 1306): 주어진 인덱스에서 시작하여 인덱스 i에서 i + nums[i] 또는 i - nums[i]로 이동합니다. 값이 0인 인덱스에 도달할 수 있을까요? 이는 최소화 문제가 아닌 도달 가능성 문제이므로(BFS/DFS), 탐욕법을 적용할 수 없습니다. 순환을 피하기 위해 방문 집합을 사용하는 BFS를 활용합니다. 시간 복잡도는 O(n)입니다.
from collections import deque
def can_reach(arr, start):
n = len(arr)
visited = set()
queue = deque([start])
while queue:
idx = queue.popleft()
if arr[idx] == 0: return True
if idx in visited: continue
visited.add(idx)
for nxt in [idx + arr[idx], idx - arr[idx]]:
if 0 <= nxt < n and nxt not in visited:
queue.append(nxt)
return False
print(can_reach([4,2,3,0,3,1,2], 5)) # True (5→4→1→3, arr[3]=0)
print(can_reach([3,0,2,1,2], 2)) # False (can't reach index 1, arr[1]=0)점프 게임 VII: 범위 내에서 도달하기
점프 게임 VII(LeetCode 1871): 인덱스 0에서 마지막 인덱스까지 점프하여 이진 문자열을 통과할 수 있는지 묻는 문제입니다. 위치 i에서는 [i+minJump, i+maxJump] 범위에 있는 '0'으로 이동할 수 있습니다. 도달 가능 배열에 슬라이딩 윈도 합을 사용합니다. 도달 가능한 위치의 누적 합을 유지하면, 위치 j는 [j-maxJump, j-minJump] 범위에 도달 가능한 위치가 있을 때 도달할 수 있습니다.
def can_reach_vii(s, min_jump, max_jump):
n = len(s)
reach = [False] * n
reach[0] = True
pre = [0] * (n + 1) # prefix sum of reachable positions
pre[1] = 1
for j in range(1, n):
# Window sum: any reachable position in [j-maxJump, j-minJump]?
lo = max(0, j - max_jump)
hi = max(0, j - min_jump + 1)
window_sum = pre[hi] - pre[lo]
if s[j] == '0' and window_sum > 0:
reach[j] = True
pre[j+1] = pre[j] + (1 if reach[j] else 0)
return reach[n-1]
print(can_reach_vii('011010', 2, 3)) # True
print(can_reach_vii('01101110', 2, 3)) # False탐욕적 해법과 BFS 해법 비교
점프 게임 II에는 서로 같은 결과를 내는 두 가지 O(n) 접근법이 있습니다. 탐욕적 범위 확장과 BFS 레벨 순회입니다. 탐욕적 접근법은 대기열을 사용하지 않으므로 공간 복잡도가 O(1)이고, BFS는 방문 집합에 O(n)의 공간을 사용합니다. 면접에서는 공간 효율이 높은 탐욕법을 선호합니다. 하지만 BFS가 먼저 도출하기는 더 쉽습니다. 탐욕적 해법이 잘 보이지 않는다면 먼저 BFS로 동작하는 해법을 작성한 다음 최적화해 보세요. 두 방법 모두 최소 점프 횟수를 정확하게 계산합니다.
# Both approaches are O(n) time
# Greedy: O(1) space — preferred in interviews
# BFS: O(n) space — easier to derive
# Greedy advantage: no auxiliary data structures
def jump_greedy(nums):
n, jumps, curr, far = len(nums), 0, 0, 0
for i in range(n-1):
far = max(far, i+nums[i])
if i == curr: jumps += 1; curr = far
return jumps
# BFS equivalence: each level = one jump
from collections import deque
def jump_bfs(nums):
n = len(nums)
if n == 1: return 0
q, visited, level = deque([0]), {0}, 0
while q:
level += 1
for _ in range(len(q)):
pos = q.popleft()
for j in range(1, nums[pos]+1):
nxt = pos + j
if nxt >= n-1: return level
if nxt not in visited: visited.add(nxt); q.append(nxt)
return -1
nums = [2,3,1,1,4]
print(jump_greedy(nums), '==', jump_bfs(nums)) # both 2점프 게임 복잡도 요약
점프 게임 변형별 복잡도는 다음과 같습니다. 점프 I(도달 가능성): 시간 O(n), 공간 O(1). 점프 II(최소 점프 탐욕법): 시간 O(n), 공간 O(1). 점프 II(BFS): 시간 O(n), 공간 O(n). 점프 II(DP): 시간 O(n × 최대 점프 길이), 공간 O(n). 점프 III(BFS/DFS): 시간 O(n), 방문 기록에 필요한 공간 O(n). 점프 VII(슬라이딩 윈도): 시간 O(n), 공간 O(n). 면접에서는 점프 I과 II에 대해 항상 탐욕적 O(n), O(1) 해법을 제시하세요.
# Comparison: all versions on the same input
nums = [2, 3, 1, 1, 4]
# Jump I
def can_jump(nums):
mr = 0
for i, j in enumerate(nums):
if i > mr: return False
mr = max(mr, i+j)
return True
# Jump II greedy O(n) O(1)
def jump_min(nums):
n, jumps, curr, far = len(nums), 0, 0, 0
for i in range(n-1):
far = max(far, i+nums[i])
if i == curr:
jumps += 1; curr = far
if curr >= n-1: break
return jumps
print('Can reach:', can_jump(nums)) # True
print('Min jumps:', jump_min(nums)) # 2
print('Complexity: O(n) time, O(1) space')빠른 확인
이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.
단원 요약
이 단원에서는 다음을 배웠습니다. 점프 게임 I에서는 max_reach를 탐색하며 O(n) 시간과 O(1) 공간으로 도달 가능성을 판별하는 방법, 점프 게임 II에서는 curr_end와 farthest를 사용한 범위 확장으로 O(n) 시간과 O(1) 공간에서 최소 점프 횟수를 세는 방법, 그리고 탐욕적 범위 확장이 대기열 오버헤드 없이 레벨별 BFS와 동등하다는 사실입니다. 다음으로는 작업 스케줄러의 대기 시간과 주유소 순환 가능성 문제에 탐욕적 사고를 적용합니다.
AI 튜터와 함께 Python을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“점프 게임 I과 II” 강의는 무료인가요?
네 — “점프 게임 I과 II” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“점프 게임 I과 II”에서 뭘 배우나요?
DP 없이 그리디한 범위 확장 방식으로 도달 가능 여부와 최소 점프 횟수를 구합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“점프 게임 I과 II” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 그리디와 DP: 각각 언제 사용할까
- 구간 스케줄링과 병합
- 점프 게임 I과 II
- 작업 스케줄러와 주유소