title: Jump Game
slug: jump-game
difficulty: medium
leetcode_id: 55
leetcode_url: https://leetcode.com/problems/jump-game/
categories:
  - arrays
  - dynamic-programming
patterns:
  - greedy
  - dynamic-programming

function_signature: "def can_jump(nums: list[int]) -> bool:"

test_cases:
  visible:
    - input: { nums: [2, 3, 1, 1, 4] }
      expected: true
    - input: { nums: [3, 2, 1, 0, 4] }
      expected: false
  hidden:
    - input: { nums: [0] }
      expected: true
    - input: { nums: [2, 0, 0] }
      expected: true
    - input: { nums: [1, 0, 1, 0] }
      expected: false
    - input: { nums: [5, 4, 3, 2, 1, 0, 0] }
      expected: true
    - input: { nums: [1, 1, 1, 1, 1] }
      expected: true
    - input: { nums: [2, 0, 2, 0, 1] }
      expected: true
    - input: { nums: [1, 2, 3] }
      expected: true

description: |
  You are given an integer array `nums`. You are initially positioned at the array's **first index**, and each element in the array represents your maximum jump length at that position.

  Return `true` *if you can reach the last index*, or `false` *otherwise*.

constraints: |
  - `1 <= nums.length <= 10^4`
  - `0 <= nums[i] <= 10^5`

examples:
  - input: "nums = [2,3,1,1,4]"
    output: "true"
    explanation: "Jump 1 step from index 0 to 1, then 3 steps to the last index."
  - input: "nums = [3,2,1,0,4]"
    output: "false"
    explanation: "You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index."

explanation:
  intuition: |
    Imagine you're hopping across stepping stones to reach the other side of a river. Each stone tells you the *maximum* distance you can jump from it — but you can choose to jump any shorter distance too.

    The key insight is that you don't need to track every possible path. Instead, think about it this way: **what's the farthest position you can possibly reach?** As you walk through the array, each position extends your reach. If at any point you find yourself stuck (your current position is beyond your maximum reach), you know you'll never make it.

    Think of it like filling a gas tank: at each position, you're potentially adding "fuel" (jump range) to extend how far you can go. The question becomes: can you keep extending your reach until it covers the finish line?

    This greedy approach works because we only care about *whether* we can reach the end, not *how* we reach it. If we can reach position `i`, and from `i` we can jump to position `j`, then we can definitely reach `j` — we don't need to track the exact path.

  approach: |
    We solve this using a **Greedy Approach** by tracking the maximum reachable index:

    **Step 1: Initialise the maximum reach**

    - `max_reach`: Set to `0` initially (we start at index 0, which we can trivially reach)

    &nbsp;

    **Step 2: Iterate through the array**

    - For each index `i`, first check if `i > max_reach`
    - If yes, we're stuck — we can't even reach this position, so return `false`
    - If no, calculate how far we can reach from here: `i + nums[i]`
    - Update `max_reach` to be the maximum of its current value and `i + nums[i]`
    - This ensures we always track the farthest point we could possibly reach

    &nbsp;

    **Step 3: Return the result**

    - If we complete the loop without getting stuck, return `true`
    - We know we can reach the end because `max_reach` must be at least `n - 1`

    &nbsp;

    The greedy choice at each step (always extend our reach as far as possible) guarantees we find a solution if one exists.

  common_pitfalls:
    - title: Simulating Every Possible Jump Path
      description: |
        A common first instinct is to use recursion or BFS to explore all possible jump sequences. This leads to **exponential time complexity** because from each position, you have up to `nums[i]` choices of where to jump next.

        With `nums.length <= 10^4` and `nums[i] <= 10^5`, this approach will cause a **Time Limit Exceeded (TLE)** error. The greedy approach avoids this by recognising that we only need to track the maximum reach, not every individual path.
      wrong_approach: "Recursively exploring all jump combinations"
      correct_approach: "Track maximum reachable index in single pass"

    - title: Forgetting to Check Reachability Before Updating
      description: |
        A subtle bug occurs when you update `max_reach` without first checking if the current index is reachable. Consider `nums = [0, 2, 3]`:

        - At index 0: `max_reach = 0 + 0 = 0`
        - At index 1: If you don't check reachability, you'd calculate `max_reach = 1 + 2 = 3`
        - But index 1 was never reachable from index 0!

        Always check `i <= max_reach` before processing position `i`.
      wrong_approach: "Update max_reach without checking if current index is reachable"
      correct_approach: "Check i <= max_reach before processing each position"

    - title: Off-by-One Errors with Array Length
      description: |
        Remember that the goal is to reach the **last index** (position `n - 1`), not to jump beyond the array. Your condition should check whether `max_reach >= n - 1`, not `max_reach >= n`.

        For a single-element array `[0]`, you're already at the last index, so the answer is `true` even though you can't jump anywhere.

  key_takeaways:
    - "**Greedy reachability**: When you only need to know *if* a destination is reachable (not *how*), tracking the maximum reachable position is often sufficient"
    - "**Single-pass efficiency**: By maintaining running state (`max_reach`), we avoid expensive path enumeration and achieve O(n) time"
    - "**Foundation for Jump Game II**: This problem extends to finding the *minimum* number of jumps (LeetCode #45), which uses a similar greedy interval approach"
    - "**Early termination**: The greedy approach allows us to return `false` as soon as we detect we're stuck, avoiding unnecessary computation"

  time_complexity: "O(n). We traverse the array exactly once, performing constant-time operations at each index."
  space_complexity: "O(1). We only use a single variable (`max_reach`) regardless of input size."

