1345. Jump Game IV
Problem Description
You are given an array of integers arr
and start at index 0 (the first position in the array). Your goal is to reach the last index of the array in the minimum number of steps.
From any index i
, you can jump to:
- Next position: index
i + 1
(if it exists within the array bounds) - Previous position: index
i - 1
(if it's not negative) - Any position with the same value: any index
j
wherearr[i] == arr[j]
andi != j
Each jump counts as one step, regardless of the distance traveled. You cannot jump outside the array boundaries at any time.
The task is to find and return the minimum number of steps needed to reach from index 0 to the last index of the array.
For example, if arr = [100, -23, -23, 404, 100, 23, 23, 23, 3, 404]
:
- From index 0 (value 100), you could jump to index 1 (next), or index 4 (same value 100)
- From index 2 (value -23), you could jump to index 1 (previous), index 3 (next), or index 1 (same value -23)
The algorithm uses BFS (Breadth-First Search) to find the shortest path. It builds a graph where each value maps to all indices containing that value, then explores all possible jumps level by level to find the minimum steps. The key optimization is using g.pop(arr[i], [])
to ensure each group of same-valued indices is only processed once, preventing redundant explorations.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph where each index is a node. Edges exist between:
- Adjacent indices (i to i+1 and i to i-1)
- Indices with the same value (all indices with value x are connected)
Is it a tree?
- No: The graph is not a tree because it contains cycles (you can jump back and forth between adjacent indices, and indices with same values can form cycles).
Is the problem related to directed acyclic graphs?
- No: The graph has cycles, so it's not a DAG.
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of steps (shortest path) from index 0 to the last index.
Is the graph Weighted?
- No: All jumps have the same cost (1 step), making this an unweighted graph shortest path problem.
BFS
- Yes: For unweighted graphs where we need the shortest path, BFS is the optimal choice.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of jumps. BFS explores the graph level by level, guaranteeing that when we first reach the target index, we've found the shortest path. This matches the solution approach where a queue is used to process indices level by level, tracking visited nodes to avoid cycles, and counting the number of levels (steps) needed to reach the destination.
Intuition
The key insight is recognizing that this is fundamentally a shortest path problem in an unweighted graph. Each array index represents a node, and we need to find the minimum "distance" (number of jumps) from the start to the end.
Why model it as a graph? Consider what makes this problem unique - we can jump to indices with the same value, not just adjacent positions. This creates a complex network of connections. For instance, if value 5
appears at indices [2, 7, 15]
, all three positions are interconnected, forming a "teleportation network" where we can instantly jump between any of them.
Since each jump has equal cost (1 step), we're dealing with an unweighted graph. In such scenarios, BFS naturally finds the shortest path because it explores all nodes at distance d
before exploring nodes at distance d+1
. This level-by-level exploration guarantees that the first time we reach our target, we've taken the minimum number of steps.
The clever optimization in the solution is how it handles same-value jumps. Instead of repeatedly checking all indices with the same value, we use g.pop(arr[i], [])
to process each group of same-valued indices only once. Why does this work? Once we've explored jumping from value x
to all its occurrences, we never need to do it again - any future encounter with value x
won't provide a shorter path since BFS already found the optimal distances.
Think of it like exploring a city's subway system where some stations are connected by "color lines" - once you've explored all stations on the "red line" from one red station, you don't need to re-explore them from another red station you encounter later, as you've already found the shortest paths to all red stations.
Learn more about Breadth-First Search patterns.
Solution Approach
The implementation uses BFS with several key data structures to efficiently find the shortest path:
1. Building the Graph Mapping (g
):
g = defaultdict(list)
for i, x in enumerate(arr):
g[x].append(i)
We create a dictionary where each value maps to a list of all indices containing that value. This preprocessing step allows O(1) lookup of all "teleportation" destinations for any value.
2. BFS Initialization:
q = deque([0]) vis = {0} ans = 0
q
: A deque (double-ended queue) for BFS traversal, starting with index 0vis
: A set to track visited indices and prevent cyclesans
: Counter for the number of steps (levels in BFS)
3. Level-by-Level BFS Traversal:
while 1:
for _ in range(len(q)):
i = q.popleft()
if i == len(arr) - 1:
return ans
The outer while
loop represents each level of BFS (each step). The inner for
loop processes all nodes at the current level before incrementing the step counter.
4. Exploring Neighbors:
for j in (i + 1, i - 1, *g.pop(arr[i], [])):
if 0 <= j < len(arr) and j not in vis:
q.append(j)
vis.add(j)
For each index i
, we explore three types of jumps:
i + 1
: Jump to the next indexi - 1
: Jump to the previous index*g.pop(arr[i], [])
: Jump to all indices with the same value
The critical optimization is g.pop(arr[i], [])
. By using pop
instead of get
, we remove the value group from the dictionary after first use. This ensures each group of same-valued indices is processed only once, preventing redundant explorations. The unpacking operator *
spreads the list of indices into the tuple.
5. Boundary and Visit Checks:
The condition 0 <= j < len(arr) and j not in vis
ensures we:
- Stay within array bounds
- Don't revisit already explored indices
6. Step Increment:
ans += 1
After processing all nodes at the current level, we increment the step counter before moving to the next level.
The algorithm guarantees finding the shortest path because BFS explores nodes in order of their distance from the source. The first time we reach the last index, we've found the minimum number of steps required.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to illustrate how the BFS solution finds the minimum steps.
Example: arr = [7, 6, 9, 6, 9, 6, 9, 7]
Step 1: Build the value-to-indices mapping
g = { 7: [0, 7], 6: [1, 3, 5], 9: [2, 4, 6] }
Step 2: Initialize BFS
- Start at index 0 (value 7)
q = [0]
,vis = {0}
,ans = 0
Step 3: First BFS level (ans = 0)
- Process index 0:
- Can jump to index 1 (next position)
- Cannot jump to index -1 (out of bounds)
- Can jump to index 7 using
g.pop(7)
which returns[0, 7]
- Index 0 is already visited, so skip it
- Index 7 is the target! Return
ans = 0
... wait, we haven't incremented yet
- Actually, we check if current index is target first: 0 β 7, continue
- Add valid unvisited indices to queue:
q = [1, 7]
- Mark as visited:
vis = {0, 1, 7}
- Remove value 7 from g:
g = {6: [1, 3, 5], 9: [2, 4, 6]}
Step 4: Increment ans to 1 and start second BFS level
-
Process index 1:
- Check if 1 == 7 (target)? No
- Explore neighbors:
- Index 0 (previous): already visited
- Index 2 (next): add to queue
- Indices with value 6 via
g.pop(6)
:[1, 3, 5]
- Index 1: already visited
- Index 3: add to queue
- Index 5: add to queue
- Remove value 6 from g:
g = {9: [2, 4, 6]}
-
Process index 7:
- Check if 7 == 7 (target)? Yes! Return ans = 1
Result: Minimum steps = 1
The algorithm found that we can reach the last index in just 1 step by jumping from index 0 directly to index 7 (both have value 7). This demonstrates the power of the "same-value jump" feature and how BFS guarantees finding the shortest path.
Solution Implementation
1class Solution:
2 def minJumps(self, arr: List[int]) -> int:
3 # Build a graph where each value maps to all its indices in the array
4 value_to_indices = defaultdict(list)
5 for index, value in enumerate(arr):
6 value_to_indices[value].append(index)
7
8 # Initialize BFS queue with starting position (index 0)
9 queue = deque([0])
10 # Track visited indices to avoid revisiting
11 visited = {0}
12 # Track the number of steps taken
13 steps = 0
14
15 # BFS to find minimum steps to reach the last index
16 while True:
17 # Process all nodes at current level
18 for _ in range(len(queue)):
19 current_index = queue.popleft()
20
21 # Check if we've reached the target (last index)
22 if current_index == len(arr) - 1:
23 return steps
24
25 # Explore all possible next positions:
26 # 1. Move one step forward (current_index + 1)
27 # 2. Move one step backward (current_index - 1)
28 # 3. Jump to any index with the same value
29 # Note: pop() is used to avoid revisiting indices with same value
30 next_indices = [current_index + 1, current_index - 1]
31 same_value_indices = value_to_indices.pop(arr[current_index], [])
32
33 for next_index in (*next_indices, *same_value_indices):
34 # Check if next_index is valid and unvisited
35 if 0 <= next_index < len(arr) and next_index not in visited:
36 queue.append(next_index)
37 visited.add(next_index)
38
39 # Increment step count after processing current level
40 steps += 1
41
1class Solution {
2 public int minJumps(int[] arr) {
3 // Build a graph where key is the value and value is list of indices with that value
4 Map<Integer, List<Integer>> valueToIndicesMap = new HashMap<>();
5 int arrayLength = arr.length;
6
7 // Populate the map with all indices for each value
8 for (int index = 0; index < arrayLength; index++) {
9 valueToIndicesMap.computeIfAbsent(arr[index], k -> new ArrayList<>()).add(index);
10 }
11
12 // Track visited indices to avoid revisiting
13 boolean[] visited = new boolean[arrayLength];
14
15 // BFS queue to store indices to process
16 Deque<Integer> queue = new ArrayDeque<>();
17
18 // Start BFS from index 0
19 queue.offer(0);
20 visited[0] = true;
21
22 // BFS to find minimum steps to reach the last index
23 for (int steps = 0; ; steps++) {
24 // Process all nodes at current level
25 int currentLevelSize = queue.size();
26 for (int i = 0; i < currentLevelSize; i++) {
27 int currentIndex = queue.poll();
28
29 // Check if we've reached the target (last index)
30 if (currentIndex == arrayLength - 1) {
31 return steps;
32 }
33
34 // Jump to all indices with the same value
35 List<Integer> sameValueIndices = valueToIndicesMap.get(arr[currentIndex]);
36 for (int nextIndex : sameValueIndices) {
37 if (!visited[nextIndex]) {
38 visited[nextIndex] = true;
39 queue.offer(nextIndex);
40 }
41 }
42
43 // Clear the list to avoid redundant checks in future iterations
44 // This optimization prevents revisiting the same value group
45 sameValueIndices.clear();
46
47 // Jump to adjacent indices (left and right)
48 for (int nextIndex : new int[] {currentIndex - 1, currentIndex + 1}) {
49 if (nextIndex >= 0 && nextIndex < arrayLength && !visited[nextIndex]) {
50 visited[nextIndex] = true;
51 queue.offer(nextIndex);
52 }
53 }
54 }
55 }
56 }
57}
58
1class Solution {
2public:
3 int minJumps(vector<int>& arr) {
4 // Build a graph where each value maps to all its indices in the array
5 unordered_map<int, vector<int>> valueToIndices;
6 int arraySize = arr.size();
7
8 // Populate the map with value -> list of indices
9 for (int i = 0; i < arraySize; ++i) {
10 valueToIndices[arr[i]].push_back(i);
11 }
12
13 // Track visited indices to avoid revisiting
14 vector<bool> visited(arraySize);
15
16 // BFS queue starting from index 0
17 queue<int> bfsQueue{{0}};
18 visited[0] = true;
19
20 // BFS level by level to find minimum jumps
21 for (int steps = 0;; ++steps) {
22 // Process all nodes at current level
23 int currentLevelSize = bfsQueue.size();
24 for (int k = 0; k < currentLevelSize; ++k) {
25 int currentIndex = bfsQueue.front();
26 bfsQueue.pop();
27
28 // Check if we've reached the last index
29 if (currentIndex == arraySize - 1) {
30 return steps;
31 }
32
33 // Jump to all indices with the same value
34 for (int nextIndex : valueToIndices[arr[currentIndex]]) {
35 if (!visited[nextIndex]) {
36 visited[nextIndex] = true;
37 bfsQueue.push(nextIndex);
38 }
39 }
40
41 // Clear the value group to avoid redundant checks in future iterations
42 valueToIndices[arr[currentIndex]].clear();
43
44 // Jump to adjacent indices (left and right)
45 vector<int> adjacentIndices = {currentIndex - 1, currentIndex + 1};
46 for (int nextIndex : adjacentIndices) {
47 if (nextIndex >= 0 && nextIndex < arraySize && !visited[nextIndex]) {
48 visited[nextIndex] = true;
49 bfsQueue.push(nextIndex);
50 }
51 }
52 }
53 }
54 }
55};
56
1/**
2 * Finds the minimum number of jumps to reach the last index of the array.
3 * You can jump to index i-1, i+1, or any index j where arr[i] === arr[j].
4 *
5 * @param arr - The input array of numbers
6 * @returns The minimum number of jumps to reach the last index
7 */
8function minJumps(arr: number[]): number {
9 // Map to store indices grouped by their values for same-value jumps
10 const valueToIndicesMap: Map<number, number[]> = new Map();
11 const arrayLength = arr.length;
12
13 // Build the map: each value points to all indices where it appears
14 for (let index = 0; index < arrayLength; index++) {
15 if (!valueToIndicesMap.has(arr[index])) {
16 valueToIndicesMap.set(arr[index], []);
17 }
18 valueToIndicesMap.get(arr[index])!.push(index);
19 }
20
21 // BFS queue starting from index 0
22 let currentLevelQueue: number[] = [0];
23
24 // Track visited indices to avoid revisiting
25 const visited: boolean[] = Array(arrayLength).fill(false);
26 visited[0] = true;
27
28 // BFS traversal level by level
29 for (let jumpCount = 0; ; jumpCount++) {
30 const nextLevelQueue: number[] = [];
31
32 // Process all indices in the current level
33 for (const currentIndex of currentLevelQueue) {
34 // Check if we've reached the target (last index)
35 if (currentIndex === arrayLength - 1) {
36 return jumpCount;
37 }
38
39 // Jump to all indices with the same value
40 for (const sameValueIndex of valueToIndicesMap.get(arr[currentIndex])!) {
41 if (!visited[sameValueIndex]) {
42 visited[sameValueIndex] = true;
43 nextLevelQueue.push(sameValueIndex);
44 }
45 }
46
47 // Clear the same-value indices to avoid redundant checks in future iterations
48 valueToIndicesMap.get(arr[currentIndex])!.length = 0;
49
50 // Jump to adjacent indices (left and right neighbors)
51 for (const adjacentIndex of [currentIndex - 1, currentIndex + 1]) {
52 if (adjacentIndex >= 0 && adjacentIndex < arrayLength && !visited[adjacentIndex]) {
53 visited[adjacentIndex] = true;
54 nextLevelQueue.push(adjacentIndex);
55 }
56 }
57 }
58
59 // Move to the next level
60 currentLevelQueue = nextLevelQueue;
61 }
62}
63
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input array.
- Building the graph
g
takesO(n)
time as we iterate through the array once. - The BFS traversal visits each index at most once due to the
vis
set, contributingO(n)
. - For each index, we explore its neighbors: the previous index
i-1
, next indexi+1
, and all indices with the same value. - The key optimization is
g.pop(arr[i], [])
which ensures that indices with the same value are only processed once collectively. After processing all indices with valuearr[i]
, they are removed from the graph, preventing redundant traversals. - Each edge in the implicit graph is traversed at most once. The maximum number of edges is
O(n)
(considering next/previous connections and value-based connections). - Overall:
O(n)
for preprocessing +O(n)
for BFS =O(n)
.
Space Complexity: O(n)
- The graph
g
stores at mostn
indices across all keys, requiringO(n)
space. - The visited set
vis
can contain at mostn
indices, usingO(n)
space. - The queue
q
can hold at mostn
indices in the worst case, takingO(n)
space. - Overall space complexity:
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Removing Same-Value Groups After First Use
The Pitfall:
A critical performance issue occurs when developers use g.get(arr[i], [])
instead of g.pop(arr[i], [])
when exploring same-value jumps. This causes the algorithm to repeatedly process the same group of indices with identical values at every encounter.
Why It's Problematic:
Consider an array like [7, 7, 7, 7, 7, 7, 7, 11]
. Without removing the value group after first use:
- When we first reach any index with value 7, we explore all other 7s
- Later, when BFS reaches another 7 from a different path, it will again try to explore all 7s
- This creates redundant work and can degrade time complexity from O(n) to O(nΒ²) in worst cases
The Solution:
# WRONG - causes redundant explorations for j in (i + 1, i - 1, *g.get(arr[i], [])): # Process neighbors # CORRECT - removes group after first use for j in (i + 1, i - 1, *g.pop(arr[i], [])): # Process neighbors
The pop()
operation ensures that once we've explored all indices with a particular value, we never process that group again. This is valid because BFS guarantees we find the shortest path on first exploration.
2. Incorrect Handling of Single-Element Arrays
The Pitfall: Forgetting to handle the edge case where the array has only one element. The starting index (0) is also the last index, requiring 0 steps.
The Solution: Add an early check before starting BFS:
if len(arr) == 1:
return 0
Or ensure the BFS check happens before exploring neighbors:
# Check immediately after dequeuing
current_index = queue.popleft()
if current_index == len(arr) - 1:
return steps
3. Memory Issues with Large Arrays of Identical Values
The Pitfall:
Arrays with many identical values (e.g., [1] * 100000
) can cause memory issues because the initial graph construction creates large lists of indices.
The Solution: Consider adding optimization for consecutive identical values:
# Preprocess to remove unnecessary consecutive duplicates
# Keep only the first and last of each consecutive group
def preprocess(arr):
if len(arr) <= 2:
return arr
processed = [arr[0]]
for i in range(1, len(arr) - 1):
if arr[i] != arr[i-1] or arr[i] != arr[i+1]:
processed.append(arr[i])
processed.append(arr[-1])
# Build mapping for processed array indices to original
# ... additional logic needed for index mapping
This reduces the graph size while maintaining correctness since jumping within consecutive identical values doesn't provide any advantage.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!