Facebook Pixel

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 where arr[i] == arr[j] and i != 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 0
  • vis: A set to track visited indices and prevent cycles
  • ans: 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 index
  • i - 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 Evaluator

Example 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 takes O(n) time as we iterate through the array once.
  • The BFS traversal visits each index at most once due to the vis set, contributing O(n).
  • For each index, we explore its neighbors: the previous index i-1, next index i+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 value arr[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 most n indices across all keys, requiring O(n) space.
  • The visited set vis can contain at most n indices, using O(n) space.
  • The queue q can hold at most n indices in the worst case, taking O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More