Files
codetutor/backend/data/questions/jump-game-ii.yaml

204 lines
9.2 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title: Jump Game II
slug: jump-game-ii
difficulty: medium
leetcode_id: 45
leetcode_url: https://leetcode.com/problems/jump-game-ii/
categories:
- arrays
- dynamic-programming
patterns:
- slug: greedy
is_optimal: true
function_signature: "def jump(nums: list[int]) -> int:"
test_cases:
visible:
- input: { nums: [2, 3, 1, 1, 4] }
expected: 2
- input: { nums: [2, 3, 0, 1, 4] }
expected: 2
- input: { nums: [1] }
expected: 0
hidden:
- input: { nums: [1, 2, 3] }
expected: 2
- input: { nums: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 0] }
expected: 2
- input: { nums: [1, 1, 1, 1, 1] }
expected: 4
- input: { nums: [5, 6, 4, 4, 6, 9, 4, 4, 7, 4, 4, 8, 2, 6, 8, 1, 5, 9, 6, 5, 2, 7, 9, 7, 9, 6, 9, 4, 1, 6, 8, 8, 4, 4, 2, 0, 3, 8, 5] }
expected: 5
- input: { nums: [7, 0, 9, 6, 9, 6, 1, 7, 9, 0, 1, 2, 9, 0, 3] }
expected: 2
- input: { nums: [2, 1] }
expected: 1
description: |
You are given a **0-indexed** array of integers `nums` of length `n`. You are initially positioned at index `0`.
Each element `nums[i]` represents the maximum length of a forward jump from index `i`. In other words, if you are at index `i`, you can jump to any index `i + j` where:
- `0 <= j <= nums[i]` and
- `i + j < n`
Return *the minimum number of jumps to reach index* `n - 1`. The test cases are generated such that you can reach index `n - 1`.
constraints: |
- `1 <= nums.length <= 10^4`
- `0 <= nums[i] <= 1000`
- It's guaranteed that you can reach `nums[n - 1]`
examples:
- input: "nums = [2,3,1,1,4]"
output: "2"
explanation: "The minimum number of jumps to reach the last index is 2. Jump 1 step from index 0 to 1, then 3 steps to the last index."
- input: "nums = [2,3,0,1,4]"
output: "2"
explanation: "Jump 1 step from index 0 to 1, then 3 steps to the last index."
explanation:
intuition: |
Imagine you're standing at the start of a path with numbered tiles, and each tile tells you the maximum distance you can leap forward. Your goal is to reach the end in as few jumps as possible.
Think of it like a **level-based exploration**: from your current position, you can reach a range of tiles. Within that range, you want to pick the tile that lets you jump the *farthest* on your next move. This is the **greedy insight** — at each "level" (jump), choose the landing spot that maximises your future reach.
Visualise it as expanding waves: your first jump creates a "wave" of reachable positions. From all positions in that wave, you determine how far the next wave can extend. Each wave represents one jump.
The key observation is that you don't need to try every possible path. By always tracking the farthest point reachable within your current jump's range, you guarantee the minimum number of jumps. This works because reaching farther never hurts — a farther position can reach everything a closer position can, plus more.
approach: |
We solve this using a **Greedy (BFS-like) Approach**:
**Step 1: Handle edge cases**
- If the array has only one element, we're already at the destination — return `0` jumps
&nbsp;
**Step 2: Initialise tracking variables**
- `jumps`: Counter for the number of jumps made, starting at `0`
- `current_end`: The farthest index reachable with the current number of jumps (initially `0`)
- `farthest`: The farthest index we can reach from any position within the current range (initially `0`)
&nbsp;
**Step 3: Iterate through the array**
- For each index `i` from `0` to `n - 2` (we don't need to process the last index):
- Update `farthest` to be the maximum of `farthest` and `i + nums[i]`
- When we reach `current_end` (the boundary of our current jump range):
- Increment `jumps` — we must take another jump
- Update `current_end` to `farthest` — this is now our new reachable boundary
- If `current_end` reaches or exceeds the last index, we can stop
&nbsp;
**Step 4: Return the result**
- Return `jumps` after processing the array
&nbsp;
This approach works because we're essentially doing a BFS level by level. Each "level" represents positions reachable in exactly `k` jumps. We greedily extend to the farthest reachable point at each level, ensuring minimum jumps.
common_pitfalls:
- title: Using Dynamic Programming When Greedy Suffices
description: |
A natural first approach is DP: let `dp[i]` be the minimum jumps to reach index `i`. For each position, check all positions that can reach it.
While correct, this is **O(n^2) time complexity**. For `n = 10^4`, this means up to 100 million operations, which may cause TLE.
The greedy approach achieves **O(n)** by recognising that we don't need to track exact paths — just the farthest reachable point at each jump level.
wrong_approach: "DP with O(n^2) transitions"
correct_approach: "Greedy tracking of reachable range per jump"
- title: Processing the Last Index
description: |
A subtle bug occurs when iterating through all indices including `n - 1`. If the last index happens to equal `current_end`, you'd incorrectly count an extra jump.
We only need to iterate to `n - 2`. Once we know we can reach the last index, we're done. Processing the last index itself is unnecessary and can inflate the jump count.
wrong_approach: "Iterating i from 0 to n - 1"
correct_approach: "Iterating i from 0 to n - 2"
- title: Forgetting to Update Farthest Before Checking Boundary
description: |
The order of operations matters. You must update `farthest = max(farthest, i + nums[i])` *before* checking if `i == current_end`.
If you check the boundary first and then update farthest, you miss accounting for the current position's reach, potentially getting a wrong answer.
wrong_approach: "Check boundary, then update farthest"
correct_approach: "Update farthest, then check boundary"
key_takeaways:
- "**Greedy as implicit BFS**: When optimising for minimum steps in reachability problems, think of expanding 'waves' or 'levels' of positions reachable in k jumps"
- "**Track ranges, not paths**: Instead of enumerating all possible paths (exponential), track the reachable range at each step (linear)"
- "**Foundation for Jump Game variants**: This pattern extends to problems with obstacles, costs, or different movement rules"
- "**Recognise when DP is overkill**: If the problem has optimal substructure but greedy choice works, prefer the simpler O(n) greedy solution"
time_complexity: "O(n). We traverse the array exactly once, processing each element in constant time."
space_complexity: "O(1). We only use three variables (`jumps`, `current_end`, `farthest`), regardless of input size."
solutions:
- approach_name: Greedy (BFS-like)
is_optimal: true
code: |
def jump(nums: list[int]) -> int:
n = len(nums)
# Already at destination
if n <= 1:
return 0
jumps = 0 # Number of jumps taken
current_end = 0 # Farthest we can reach with current jumps
farthest = 0 # Farthest we can reach from positions in current range
# Don't process last index - we just need to reach it
for i in range(n - 1):
# Update the farthest point reachable from current position
farthest = max(farthest, i + nums[i])
# Reached the end of current jump's range
if i == current_end:
jumps += 1 # Must take another jump
current_end = farthest # Extend range to farthest reachable
# Early exit if we can reach the end
if current_end >= n - 1:
break
return jumps
explanation: |
**Time Complexity:** O(n) — Single pass through the array.
**Space Complexity:** O(1) — Only three integer variables used.
We simulate a BFS where each "level" represents positions reachable with the same number of jumps. At each level, we track the farthest position we can reach, then "jump" to extend our range. The greedy choice of always extending to the farthest point guarantees minimum jumps.
- approach_name: Dynamic Programming
is_optimal: false
code: |
def jump(nums: list[int]) -> int:
n = len(nums)
# dp[i] = minimum jumps to reach index i
dp = [float('inf')] * n
dp[0] = 0 # Start position needs 0 jumps
for i in range(n):
# Skip unreachable positions
if dp[i] == float('inf'):
continue
# Update all positions reachable from i
for j in range(1, nums[i] + 1):
if i + j < n:
dp[i + j] = min(dp[i + j], dp[i] + 1)
return dp[n - 1]
explanation: |
**Time Complexity:** O(n × m) where m is the average jump length — can be O(n^2) in worst case.
**Space Complexity:** O(n) — DP array storing minimum jumps to each index.
For each position, we update all positions reachable from it. While correct, this is slower than the greedy approach because we're doing redundant work. Included to illustrate why greedy is preferred when it works.