Jump Game
배열 nums의 인덱스 0에 서 있습니다. 인덱스 i에서 nums[i]까지의 단계 수만큼 앞으로 점프할 수 있으므로, nums[i]는 그 위치에서 가능한 가장 긴 점프이며 0은 이동할 수 없다는 뜻입니다. 어떤 점프 순서로든 마지막 인덱스에 도달할 수 있으면 true를 반환하고, 그렇지 않으면 false를 반환하세요.
함수
- numsinteger-array
- 각 인덱스에서 이동할 수 있는 가장 긴 거리
- 반환값boolean
- 0번 인덱스에서 시작해 마지막 인덱스에 도달할 수 있으면 true, 그렇지 않으면 false
제약 조건
1 ≤ nums.length ≤ 1040 ≤ nums[i] ≤ 105- 점프 거리는
nums[i]보다 짧을 수 있으므로, 긴 점프를 하더라도 마지막 인덱스를 지나치게 되지는 않습니다.
예제
- 입력
- nums = [2, 0, 3, 1, 0, 2]
- 출력
- true
- 설명
- 인덱스 0에서 인덱스 1 또는 2로 이동할 수 있습니다. 인덱스 1에는 0이 있으며 막다른 곳이지만, 인덱스 2에는 3이 있으며 마지막 인덱스인 인덱스 5에 도달합니다.
- 입력
- nums = [1, 3, 0, 0, 0, 2]
- 출력
- false
- 설명
- 인덱스 0은 인덱스 1로만 이동할 수 있고, 인덱스 1은 최대 인덱스 4까지 도달합니다. 인덱스 2, 3, 4는 모두 0을 담고 있으므로, 어떤 값도 인덱스 4를 넘어 인덱스 5까지 도달하지 못합니다.
- 입력
- nums = [0]
- 출력
- true
- 설명
- 배열에는 요소가 하나 있으므로 마지막 인덱스에서 시작하며 전혀 건너뛸 필요가 없습니다.
제출 시 숨은 테스트 +18개
후속 질문
마지막 인덱스에 도달하는 서로 다른 점프 시퀀스의 개수를 10^9+7로 나눈 나머지로 구하세요. 시간 복잡도는 여전히 O(n)입니다.
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
앞에 있는 어떤 것도 그 너머로 건너뛸 수 없을 때만
0에 갇히게 됩니다. 그러한 판단을 하려면 그 앞에 있는 인덱스에 대해 무엇을 알아야 할까요?인덱스
i에 도달할 수 있다면, 더 짧은 점프도 가능하므로i부터i+nums[i]까지의 모든 인덱스에 도달할 수 있습니다. 따라서 도달 가능한 인덱스는 항상 인덱스 0에서 시작하는 끊김 없는 하나의 구간을 이룹니다.왼쪽에서 오른쪽으로 이동하면서 해당 블록의 오른쪽 끝인
farthest를 유지합니다. 현재 인덱스가farthest를 넘어가면 그 인덱스에는 절대 도달할 수 없습니다. 그렇지 않으면i+nums[i]가 더 클 때farthest를 그 값으로 늘립니다. 배열 전체를 끝까지 이동할 수 있다면 마지막 인덱스에 도달할 수 있습니다.
풀이
가능한 경로의 수는 기하급수적으로 증가하므로 긴 배열에서는 경로를 하나씩 확인하는 방식이 통하지 않습니다. 핵심은 도달할 수 있는 인덱스가 항상 인덱스 0에서 시작하는 끊기지 않은 하나의 구간을 이룬다는 점입니다. 그 구간의 오른쪽 끝을 나타내는 숫자 하나에 필요한 모든 정보가 담겨 있으며, 한 번의 순회로 답을 결정할 수 있습니다.
모든 점프를 시도해 보세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
가장 직접적인 방법은 직접 해 보는 것입니다. 인덱스 0에 서서 점프할 수 있는 모든 착지 지점을 하나씩 시도합니다. 각 착지 지점에서도 똑같이 반복합니다. 어떤 경로에서든 마지막 인덱스에 도달하면 답은 true입니다. 모든 경로가 막다른 곳에 이르면 false입니다.
첫 번째 예시에서 인덱스 0에는 2가 있으므로 인덱스 1과 인덱스 2를 시도합니다. 인덱스 1에는 0이 있어 막다른 곳이므로, 되돌아가 인덱스 2를 시도합니다. 인덱스 2에는 3이 있고 인덱스 5인 마지막 인덱스에 도달하므로, 탐색은 true로 끝납니다.
모든 경로를 살펴보기 때문에 이 탐색은 정확합니다. 하지만 이것이 바로 문제이기도 합니다. 이미 탐색한 인덱스를 기억하지 않으므로, 그 인덱스에 도달하는 경로마다 같은 인덱스를 다시 탐색합니다. 답이 false인 경우에는 모든 경로를 배제해야 합니다. [4, 3, 2, 1, 0, 5]에서는 0 앞에 있는 모든 인덱스가 0에 도달할 수 있어, 0에 이르는 서로 다른 경로가 8개입니다. 이러한 인덱스가 30개라면 경로가 5억 개를 넘으며, 가장 큰 테스트에는 요소가 10,000개 있습니다. 이렇게 긴 경로는 일부 언어에서 호출 스택 오버플로도 일으킵니다. Python은 기본적으로 중첩 호출이 1,000회에 이르면 중단합니다.
알고리즘
- 인덱스
i에서 마지막 인덱스까지 도달할 수 있는지 확인하는 도우미 함수reach(i)를 작성하세요. i가 마지막 인덱스라면true를 반환하세요.- 그렇지 않으면
i+1부터min(i+nums[i], n-1)까지의 각 착지 지점next를 시도하고,reach(next)가true를 반환하는 즉시true를 반환하세요. - 가능한 착지 지점이 없다면
false를 반환하세요. - 정답은
reach(0)입니다.
def canJump(nums):
last = len(nums) - 1
def reach(i):
# Can you get from index i to the last index?
if i == last:
return True
for nxt in range(i + 1, min(i + nums[i], last) + 1):
if reach(nxt):
return True
return False
return reach(0)어떤 인덱스에서 끝날 수 있는지 기억하세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
위의 탐색은 "인덱스 j에서 끝까지 갈 수 있는가?"라는 같은 질문을 계속 반복합니다. j에 대한 답은 바뀌지 않으므로, 한 번 계산해서 저장하면 됩니다. 어떤 인덱스에서 마지막 인덱스까지 갈 수 있으면 그 인덱스를 양호하다고 하겠습니다. 마지막 인덱스는 양호합니다. 그 외의 인덱스 i는 도달할 수 있는 인덱스, 즉 i+1부터 i+nums[i]까지의 인덱스 중 하나 이상이 양호하면 양호합니다.
각 인덱스는 오른쪽에 있는 인덱스에만 의존하므로, 테이블 good을 오른쪽에서 왼쪽으로 채웁니다. 첫 번째 예시에서 인덱스 5는 양호합니다. 인덱스 4의 값은 0이므로 양호하지 않습니다. 인덱스 3에서는 인덱스 4에만 도달할 수 있으므로 양호하지 않습니다. 인덱스 2에서는 인덱스 3, 4, 5에 도달할 수 있고, 5가 양호하므로 2도 양호합니다. 인덱스 1의 값은 0이므로 양호하지 않습니다. 인덱스 0에서는 1과 2에 도달할 수 있고, 2가 양호하므로 답은 true입니다.
이제 각 인덱스의 상태는 한 번만 결정하지만, 결정하는 과정에서 여전히 최대 n개의 셀을 검사할 수 있습니다. [9998, 9997, …, 1, 0, 7]에서는 모든 인덱스가 0에 도달할 수 있지만 그 너머에는 도달할 수 없으므로, 각 인덱스는 전체 범위를 검사하고도 양호한 인덱스를 찾지 못합니다. 원소가 10,000개일 때 검사 횟수는 약 5 × 10^7회이며, 가장 큰 테스트는 이와 같은 방식으로 구성되어 있습니다. 작업량은 길이의 제곱에 비례해 증가하므로, 이러한 테스트에서는 시간 초과가 발생합니다.
알고리즘
- 길이가
n인 불리언 배열good을 만들고good[n-1]을 true로 설정합니다. i를n-2부터 0까지 순회합니다.j를i+1부터min(i+nums[i], n-1)까지 살펴봅니다.good[j]중 하나라도 true이면good[i]를 true로 설정하고 탐색을 중단합니다.good[0]을 반환합니다.
def canJump(nums):
n = len(nums)
good = [False] * n # good[i]: from i you can reach the last index
good[n - 1] = True
for i in range(n - 2, -1, -1):
for j in range(i + 1, min(i + nums[i], n - 1) + 1):
if good[j]:
good[i] = True
break
return good[0]도달 가능한 가장 먼 인덱스 추적
핵심 아이디어
경로가 아니라 도달할 수 있는 인덱스를 살펴보세요. 인덱스 i에서는 i+1부터 i+nums[i]까지의 모든 인덱스에 도달할 수 있으며, 그 사이에 빠지는 인덱스는 없습니다. 따라서 인덱스 i에 도달할 수 있다면 i+nums[i]까지의 모든 인덱스에도 도달할 수 있습니다. 인덱스 0만 있는 상태에서 시작해 이 구간들을 계속 추가하세요. 새 구간은 이미 확보한 블록 안에서 시작하므로, 도달 가능한 인덱스는 항상 끊기지 않은 하나의 블록, 즉 [0, farthest]를 이룹니다.
그래서 숫자 하나면 충분합니다. i를 왼쪽에서 오른쪽으로 이동하세요. i ≤ farthest인 동안에는 인덱스 i에 도달할 수 있으므로, farthest를 max(farthest, i+nums[i])까지 늘리세요. i가 farthest를 넘어가면, 도달 가능한 어떤 인덱스에서도 i로 점프할 수 없습니다. 이 블록은 그 간격을 넘어 확장될 수 없으므로, 마지막 인덱스를 포함해 그 오른쪽의 어떤 인덱스에도 도달할 수 없습니다. 간격 없이 끝까지 이동하면 마지막 인덱스에 도달할 수 있습니다.
두 번째 예시에서는 farthest가 0에서 시작해 인덱스 0을 지난 뒤 1이 되고, 인덱스 1을 지난 뒤 4가 됩니다. 인덱스 2, 3, 4의 값은 0이므로 farthest는 4에 그대로 있습니다. 인덱스 5는 4를 넘어가므로 답은 false입니다. 첫 번째 예시에서는 인덱스 2가 farthest를 5까지 늘리고, 어떤 인덱스도 이를 넘어가지 않으므로 답은 true입니다.
가장 멀리 도달할 수 있는 위치만 기억해도 안전한 이유는 무엇일까요? 점프를 하나로 확정하지 않기 때문입니다. 이 블록에는 어떤 경로로든 도달할 수 있는 모든 인덱스가 포함되며, 더 가까운 도착 지점은 모두 그 안에 있습니다. 오른쪽 끝을 제외한 나머지를 버려도 정보는 손실되지 않습니다.
알고리즘
farthest = 0으로 설정합니다.- 왼쪽에서 오른쪽으로 각 인덱스
i에 대해 다음을 수행합니다.i > farthest이면false를 반환합니다. - 그렇지 않으면
farthest = max(farthest, i+nums[i])로 설정합니다. - 루프가 끝나면 모든 인덱스에 도달할 수 있으므로
true를 반환합니다.
def canJump(nums):
farthest = 0 # every index up to farthest can be reached
for i, jump in enumerate(nums):
if i > farthest:
return False # nothing reachable jumps to i
farthest = max(farthest, i + jump)
return True
함정과 경계 사례
잘못된 답변은 대부분 nums[i]를 유일한 점프로 읽거나 루프 안의 두 검사 순서 때문에 발생합니다.
- 항상 정확히
nums[i]칸을 점프하거나 항상 가장 긴 점프를 합니다.[2, 5, 0, 0]에서 인덱스 0에서 한 번에 점프하면 0에 도달하지만, 1칸 점프해 인덱스 1로 가면 끝에 도달합니다. - 0을 보자마자
false를 반환합니다. 0은 그 전에 0을 넘어 점프할 수 있는 경우가 없을 때만 중요합니다.[2, 0, 1]은 0을 건너뛰므로 답은true입니다. i > farthest를 확인하기 전에farthest를 갱신합니다. 도달할 수 없는 인덱스가 범위를 늘려서는 안 되므로, 먼저 확인한 다음 갱신하세요.- 원소가 하나인 배열을 실패로 취급합니다. 이미 마지막 인덱스에 있으므로, 그 원소가 0이더라도 답은
true입니다. - 긴 배열에서 재귀를 사용합니다. 경로가 10,000번의 점프로 이어질 수 있으며, 여러 언어에서 호출 스택이 넘칠 수 있습니다. 한 번만 순회하면 재귀가 필요하지 않습니다.
자주 묻는 질문4
Jump Game의 시간 복잡도는 얼마인가요?
가장 멀리 도달하는 패스는 각 인덱스를 한 번씩 방문하므로 O(n) 시간에 실행되며 추가 공간은 O(1)입니다. 테이블 방식은 최악의 경우 O(n²)이고, 모든 경로를 시도하면 지수 시간이 걸립니다.
Jump Game에서는 탐욕적 접근 방식이 왜 효과가 있을까요?
더 짧은 점프도 허용되므로 인덱스 i에 도달할 수 있다는 것은 i+nums[i]까지의 모든 인덱스에 도달할 수 있다는 뜻입니다. 이러한 구간은 항상 이미 도달한 부분과 겹치므로, 도달 가능한 인덱스는 0에서 시작하는 하나의 구간을 이룹니다. 그리디 순회에서는 이 구간의 오른쪽 끝만 추적하며, 이 값이 구간 전체를 나타내므로 성공할 수 있었던 경로를 절대 버리지 않습니다.
Jump Game은 동적 프로그래밍 문제인가요?
동적 프로그래밍으로 해결할 수 있습니다. 오른쪽에서 왼쪽으로 표를 채우면서, 착지할 수 있는 위치 중 하나가 좋은 위치일 때 각 인덱스를 좋은 위치로 표시합니다. 이 방법의 시간 복잡도는 O(n²)입니다. 가장 왼쪽에 있는 좋은 인덱스만 중요하다는 점에 주목하세요. 어떤 인덱스가 좋은 인덱스에 도달할 수 있다면 가장 왼쪽에 있는 인덱스에도 도달할 수 있기 때문입니다. 그 인덱스인 goal만 유지하고, i+nums[i] ≥ goal이면 goal을 i로 옮기세요. 답은 goal이 0에서 끝나는지 여부이며, 이는 그리디 방법과 같은 원리의 O(n) 순회입니다.
최소 점프 횟수는 어떻게 구하나요?
같은 최장 도달 아이디어를 레이어별로 적용하세요. 현재 점프 횟수로 도달할 수 있는 블록의 끝과 다음 점프로 도달할 수 있는 가장 먼 인덱스를 유지하세요. i가 현재 블록의 끝을 지나면 점프가 하나 더 필요하고, 다음 블록은 그 가장 먼 인덱스에서 끝납니다. 여전히 한 번의 O(n) 순회입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def canJump(nums):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
nums = [2, 0, 3, 1, 0, 2]
기대값
true