solutions:
  - approach_name: Greedy (Maximum Reach)
    is_optimal: true
    code: |
      def can_jump(nums: list[int]) -> bool:
          # Track the farthest index we can reach
          max_reach = 0

          for i in range(len(nums)):
              # If current index is beyond our reach, we're stuck
              if i > max_reach:
                  return False

              # Update max reach from current position
              # We can jump up to nums[i] steps from index i
              max_reach = max(max_reach, i + nums[i])

          # If we processed all indices, we can reach the end
          return True
    explanation: |
      **Time Complexity:** O(n) — Single pass through the array.

      **Space Complexity:** O(1) — Only one variable used.

      We iterate through each index, checking if it's reachable and updating our maximum reach. If we ever find ourselves at an unreachable position, we return `false`. Otherwise, completing the loop means the end is reachable.

  - approach_name: Greedy (Backward)
    is_optimal: true
    code: |
      def can_jump(nums: list[int]) -> bool:
          # Start with the goal at the last index
          goal = len(nums) - 1

          # Work backwards through the array
          for i in range(len(nums) - 2, -1, -1):
              # If we can reach the goal from position i,
              # then position i becomes our new goal
              if i + nums[i] >= goal:
                  goal = i

          # If goal moved all the way to index 0, we can reach the end
          return goal == 0
    explanation: |
      **Time Complexity:** O(n) — Single pass through the array (backwards).

      **Space Complexity:** O(1) — Only one variable used.

      This alternative greedy approach works backwards from the end. We ask: "What's the leftmost position that can reach my current goal?" Each time we find such a position, it becomes the new goal. If the goal reaches index 0, we know the end is reachable from the start.

  - approach_name: Dynamic Programming
    is_optimal: false
    code: |
      def can_jump(nums: list[int]) -> bool:
          n = len(nums)
          # dp[i] indicates whether index i is reachable
          dp = [False] * n
          dp[0] = True  # Starting position is always reachable

          for i in range(n):
              # Skip unreachable positions
              if not dp[i]:
                  continue

              # Mark all positions reachable from i
              for j in range(1, nums[i] + 1):
                  if i + j < n:
                      dp[i + j] = True

              # Early exit if we've reached the end
              if dp[n - 1]:
                  return True

          return dp[n - 1]
    explanation: |
      **Time Complexity:** O(n × max(nums[i])) — For each position, we may mark up to `nums[i]` subsequent positions.

      **Space Complexity:** O(n) — Boolean array tracking reachability of each position.

      This DP approach explicitly tracks which positions are reachable. While correct, it's slower than the greedy approach because it does redundant work marking positions that have already been marked. Included to illustrate the progression from DP thinking to greedy optimisation.
