208. Jump Game
π LeetCode Problem: 55. Jump Game
π Difficulty: Medium
π·οΈ Topics: Dynamic Programming, Array, Greedy, 1D DP
Problem Statement
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.
Example 1:
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.
Example 2:
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.
Constraints:
- 1 <= nums.length <= 10^4
- 0 <= nums[i] <= 10^5
π³ Visual Understanding - The Jump Problem
The Problem We're Solving:
Array: [2, 3, 1, 1, 4]
Index: 0 1 2 3 4
Start at index 0 (value = 2)
Goal: Reach index 4 (last index)
At each position i:
- nums[i] = maximum jump length
- Can jump 1, 2, 3, ..., up to nums[i] steps forward
Question: Can we reach the last index? π€
Understanding Jump Lengths:
nums = [2, 3, 1, 1, 4]
At index 0 (value = 2):
Can jump: 1 or 2 steps
Reachable indices: 1, 2
At index 1 (value = 3):
Can jump: 1, 2, or 3 steps
Reachable indices: 2, 3, 4 β (can reach end!)
At index 2 (value = 1):
Can jump: 1 step
Reachable indices: 3
At index 3 (value = 1):
Can jump: 1 step
Reachable indices: 4 β
At index 4 (value = 4):
Already at end! β
All Possible Paths for Example 1:
nums = [2, 3, 1, 1, 4]
Path 1: 0 β 1 β 4
ββββββββββββββββββ
Start: index 0
Jump 1 step β index 1
Jump 3 steps β index 4 β
Path 2: 0 β 1 β 2 β 3 β 4
βββββββββββββββββββββββββ
Start: index 0
Jump 1 step β index 1
Jump 1 step β index 2
Jump 1 step β index 3
Jump 1 step β index 4 β
Path 3: 0 β 2 β 3 β 4
ββββββββββββββββββββββ
Start: index 0
Jump 2 steps β index 2
Jump 1 step β index 3
Jump 1 step β index 4 β
Multiple paths exist! Answer: true β
Failed Example (Example 2):
nums = [3, 2, 1, 0, 4]
Index: 0 1 2 3 4
Trying all paths:
Path 1: 0 β 1 β ... ?
Index 0: jump 1 β index 1 (value=2)
Index 1: jump 1 β index 2 (value=1)
Index 2: jump 1 β index 3 (value=0) β οΈ
Index 3: value=0 β STUCK! Cannot jump! β
Path 2: 0 β 2 β ... ?
Index 0: jump 2 β index 2 (value=1)
Index 2: jump 1 β index 3 (value=0) β οΈ
Index 3: value=0 β STUCK! Cannot jump! β
Path 3: 0 β 3 β ... ?
Index 0: jump 3 β index 3 (value=0) β οΈ
Index 3: value=0 β STUCK! Cannot jump! β
All paths lead to index 3 (the TRAP!)
Cannot reach index 4! Answer: false β
Visual:
[3, 2, 1, 0, 4]
β
TRAP!
Every path gets stuck at index 3!
π§ The AHA Moment - Why Dynamic Programming?
Key Observation:
To know if we can reach the last index:
- We need to know if we can reach intermediate indices
- If we can reach index i, check all jumps from i
Recursive thinking:
canReach(lastIndex) = true if we can reach any index j where:
- j < lastIndex
- canReach(j) = true
- j + nums[j] >= lastIndex (can jump from j to last)
This is a reachability problem! π―
Why Not Greedy?
Initial thought: "Always jump the maximum distance"
nums = [2, 3, 1, 1, 4]
Greedy approach:
At index 0: jump max (2 steps) β index 2
At index 2: jump max (1 step) β index 3
At index 3: jump max (1 step) β index 4 β
Greedy works here! But...
Counter-example: nums = [5, 4, 0, 2, 0, 1, 0, 1, 0]
Greedy:
At index 0: jump max (5 steps) β index 5 (value=1)
At index 5: jump max (1 step) β index 6 (value=0)
At index 6: STUCK! β
Optimal:
At index 0: jump 1 step β index 1 (value=4)
At index 1: jump 4 steps β index 5 β ... β index 8 β
Wait... actually for THIS problem, a modified greedy works!
But DP is more general and helps understand the structure! π―
Why Dynamic Programming?
1. OVERLAPPING SUBPROBLEMS:
To check if we can reach index 5:
- Check if we can reach 4, and jump from 4 to 5
- Check if we can reach 3, and jump from 3 to 5
- Check if we can reach 2, and jump from 2 to 5
Many paths might revisit same indices! β οΈ
2. OPTIMAL SUBSTRUCTURE:
canReach(n) depends on canReach(0), canReach(1), ..., canReach(n-1)
If we can reach index i, and i + nums[i] >= n:
β We can reach n! β
These properties = Perfect for DP! π―
Note: This problem also has an O(n) greedy solution!
But DP approach teaches the framework! π‘
π΄ Approach 1: Brute Force (Recursion)
π Function Definition
Function Signature:
boolean canJump(int[] nums)
boolean canJumpFromPosition(int position, int[] nums)
What it represents:
Input Parameter position:
- Current index in the array
- Where we are right now
- Example: position=2 means "currently at index 2"
Input Parameter nums:
- The jump array
- nums[i] = max jump length at index i
Return Value (boolean):
- true = Can reach the last index FROM current position
- false = Cannot reach the last index FROM current position
- Example: canJumpFromPosition(0, nums) asks "can reach end from start?"
Purpose: - Check if we can reach last index from given position - Try all possible jump lengths from current position - Recursive exploration of all paths
Key Understanding:
canJumpFromPosition(2, [2,3,1,1,4]) asks:
"Starting from index 2, can I reach index 4?"
Process:
- At index 2, nums[2] = 1
- Can jump 1 step β check canJumpFromPosition(3, nums)
- If ANY jump reaches the end β return true
π‘ Intuition
From current position, try ALL possible jumps:
- Jump 1 step, 2 steps, 3 steps, ..., up to nums[position] steps
- For each jump, check if we can reach the end from new position
- If ANY jump works β return true
- If NO jump works β return false
Base cases:
- position == lastIndex: Already at end β return true
- position >= nums.length: Out of bounds β return false
Recursion explores all possible paths!
π Implementation
class Solution {
public boolean canJump(int[] nums) {
return canJumpFromPosition(0, nums);
}
private boolean canJumpFromPosition(int position, int[] nums) {
// Base case: reached the last index
if (position == nums.length - 1) {
return true;
}
// Try all possible jumps from current position
int maxJump = Math.min(position + nums[position], nums.length - 1);
for (int nextPosition = position + 1; nextPosition <= maxJump; nextPosition++) {
if (canJumpFromPosition(nextPosition, nums)) {
return true; // Found a path to the end!
}
}
// No jump from this position can reach the end
return false;
}
}
π Dry Run Example: nums = [2, 3, 1, 1, 4]
canJumpFromPosition(0, [2,3,1,1,4])
ββ position=0, nums[0]=2, lastIndex=4
ββ maxJump = min(0+2, 4) = 2
ββ Try jumps: 1, 2
β
ββ Try nextPosition=1:
β ββ canJumpFromPosition(1, nums)
β β ββ position=1, nums[1]=3, lastIndex=4
β β ββ maxJump = min(1+3, 4) = 4
β β ββ Try jumps: 2, 3, 4
β β β
β β ββ Try nextPosition=2:
β β β ββ canJumpFromPosition(2, nums)
β β β β ββ position=2, nums[2]=1, lastIndex=4
β β β β ββ maxJump = min(2+1, 4) = 3
β β β β ββ Try jump: 3
β β β β β
β β β β ββ Try nextPosition=3:
β β β β β ββ canJumpFromPosition(3, nums)
β β β β β β ββ position=3, nums[3]=1, lastIndex=4
β β β β β β ββ maxJump = min(3+1, 4) = 4
β β β β β β ββ Try jump: 4
β β β β β β β
β β β β β β ββ Try nextPosition=4:
β β β β β β β ββ position=4 == lastIndex β return true β
β β β β β β β
β β β β β β ββ return true
β β β β β β
β β β β β ββ return true
β β β β β
β β β β ββ return true
β β β β
β β β ββ return true
β β β
β β ββ return true (found path!)
β β
β ββ return true
β
ββ return true
Result: true β (can reach last index)
π Complexity Analysis
Time Complexity: O(2^n)
At each position, try multiple jumps
Each creates a branch in recursion tree
Worst case: exponential branches
For nums = [1,1,1,1,1]:
Position 0 β try 1
Position 1 β try 1
...
Creates long chain, but with backtracking
For nums = [n,n,n,...]:
Each position tries n jumps
Exponential explosion! π₯
Space Complexity: O(n)
Recursion stack depth = n (worst case)
Maximum n calls on stack at once
Why This Fails:
β Exponential time - too slow for n=10^4
β Recalculates same positions many times
β Times out on large inputs
β
But shows the recursive structure!
π‘ Approach 2: Top-Down (Memoization)
π Function Definition
Function Signature:
boolean canJumpFromPosition(int position, int[] nums, Boolean[] memo)
What it represents:
Input Parameter position:
- Current index in the array
- Starting point for this subproblem
- Example: position=3 means "currently at index 3"
Input Parameter nums:
- The jump array
- Unchanged throughout recursion
Input Parameter memo:
- Memoization cache (array of size n)
- memo[i] = can we reach the end FROM index i?
- null = not yet calculated
- true = can reach end from index i
- false = cannot reach end from index i
Return Value (boolean):
- true = Can reach last index FROM position
- false = Cannot reach last index FROM position
Purpose: - Same as brute force BUT with caching - Store results to avoid recalculation - Each position calculated at most once
Key Understanding:
canJumpFromPosition(2, nums, memo) asks:
"Can I reach the end from index 2?"
First call:
- memo[2] = null (not calculated)
- Calculate the answer
- Store in memo[2]
- Return result
Second call:
- memo[2] already has answer
- Return immediately (no recalculation!)
π‘ Intuition - The Clever Conversion
PROBLEM IDENTIFIED in Brute Force:
Same positions checked multiple times
SOLUTION:
Cache results the FIRST time we calculate them
Next time β instant lookup! π―
HOW TO IDENTIFY WHAT TO MEMOIZE:
1. Function parameter: position
2. Same position always gives same answer
3. Memoize using position as key
4. Key range: 0 to n-1
CONVERSION FROM BRUTE FORCE:
1. Add memo array: Boolean[n]
2. Check cache before calculating
3. Store result before returning
4. Same logic, just add caching!
π― Base Cases Transformation
BRUTE FORCE BASE CASES:
if (position == nums.length - 1) return true;
TOP-DOWN BASE CASES (SAME!):
if (position == nums.length - 1) return true;
Why same?
- Base case: "already at the end"
- Memoization doesn't change the logic
ONLY ADDITIONS:
1. Create memo array: Boolean[n]
2. Check memo[position] before calculating
3. Store memo[position] before returning
π Implementation
class Solution {
public boolean canJump(int[] nums) {
Boolean[] memo = new Boolean[nums.length];
return canJumpFromPosition(0, nums, memo);
}
private boolean canJumpFromPosition(int position, int[] nums, Boolean[] memo) {
// Base case: reached the last index
if (position == nums.length - 1) {
return true;
}
// Check if already calculated
if (memo[position] != null) {
return memo[position]; // Cache HIT!
}
// Try all possible jumps from current position
int maxJump = Math.min(position + nums[position], nums.length - 1);
for (int nextPosition = position + 1; nextPosition <= maxJump; nextPosition++) {
if (canJumpFromPosition(nextPosition, nums, memo)) {
memo[position] = true; // Store before returning
return true;
}
}
// No jump from this position can reach the end
memo[position] = false; // Store before returning
return false;
}
}
π Detailed Dry Run: nums = [2, 3, 1, 1, 4]
Input: nums = [2, 3, 1, 1, 4]
n = 5, lastIndex = 4
Initial State:
memo = [null, null, null, null, null]
β β β β β
0 1 2 3 4
ββββββββββββββββββββββββββββββββββββββββββββββββ
Call: canJumpFromPosition(0, nums, memo)
ββββββββββββββββββββββββββββββββββββββββββββββββ
Step 1: position=0
Not at end (0 β 4)
memo[0] = null β CACHE MISS
maxJump = min(0+2, 4) = 2
Try positions: 1, 2
Try position=1 β canJumpFromPosition(1, nums, memo)
Step 2: position=1
Not at end (1 β 4)
memo[1] = null β CACHE MISS
maxJump = min(1+3, 4) = 4
Try positions: 2, 3, 4
Try position=2 β canJumpFromPosition(2, nums, memo)
Step 3: position=2
Not at end (2 β 4)
memo[2] = null β CACHE MISS
maxJump = min(2+1, 4) = 3
Try positions: 3
Try position=3 β canJumpFromPosition(3, nums, memo)
Step 4: position=3
Not at end (3 β 4)
memo[3] = null β CACHE MISS
maxJump = min(3+1, 4) = 4
Try positions: 4
Try position=4 β canJumpFromPosition(4, nums, memo)
Step 5: position=4
position == lastIndex (4 == 4) β BASE CASE
Return true
Back to Step 4
Step 6: Back at position=3
Got true from position=4
Store: memo[3] = true β
Return true
memo = [null, null, null, true, null]
Back to Step 3
Step 7: Back at position=2
Got true from position=3
Store: memo[2] = true β
Return true
memo = [null, null, true, true, null]
Back to Step 2
Step 8: Back at position=1
Got true from position=2
Store: memo[1] = true β
Return true
memo = [null, true, true, true, null]
Back to Step 1
Step 9: Back at position=0
Got true from position=1
Store: memo[0] = true β
Return true
Final memo = [true, true, true, true, null]
ββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT: true (can reach last index) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Understanding memo:
memo[0] = true β "Can reach end from index 0"
memo[1] = true β "Can reach end from index 1"
memo[2] = true β "Can reach end from index 2"
memo[3] = true β "Can reach end from index 3"
memo[4] = null β "Never checked (already at end)"
π Complexity Analysis
Time Complexity: O(nΒ²)
Each position calculated at most once: O(n)
For each position, try up to n jumps: O(n)
Total: O(n) Γ O(n) = O(nΒ²)
Much better than O(2^n)! π
Space Complexity: O(n)
Memoization array: O(n)
Recursion stack: O(n)
Total: O(n) + O(n) = O(n)
π’ Approach 3: Bottom-Up (Tabulation)
π Function Definition
DP Array Definition:
boolean[] dp = new boolean[n];
What dp[i] represents:
Index i:
- Position in the array
- Example: i=2 means "index 2"
Value dp[i] (boolean):
- dp[i] = true β Can reach the LAST index FROM index i
- dp[i] = false β Cannot reach the LAST index FROM index i
- Example: dp[0] = true means "can reach end from start"
Purpose: - Store reachability for each position - Build from end to start (backwards) - Bottom-up: start from last index, build to index 0
Key Understanding:
dp[3] = true means:
"Starting from index 3, I CAN reach the last index"
dp[2] = false means:
"Starting from index 2, I CANNOT reach the last index"
Building backwards ensures we know reachability
of later positions before checking earlier ones!
π‘ Intuition - The Clever Conversion from Top-Down
TOP-DOWN (Recursion + Memoization):
Start from index 0 β recurse forward β cache results
BOTTOM-UP (Iteration + Tabulation):
Start from last index β iterate backwards to index 0
dp[n-1] = true (already at end)
β build backwards
dp[n-2] = can jump to any dp[j]=true where j > n-2?
β build backwards
dp[n-3] = can jump to any dp[j]=true where j > n-3?
β build backwards
...
β build backwards
dp[0] = answer!
Direction: END β backwards to START
WHY BACKWARDS?
To check if we can reach end from position i:
- Need to know reachability of positions i+1, i+2, ...
- Build from end ensures later positions are ready!
π― Base Cases Transformation - VERY IMPORTANT! π
TOP-DOWN BASE CASES (in recursion):
if (position == n-1) return true;
Termination condition: "reached the end"
BOTTOM-UP BASE CASES (in DP table):
dp[n-1] = true;
Initialization: "last position is reachable from itself"
KEY INSIGHT - THE CLEVER THINKING:
1. Top-down: "When do I stop recursing?"
β At last index, return true
2. Bottom-up: "What's the known base case?"
β dp[n-1] = true, build backwards
3. Same value (true), different role:
- Top-down: where recursion STOPS
- Bottom-up: where iteration STARTS
π Recurrence Relation
Definition:
dp[i] = can we reach the last index FROM index i?
For position i:
- Can jump 1, 2, ..., nums[i] steps
- Check all reachable positions j = i+1, i+2, ..., i+nums[i]
- If ANY dp[j] = true β dp[i] = true
Formula:
dp[i] = true if EXISTS j: (i < j <= i+nums[i]) AND dp[j]=true
dp[i] = false otherwise
Dependencies:
dp[i] depends on dp[i+1], dp[i+2], ..., dp[n-1]
(all positions reachable from i)
Must build BACKWARDS: n-1, n-2, ..., 1, 0
π Implementation
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
// Create DP table
// dp[i] = can we reach the end FROM index i?
boolean[] dp = new boolean[n];
// Base case: last position is reachable from itself
dp[n - 1] = true;
// Build backwards from second-last to first
for (int i = n - 2; i >= 0; i--) {
// Check all positions we can jump to from i
int farthestJump = Math.min(i + nums[i], n - 1);
for (int j = i + 1; j <= farthestJump; j++) {
if (dp[j]) {
dp[i] = true; // Found reachable position!
break; // No need to check further
}
}
}
// Answer: can we reach end from start?
return dp[0];
}
}
π Detailed Dry Run: nums = [2, 3, 1, 1, 4]
Input: nums = [2, 3, 1, 1, 4]
n = 5, lastIndex = 4
ββββββββββββββββββββββββββββββββββββββββββββββββ
INITIALIZATION
ββββββββββββββββββββββββββββββββββββββββββββββββ
Create DP table:
dp = [false, false, false, false, false]
β β β β β
0 1 2 3 4
Base case:
dp[4] = true (last position reachable from itself)
dp = [false, false, false, false, true]
β
base case β
ββββββββββββββββββββββββββββββββββββββββββββββββ
ITERATION - Building backwards
ββββββββββββββββββββββββββββββββββββββββββββββββ
Iteration 1: i = 3
ββββββββββββββββ
Position 3, nums[3] = 1
Can jump 1 step
farthestJump = min(3+1, 4) = 4
Check positions: 4
dp[4] = true β Found reachable position!
dp[3] = true
dp = [false, false, false, true, true]
Iteration 2: i = 2
ββββββββββββββββ
Position 2, nums[2] = 1
Can jump 1 step
farthestJump = min(2+1, 4) = 3
Check positions: 3
dp[3] = true β Found reachable position!
dp[2] = true
dp = [false, false, true, true, true]
Iteration 3: i = 1
ββββββββββββββββ
Position 1, nums[1] = 3
Can jump 1, 2, or 3 steps
farthestJump = min(1+3, 4) = 4
Check positions: 2, 3, 4
dp[2] = true β Found reachable position!
dp[1] = true
dp = [false, true, true, true, true]
Iteration 4: i = 0 (FINAL)
βββββββββββββββββββββββββ
Position 0, nums[0] = 2
Can jump 1 or 2 steps
farthestJump = min(0+2, 4) = 2
Check positions: 1, 2
dp[1] = true β Found reachable position!
dp[0] = true
Final DP table:
dp = [true, true, true, true, true]
β
ANSWER!
ββββββββββββββββββββββββββββββββββββββββββββββββ
COMPLETE DP TABLE VISUALIZATION
ββββββββββββββββββββββββββββββββββββββββββββββββ
Index: 0 1 2 3 4
ββββββ¬βββββ¬βββββ¬βββββ¬βββββ
dp[] βtrueβtrueβtrueβtrueβtrueβ
ββββββ΄βββββ΄βββββ΄βββββ΄βββββ
β β
Answer Base case
Meaning of each value:
dp[0] = true β "Can reach end from index 0" β
dp[1] = true β "Can reach end from index 1" β
dp[2] = true β "Can reach end from index 2" β
dp[3] = true β "Can reach end from index 3" β
dp[4] = true β "Already at end" (base case)
ββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT: true (can reach last index from start) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
π Complexity Analysis
Time Complexity: O(nΒ²)
Loop from n-2 to 0: O(n)
For each position, check up to n jumps: O(n)
Total: O(n) Γ O(n) = O(nΒ²)
Space Complexity: O(n)
DP table of size n: O(n)
No recursion stack: O(1)
Total: O(n)
π΅ Approach 4: Space-Optimized (Greedy)
π Function Definition
Function Signature:
boolean canJump(int[] nums)
What it represents:
Input Parameter nums:
- The jump array
- nums[i] = max jump length at index i
Return Value (boolean):
- true = Can reach the last index from start
- false = Cannot reach the last index from start
Purpose: - Instantly determine reachability using greedy approach - Track the farthest position reachable - O(n) time, O(1) space solution
Key Understanding:
Instead of checking every position:
Track maxReach = farthest index we can reach
At each position i:
- If i > maxReach β unreachable β return false
- Update maxReach = max(maxReach, i + nums[i])
If maxReach >= lastIndex at any point β return true
Greedy: Always track maximum reach possible!
π‘ Intuition - Greedy Optimization
KEY INSIGHT from DP:
We only care if we CAN reach the end
Don't need to know exact path!
GREEDY IDEA:
Track the farthest position we can reach
maxReach = maximum index reachable so far
At each position i:
- If i > maxReach β can't reach this position β return false
- Update: maxReach = max(maxReach, i + nums[i])
- If maxReach >= lastIndex β found path β return true
WHY THIS WORKS:
If we can reach position i:
- We explored all positions 0 to i
- maxReach represents farthest we can go
- If maxReach >= lastIndex β we win!
If we can't reach position i:
- i > maxReach means there's a gap
- No way to reach i or beyond β return false
OPTIMIZATION:
- No DP table needed
- Single pass through array
- O(1) space! π
π Implementation
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
int maxReach = 0; // Farthest position we can reach
for (int i = 0; i < n; i++) {
// If current position is beyond max reach β unreachable
if (i > maxReach) {
return false;
}
// Update max reach from current position
maxReach = Math.max(maxReach, i + nums[i]);
// Early exit: if we can reach or pass the last index
if (maxReach >= n - 1) {
return true;
}
}
return true;
}
}
π Detailed Dry Run: nums = [2, 3, 1, 1, 4]
Input: nums = [2, 3, 1, 1, 4]
n = 5, lastIndex = 4
ββββββββββββββββββββββββββββββββββββββββββββββββ
INITIALIZATION
ββββββββββββββββββββββββββββββββββββββββββββββββ
maxReach = 0 (initially can only reach index 0)
ββββββββββββββββββββββββββββββββββββββββββββββββ
ITERATION
ββββββββββββββββββββββββββββββββββββββββββββββββ
Iteration 1: i = 0
ββββββββββββββββ
Position 0, nums[0] = 2
Check: i (0) > maxReach (0)? No β (can reach this position)
Update maxReach:
maxReach = max(0, 0 + 2) = 2
Check: maxReach (2) >= lastIndex (4)? No
State: maxReach = 2
Meaning: "Can reach up to index 2"
Iteration 2: i = 1
ββββββββββββββββ
Position 1, nums[1] = 3
Check: i (1) > maxReach (2)? No β (can reach this position)
Update maxReach:
maxReach = max(2, 1 + 3) = 4
Check: maxReach (4) >= lastIndex (4)? Yes! β
Early exit: We can reach the last index!
ββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT: true (can reach last index) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Why we stopped early:
At index 1, we discovered maxReach = 4
This means we can reach index 4 (the end)
No need to check remaining positions!
π Dry Run - Failing Case: nums = [3, 2, 1, 0, 4]
Input: nums = [3, 2, 1, 0, 4]
n = 5, lastIndex = 4
ββββββββββββββββββββββββββββββββββββββββββββββββ
maxReach = 0
Iteration 1: i = 0
nums[0] = 3
i (0) <= maxReach (0) β
maxReach = max(0, 0+3) = 3
maxReach (3) < lastIndex (4)
Iteration 2: i = 1
nums[1] = 2
i (1) <= maxReach (3) β
maxReach = max(3, 1+2) = 3
maxReach (3) < lastIndex (4)
Iteration 3: i = 2
nums[2] = 1
i (2) <= maxReach (3) β
maxReach = max(3, 2+1) = 3
maxReach (3) < lastIndex (4)
Iteration 4: i = 3
nums[3] = 0
i (3) <= maxReach (3) β
maxReach = max(3, 3+0) = 3
maxReach (3) < lastIndex (4)
Iteration 5: i = 4
i (4) > maxReach (3)? Yes! β
Cannot reach position 4!
Return false
ββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT: false (cannot reach last index) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Why it failed:
maxReach stuck at 3 (the trap position)
Position 3 has value 0 β can't jump further
Position 4 is beyond maxReach β unreachable!
π Complexity Analysis
Time Complexity: O(n)
Single pass through array
Each position visited once
Total: O(n) β
MASSIVE improvement from O(nΒ²)! π
Space Complexity: O(1)
Only one variable: maxReach
No array, no recursion
Constant space! β
Improvement from O(n) to O(1)! π
π¨ Building Greedy Intuition - Step by Step
Let's understand WHY greedy works through discovery, not just code!
The Core Question We're Asking:
Instead of asking: "Can I reach the END from position i?"
Ask this instead: "What's the FURTHEST I can possibly reach?"
This mindset shift is KEY! π
Visual Discovery - Example 1
nums = [2, 3, 1, 1, 4]
Index: 0 1 2 3 4
Let's track "furthest reachable position" as we scan left to right:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 0 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
At index 0:
Value: 2
Can jump: 1 or 2 steps
Reachable: indices 1, 2
furthest = 0 + 2 = 2
Visual:
[0, 1, 2, 3, 4]
β βββ
|_____|
Can reach up to here!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 1 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
At index 1:
Value: 3
Can jump: 1, 2, or 3 steps
From index 1, can reach: 2, 3, 4
Calculate: 1 + 3 = 4
furthest = max(2, 4) = 4 β
Visual:
[0, 1, 2, 3, 4]
β βββββββ
|___________|
Extended our reach to the END!
Key insight:
Position 1 is WITHIN our reach (1 β€ 2)
From position 1, we can extend reach to 4
Since 4 is the last index β WE CAN REACH THE END! β
We don't need to check positions 2, 3, 4 anymore!
Already know we can reach them!
Result: true β
Understanding "furthest"
Think of it like a FLASHLIGHT:
At each position, our flashlight shines further or stays same:
Position 0: [ββββββββ] (can see up to index 2)
Position 1: [ββββββββ] (can see up to index 4 - THE END!)
The key questions:
1. "Can I reach this position?" β Is position β€ furthest?
2. "From here, how far can my light extend?" β position + nums[position]
3. "Did I reach the end?" β Is furthest β₯ lastIndex?
That's it! Three simple checks! π―
Why This Works - The "NO GAPS" Property
CRITICAL INSIGHT:
If furthest = 5, it means:
β We can reach index 0 (start)
β We can reach index 1
β We can reach index 2
β We can reach index 3
β We can reach index 4
β We can reach index 5
We can reach ALL positions from 0 to furthest!
Why? Because:
- We started at 0
- At each position i (where i β€ furthest), we checked jumps
- We updated furthest to maximum possible
- This creates a CONTINUOUS REACHABLE RANGE with NO GAPS!
If there was a gap:
Some position i would be > furthest
We'd detect it and return false immediately!
Visual Discovery - Example 2 (Failure Case)
nums = [3, 2, 1, 0, 4]
Index: 0 1 2 3 4
Let's trace furthest:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 0 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
At index 0:
Value: 3
furthest = 0 + 3 = 3
Visual:
[0, 1, 2, 3, 4]
β βββββββ
Can reach up to index 3
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 1 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Check: Is 1 β€ furthest(3)? YES β (reachable)
At index 1:
Value: 2
Calculate: 1 + 2 = 3
furthest = max(3, 3) = 3 (no improvement)
Visual:
[0, 1, 2, 3, 4]
β βββββββ
Still stuck at index 3
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 2 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Check: Is 2 β€ furthest(3)? YES β (reachable)
At index 2:
Value: 1
Calculate: 2 + 1 = 3
furthest = max(3, 3) = 3 (no improvement)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 3 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Check: Is 3 β€ furthest(3)? YES β (barely reachable)
At index 3:
Value: 0 β οΈ
Calculate: 3 + 0 = 3
furthest = max(3, 3) = 3 (STUCK!)
Visual:
[0, 1, 2, 3 | 4]
βββββββββββ β
Can reach TRAP! Can't reach here!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POSITION 4 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Check: Is 4 β€ furthest(3)? NO! β
Position 4 is BEYOND our furthest reach!
There's a GAP - we can't get to position 4!
Result: false β
The TRAP at index 3 (value=0) prevented us from going further!
The "Maximum Reach" Analogy
Think of it like EXPLORING A MAP:
Starting Position: You're at city 0
Goal: Reach city (n-1)
At each city you visit:
- You get a MAP showing how far you can travel from there
- You keep track of the FURTHEST city you know you can reach
- You visit each city in order (0, 1, 2, ...)
Rules:
1. If next city is beyond your "known furthest":
β You CAN'T reach it (there's a gap in your map!)
β Return false
2. If you can reach a city:
β Check its map
β Update your "known furthest" if this city's map extends further
3. If "known furthest" reaches or passes the goal:
β You WIN! Return true
This is EXACTLY what the greedy algorithm does! πΊοΈ
Why We Don't Need DP's Detailed Tracking
DP Approach:
dp[0] = Can reach end from 0? β Check all paths β YES
dp[1] = Can reach end from 1? β Check all paths β YES
dp[2] = Can reach end from 2? β Check all paths β YES
dp[3] = Can reach end from 3? β Check all paths β YES
dp[4] = At end already β YES
Stores: 5 booleans
Checks: Many paths from each position
Greedy Approach:
furthest = Maximum index reachable so far
Just one number! And it tells us EVERYTHING:
furthest = 5 means:
β Can reach end (5 β₯ 4)
β Can reach ALL positions 0,1,2,3,4,5
β Don't need to know HOW, just that we CAN!
The beauty: We only care about REACHABILITY, not PATHS!
Greedy is sufficient! β¨
π― The Complete Greedy Algorithm - Intuitive Version
class Solution {
public boolean canJump(int[] nums) {
// Track the furthest position we can possibly reach
int furthest = 0;
// Check each position in order
for (int i = 0; i < nums.length; i++) {
// CRITICAL CHECK: Can we even reach this position?
// If current position is beyond our furthest reach β GAP exists!
if (i > furthest) {
return false; // Stuck! Can't proceed!
}
// We can reach position i (because i β€ furthest)
// From position i, we can jump up to nums[i] steps
// So from i, we can reach up to: i + nums[i]
// Update our furthest reachable position
furthest = Math.max(furthest, i + nums[i]);
// β β
// old furthest new possibility from position i
// OPTIMIZATION: Early exit if we can already reach the end
if (furthest >= nums.length - 1) {
return true; // Success! We can reach the end!
}
}
// If we checked all positions and didn't find a gap
// Check if our furthest reach includes the last index
return furthest >= nums.length - 1;
}
}
π Detailed Walkthrough with Every Variable
nums = [2, 3, 1, 1, 4]
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
INITIALIZATION
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
furthest = 0
Meaning: "Right now, I can only reach index 0 (where I start)"
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LOOP ITERATION 1: i = 0
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Question 1: Can I reach position 0?
Check: i (0) > furthest (0)?
Answer: NO (0 is not > 0)
Conclusion: β I can reach position 0 (I start here!)
At position 0:
nums[0] = 2
Question 2: From position 0, how far can I jump?
Maximum jump: i + nums[i] = 0 + 2 = 2
Update furthest:
furthest = max(0, 2) = 2
New understanding: "I can now reach up to index 2!"
Question 3: Did I reach the end?
Check: furthest (2) >= lastIndex (4)?
Answer: NO (2 < 4)
Conclusion: Keep searching!
State after iteration 1:
furthest = 2
Reachable positions: [0, 1, 2]
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LOOP ITERATION 2: i = 1
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Question 1: Can I reach position 1?
Check: i (1) > furthest (2)?
Answer: NO (1 β€ 2)
Conclusion: β Position 1 is within my reach!
At position 1:
nums[1] = 3
Question 2: From position 1, how far can I jump?
Maximum jump: i + nums[i] = 1 + 3 = 4
Update furthest:
furthest = max(2, 4) = 4
New understanding: "I can now reach up to index 4!"
Question 3: Did I reach the end?
Check: furthest (4) >= lastIndex (4)?
Answer: YES! (4 = 4) β
EARLY EXIT: Return true!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT: true
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
We discovered we can reach the end at iteration 2!
Didn't need to check positions 2, 3, 4!
Visual of reachability:
Position 0: furthest=2 β [0,1,2]ββ
Position 1: furthest=4 β [0,1,2,3,4] β REACHED END!
π¨ Visual Understanding - The Expanding Window
Think of "furthest" as an EXPANDING WINDOW:
nums = [2, 3, 1, 1, 4]
Initial:
[β’]ββββ furthest=0 (only at start)
After position 0 (value=2):
[β’β’β’]ββ furthest=2 (expanded window!)
0 1 2
After position 1 (value=3):
[β’β’β’β’β’] furthest=4 (window reached end!)
0 1 2 3 4 β DONE!
If position 3 was beyond window:
[β’β’β’ ? ] Can't reach position 3!
0 1 2 3 Gap! Return false!
The window MUST be continuous!
Any gap means unreachable!
π‘ The "AHA!" Moment
THE KEY INSIGHT:
You don't need to know the EXACT PATH to the end!
You just need to know IF you can reach there!
DP asks: "What's the path?"
Greedy asks: "Can I reach that far?"
Example:
Position 0 β can reach positions [1, 2]
Position 1 β can reach positions [2, 3, 4]
DP: Stores "from 0 can reach end" and "from 1 can reach end"
Greedy: Stores "furthest reachable = 4" (which is β₯ end)
Same answer, but greedy is:
β Simpler (one number vs. array)
β Faster (one pass vs. checking all jumps)
β Less memory (O(1) vs. O(n))
This is the power of asking the RIGHT QUESTION! π―
π Comparing with DP to Build Intuition
Same Example: nums = [2, 3, 1, 1, 4]
DP THINKING (Backwards):
βββββββββββββββββββββ
dp[4] = true (base case: at end)
dp[3] = true (can jump to dp[4])
dp[2] = true (can jump to dp[3])
dp[1] = true (can jump to dp[2], dp[3], dp[4])
dp[0] = true (can jump to dp[1] or dp[2])
Stores: 5 values
Checks: Multiple positions from each index
GREEDY THINKING (Forward):
βββββββββββββββββββββββ
furthest starts at 0
At i=0: furthest becomes 2 (can reach [0,1,2])
At i=1: furthest becomes 4 (can reach [0,1,2,3,4]) β DONE!
Stores: 1 value
Checks: Each position once
SAME RESULT, but greedy is simpler and faster!
Why can greedy be simpler?
Because we only need YES/NO answer!
We don't need to track WHICH positions can reach end
Just need to know IF end is reachable!
"furthest β₯ end" tells us everything! β¨
π Practice Exercise - Trace It Yourself!
Try this example yourself: nums = [1, 2, 0, 1]
Step 1: Initialize furthest = 0
Step 2: Process each position:
Position 0:
Can reach? _____
nums[0] = _____
furthest = max(_____, 0 + _____) = _____
Reached end? _____
Position 1:
Can reach? _____
nums[1] = _____
furthest = max(_____, 1 + _____) = _____
Reached end? _____
Position 2:
Can reach? _____
nums[2] = _____
furthest = max(_____, 2 + _____) = _____
Reached end? _____
Position 3:
Can reach? _____
Result: _____
Solution at the end of this section! (Don't peek!)
β Solution to Practice Exercise
nums = [1, 2, 0, 1]
furthest = 0
Position 0:
Can reach? YES (0 β€ 0)
nums[0] = 1
furthest = max(0, 0 + 1) = 1
Reached end (3)? NO
Position 1:
Can reach? YES (1 β€ 1)
nums[1] = 2
furthest = max(1, 1 + 2) = 3
Reached end (3)? YES! β
Result: true
We can reach the end!
Actual path: 0 β 1 β 3 (skip index 2)
But greedy doesn't need to know the path! Just knows it's possible!
π― Pattern Recognition - Reachability Problems
Core Pattern: Can We Reach Target?
Template for reachability problems:
Track maximum reachable position:
maxReach = 0
For each position i:
- If i > maxReach β unreachable β fail
- Update maxReach with jumps from i
- If maxReach >= target β success
Key insight: Greedy tracking of farthest reach!
Similar Problems:
- Jump Game II (45): Minimum jumps to reach end
- Jump Game III (1306): Can reach value 0?
- Jump Game IV (1345): Minimum jumps with equal values
- Jump Game V (1340): Maximum visits with constraints
β οΈ Important Edge Cases to Test
// Minimum array size
nums = [0] // Expected: true (already at end)
nums = [2] // Expected: true (already at end)
// Cannot reach end
nums = [0,1] // Expected: false (stuck at start)
nums = [1,0,1,0] // Expected: false (stuck at index 1)
nums = [3,2,1,0,4] // Expected: false (trap at index 3)
// Can reach end
nums = [2,3,1,1,4] // Expected: true (multiple paths)
nums = [1,1,1,1] // Expected: true (can hop to end)
// Large jumps
nums = [10,0,0,0] // Expected: true (big jump over zeros)
nums = [0,10,0,0] // Expected: false (can't start)
// All zeros except last
nums = [0,0,0,0] // Expected: false (stuck at start)
// Maximum jump at each position
nums = [5,4,3,2,1,0] // Expected: true (greedy works)
π Quick Revision Notes
π― Core Concept
Problem: Given array where nums[i] = max jump length at position i, can we reach the last index from index 0?
Function Definitions:
- canJumpFromPosition(i) = Can we reach the last index FROM position i?
- dp[i] = Can we reach the last index FROM position i?
- maxReach = Farthest index we can reach (greedy approach)
Key Insight: Track maximum reachable position. If any position is beyond maxReach, it's unreachable. Update maxReach greedily as we go.
Four Approaches: 1. Brute Force: Try all jump paths β O(2^n) time β 2. Top-Down: Memoization β O(nΒ²) time, O(n) space β 3. Bottom-Up: DP table backwards β O(nΒ²) time, O(n) space β 4. Greedy: Track maxReach β O(n) time, O(1) space βββ
β‘ Quick Implementation
public class Solution {
public boolean canJump(int[] a) {
// // Approach 1 - recursion
// return recursive(0, a);
// // Approach 2 - top down
// Boolean[] memo = new Boolean[a.length + 1];
// return topDown(0, a, memo);
// Approach 3 - bottom up
// return bottomUp(a);
return greedy(a);
}
private boolean greedy(int[] a) {
int n = a.length;
int maxCanReach = 0;
// step 1: start journey at every index
for (int i = 0; i < n; i++) {
// step 5: is that even possible if I can reach this index 'i'?
if (i > maxCanReach) {
// from earlier indices, we should be able to reach the current index
return false;
}
// step 2: how much farthest i can reach from the current index
maxCanReach = Math.max(maxCanReach, i + a[i]);
// step 3: did i cross boundary. If yes, true
if (maxCanReach >= n - 1) {
return true;
}
}
// step 4: all elements done
return true;
}
private boolean bottomUp(int[] a) {
int n = a.length;
Boolean[] dp = new Boolean[n];
// same base case - already at the last step
dp[a.length - 1] = true;
for (int index = n - 2; index >= 0; index--) {
dp[index] = false;
for (int step = 1; step <= a[index]; step++) {
if (index + step <= n && dp[index + step]) {
dp[index] = true;
break;
}
}
}
return dp[0];
}
private boolean topDown(int index, int[] a, Boolean[] memo) {
// base case
if (index >= a.length - 1) {
// Commented below line as sometimes index out of bounds can happen.
// return memo[index] = true;
return true;
}
if (memo[index] != null) {
return memo[index];
}
for (int step = 1; step <= a[index]; step++) {
memo[index] = false;
if (topDown(index + step, a, memo)) {
return memo[index] = true;
}
}
// Above never returned true, so return false
return memo[index] = false;
}
private boolean recursive(int index, int[] a) {
// base case
if (index >= a.length - 1) {
return true;
}
for (int step = 1; step <= a[index]; step++) {
if (recursive(index + step, a)) {
return true;
}
}
// Above never returned true, so return false
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.canJump(new int[] { 2, 3, 1, 1, 4 }) == true);
System.out.println(s.canJump(new int[] { 3, 2, 1, 0, 4 }) == false);
System.out.println(s.canJump(new int[] { 0 }) == true);
System.out.println(s.canJump(new int[] { 2 }) == true);
System.out.println(s.canJump(new int[] { 0, 1 }) == false);
System.out.println(s.canJump(new int[] { 1, 0, 1, 0 }) == false);
System.out.println(s.canJump(new int[] { 3, 2, 1, 0, 4 }) == false);
System.out.println(s.canJump(new int[] { 2, 3, 1, 1, 4 }) == true);
System.out.println(s.canJump(new int[] { 1, 1, 1, 1 }) == true);
System.out.println(s.canJump(new int[] { 10, 0, 0, 0 }) == true);
System.out.println(s.canJump(new int[] { 0, 10, 0, 0 }) == false);
System.out.println(s.canJump(new int[] { 0, 0, 0, 0 }) == false);
System.out.println(s.canJump(new int[] { 5, 4, 3, 2, 1, 0 }) == true);
}
}
π Key Insights
Greedy Strategy:
maxReach = farthest position we can reach
At position i:
- If i > maxReach β unreachable β return false
- Update: maxReach = max(maxReach, i + nums[i])
- If maxReach >= lastIndex β return true
Why greedy works:
We only care IF we can reach end, not HOW
Tracking max reach is sufficient!
DP vs Greedy:
DP Approach:
- Checks each position explicitly
- Stores reachability for all positions
- O(nΒ²) time, O(n) space
Greedy Approach:
- Tracks only maximum reach
- Single pass, no storage
- O(n) time, O(1) space
Greedy is OPTIMAL for this problem! π―
πͺ Memory Aid
"Track max reach, not each position!"
"Can't reach position if beyond max!"
"Greedy beats DP here!"
"One pass, one variable!" β¨
β οΈ Common Mistakes
- β Building DP table forwards instead of backwards
- β Not checking if position i > maxReach (greedy)
- β Forgetting early exit when maxReach >= lastIndex
- β Using O(nΒ²) DP when O(n) greedy exists
- β Confusing "reach FROM i" vs "reach TO i"
β Interview Talking Points
"This is a reachability problem with optimal greedy solution.
Greedy Approach (Optimal):
1. Track maxReach = farthest position reachable
2. At each position i:
- If i > maxReach β unreachable β return false
- Update maxReach = max(maxReach, i + nums[i])
- If maxReach >= lastIndex β return true
Why greedy works:
- We only need to know IF we can reach end
- Don't need to store paths or intermediate states
- Maximum reach tells us everything we need
DP Alternative:
- Build dp[i] = can reach end from i
- Work backwards from last index
- O(nΒ²) time, O(n) space
- Valid but not optimal
Complexity: Greedy is O(n) time, O(1) space
DP is O(nΒ²) time, O(n) space"
π Congratulations!
You've mastered Jump Game - from DP to optimal greedy!
What You Learned:
β
Reachability Problems - Can we reach target position?
β
Bottom-Up DP - Building backwards from goal
β
Greedy Optimization - Tracking maximum reach
β
O(nΒ²) to O(n) - From DP to greedy improvement
β
Space Optimization - From O(n) to O(1)
β
Early Exit Strategy - Stop when answer is found
The Beautiful Journey:
Brute Force (O(2^n)):
"Try every possible jump sequence"
β Too slow! Exponential! π°
Top-Down DP (O(nΒ²)):
"Cache which positions can reach end"
β Better! Polynomial time! π
Bottom-Up DP (O(nΒ²)):
"Build reachability table backwards"
β Clear! No recursion! π
Greedy (O(n)):
"Just track maximum reachable position!"
β Optimal! Linear time, constant space! πβ
Pattern Evolution:
Problem 205: Fibonacci Counting
Problem 206: Cost Optimization
Problem 207: Game Theory
Problem 208: Reachability + Greedy! π―
DP teaches us to think systematically
Greedy shows us when simpler solution exists!
You've now mastered both DP thinking AND greedy optimization! πβ¨