{x}
blog image

Jump Game IV

Given an array of integers arr, you are initially positioned at the first index of the array.

In one step you can jump from index i to index:

  • i + 1 where: i + 1 < arr.length.
  • i - 1 where: i - 1 >= 0.
  • j where: arr[i] == arr[j] and i != j.

Return the minimum number of steps to reach the last index of the array.

Notice that you can not jump outside of the array at any time.

 

Example 1:

Input: arr = [100,-23,-23,404,100,23,23,23,3,404]
Output: 3
Explanation: You need three jumps from index 0 --> 4 --> 3 --> 9. Note that index 9 is the last index of the array.

Example 2:

Input: arr = [7]
Output: 0
Explanation: Start index is the last index. You do not need to jump.

Example 3:

Input: arr = [7,6,9,6,9,6,9,7]
Output: 1
Explanation: You can jump directly from index 0 to index 7 which is last index of the array.

 

Constraints:

  • 1 <= arr.length <= 5 * 104
  • -108 <= arr[i] <= 108

Solution Explanation:

This problem asks for the minimum number of steps to reach the last index of an array, given the ability to jump to adjacent indices or to any index with the same value. The optimal approach is using Breadth-First Search (BFS).

Algorithm:

  1. Build an Adjacency List: Create a dictionary (hash map) where keys are the numbers in the array, and values are lists of indices where those numbers appear. This allows for quick access to all indices with the same value.

  2. BFS Traversal:

    • Initialize a queue with the starting index (0).
    • Maintain a visited set to track visited indices, preventing cycles.
    • Perform BFS:
      • In each iteration, process all nodes at the current level of the queue.
      • For each node i:
        • If i is the last index, return the current level (number of steps).
        • Explore adjacent indices (i + 1 and i - 1). Add them to the queue if they are within bounds and haven't been visited.
        • Explore indices with the same value (arr[i]). Add them to the queue if they haven't been visited. Crucially, after adding these same-value indices to the queue, clear the list of indices associated with arr[i] in the adjacency list to avoid redundant exploration. This optimization is crucial for efficiency.
  3. Return Minimum Steps: The BFS algorithm guarantees finding the shortest path (minimum steps) because it explores level by level.

Time Complexity Analysis:

  • Building the adjacency list takes O(N) time, where N is the length of the array.

  • In the worst case, BFS visits each node once. While the number of edges in the adjacency list can be O(N^2) in a pathological case (all elements are the same), the clearing of the adjacency list entries ensures that each node is visited and processed at most once. Therefore, the time complexity of the BFS traversal is effectively O(N).

  • Overall Time Complexity: O(N)

Space Complexity Analysis:

  • The adjacency list uses O(N) space in the worst case (all unique elements).
  • The queue can hold up to N elements in the worst case.
  • The visited set uses O(N) space.
  • Overall Space Complexity: O(N)

Code Examples (with detailed comments):

Python:

from collections import defaultdict, deque
 
def minJumps(arr):
    n = len(arr)
    graph = defaultdict(list) # Adjacency list: number -> list of indices
    for i, num in enumerate(arr):
        graph[num].append(i)
 
    queue = deque([0]) # Queue for BFS
    visited = {0} # Set to track visited indices
    steps = 0 # Count of steps
 
    while queue:
        for _ in range(len(queue)): # Process all nodes at current level
            curr = queue.popleft()
            if curr == n - 1:
                return steps
 
            # Explore neighbors with same value:
            for neighbor in graph[arr[curr]]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)
            graph[arr[curr]] = [] # Crucial optimization: Clear to avoid revisits
 
            # Explore adjacent indices:
            for neighbor in [curr - 1, curr + 1]:
                if 0 <= neighbor < n and neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)
        steps += 1  # Increment steps after processing a level
    return steps # Should never reach here if the last index is reachable
 

The other languages (Java, C++, Go, TypeScript) follow a very similar structure, using appropriate data structures for their respective languages (e.g., HashMap, vector, etc.). The core algorithmic steps remain the same. Remember the crucial optimization of clearing the adjacency list entries after processing nodes with the same value – this is key to achieving linear time complexity.