Coding Interview Prep · レッスン

Jump Game IとII

DPを使わず、到達可能範囲を貪欲に拡張する方法で、到達可能性と最小ジャンプ回数を求めます。

レッスン 3/413 ステップ

「Jump Game IとII」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

Jump Game I:末尾に到達できますか

Jump Game I(LeetCode 55)は、nums[i] がインデックス i からの最大ジャンプ長を表す配列について、インデックス0から開始して最後のインデックスに到達できるかを判定する問題です。[2, 3, 1, 1, 4] では、末尾に到達できます(2つ先へジャンプして3に進み、そこから末尾まで到達できます)。[3, 2, 1, 0, 4] では到達できません(常にジャンプ長0のインデックス3に着地してしまいます)。貪欲法の計算量は 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

貪欲法:最大到達位置を追跡する

Jump Game I における貪欲法の着眼点は、これまでに到達可能な最も遠いインデックスを表す max_reach を保持することです。各インデックス i で、max_reach = max(max_reach, i + nums[i]) と更新します。途中で i > max_reach となった場合、現在のインデックスには到達できないため、return False します。最後のインデックスに到達するか、それを越えたら return True します。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

Jump Game I を追跡する

[3, 2, 1, 0, 4] を追跡してみましょう。i=0、jump=3、max_reach=3。i=1、jump=2、max_reach=max(3,3)=3。i=2、jump=1、max_reach=max(3,3)=3。i=3、jump=0、max_reach=max(3,3)=3。i=4 では、i=4 > max_reach=3 なので、return False します。このアルゴリズムは、インデックス4に到達できないことを正しく判定しています。インデックス0からのすべての経路は、インデックス3の 0 によって max_reach が3に制限されるため、そこで行き詰まります。

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]))

Jump Game II:最小ジャンプ数

Jump Game 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

Jump Game II を視覚化する

Jump Game II は、キューのオーバーヘッドをなくしたBFSのレベルごとの走査だと考えることができます。各ジャンプがBFSの1つのレベルに対応します。curr_end は現在のレベルの境界です。farthest は次のレベルで到達可能な最大インデックスです。現在のレベルの走査を終えた時点(i == curr_end)で、次のレベルの境界が確定するため、ジャンプ数を増やします。これは暗黙的なグラフに対する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]))

なぜ貪欲法が Jump Game II で正しいのか

なぜ、常に最も遠くまで範囲を広げる貪欲法で最小ジャンプ数を求められるのでしょうか。交換論法で考えます。最適解が、最も遠い地点まで到達しないジャンプを行っているとします。そのジャンプを farthest まで到達するように延長しても、追加コストはありません。依然として1回のジャンプだからです。ジャンプごとに最大の範囲を選ぶことで、必要なジャンプ数が最小になることを保証できます。ジャンプごとの範囲を小さくする解がこれより良くなることはなく、同じ距離を進むためにより多くのジャンプが必要になります。

# 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))

Jump Game II のDPによる別解

DP解では、dp[i] をインデックス i に到達するための最小ジャンプ数とします。各位置 j について、到達可能なすべての位置を更新します。つまり、k が 1..nums[j] の範囲にあるとき、dp[j+k] = min(dp[j+k], dp[j]+1) とします。計算量は 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

Jump Game III:インデックス0に到達する

Jump Game 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)

Jump Game VII:範囲内をジャンプして到達する

Jump Game VII(LeetCode 1871)は、0と1からなる文字列について、インデックス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解法の比較

Jump Game II には、同じく O(n) で解ける2つの方法があります。貪欲な範囲拡張と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

Jump Game の計算量まとめ

Jump Game の各バリエーションにおける計算量をまとめます。Jump I(到達可能性):時間 O(n)、空間 O(1)。Jump II(最小ジャンプ数の貪欲法):時間 O(n)、空間 O(1)。Jump II(BFS):時間 O(n)、空間 O(n)。Jump II(DP):時間 O(n × max_jump)、空間 O(n)。Jump III(BFS/DFS):時間 O(n)、訪問済み情報に O(n) の空間。Jump VII(スライディングウィンドウ):時間 O(n)、空間 O(n)。面接では、Jump 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')

理解度チェック

このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、Jump Game I で max_reach を貪欲に追跡し、O(n) の時間計算量と O(1) の空間計算量で到達可能性を判定する方法、Jump Game II で curr_end と farthest を使って範囲を拡張し、O(n)、O(1) で最小ジャンプ数を数える方法、そして貪欲な範囲拡張が、キューのオーバーヘッドなしのレベルごとのBFSと等価であることを学びました。次は、Task Scheduler のクールダウン期間と Gas Station の円環状の実行可能性問題に、貪欲な考え方を応用します。

無料で開始

AI チューターと学ぶ Coding Interview Prep — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
90
レッスン
360

よくある質問

「Jump Game IとII」レッスンは無料ですか?

はい。「Jump Game IとII」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「Jump Game IとII」で何を学びますか?

DPを使わず、到達可能範囲を貪欲に拡張する方法で、到達可能性と最小ジャンプ回数を求めます。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「Jump Game IとII」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 貪欲法とDP:使い分け
  2. 区間スケジューリングとマージ
  3. Jump Game IとII
  4. Task SchedulerとGas Station
← Coding Interview Prepに戻る