Facebook Pixel

975. Odd Even Jump

Problem Description

You have an integer array arr and need to find how many starting positions allow you to reach the last index through a series of alternating jump rules.

The jumping mechanics work as follows:

Jump Types:

  • Odd-numbered jumps (1st, 3rd, 5th, etc.): From index i, jump forward to index j where:

    • arr[i] <= arr[j]
    • arr[j] is the smallest value among all valid options
    • If multiple indices have the same smallest value, jump to the leftmost one
  • Even-numbered jumps (2nd, 4th, 6th, etc.): From index i, jump forward to index j where:

    • arr[i] >= arr[j]
    • arr[j] is the largest value among all valid options
    • If multiple indices have the same largest value, jump to the leftmost one

Key Rules:

  • You can only jump forward (from index i to j where i < j)
  • Your first jump from any starting position is always an odd-numbered jump
  • Jumps alternate between odd and even
  • Some positions may have no valid jumps available
  • A starting index is "good" if you can eventually reach the last index (arr.length - 1) from there

Goal: Count how many indices in the array are "good" starting positions.

Example walkthrough: If arr = [10, 13, 12, 14, 15]:

  • Starting from index 0 (value 10):

    • 1st jump (odd): Find smallest value >= 10 to the right → jump to index 2 (value 12)
    • 2nd jump (even): Find largest value <= 12 to the right → no valid jump available
    • Cannot reach the end, so index 0 is not good
  • Starting from index 4 (value 15):

    • Already at the last index, so index 4 is good

The solution uses dynamic programming with memoization (dfs function) and a SortedDict to efficiently find the next valid jump positions for each index.

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

Intuition

The key insight is that from any position, the next jump is deterministic - there's only one valid destination (or no valid jump at all). This means we can precompute where each position jumps to for both odd and even jumps.

Breaking down the problem:

  1. The jumping pattern is fixed: From any index i, if we're making an odd jump, we always go to the same specific index. Similarly for even jumps. This suggests we can build a "jump graph" ahead of time.

  2. Working backwards is easier: If we process the array from right to left, we can maintain a sorted structure of indices we've seen so far. For any current position, we need to find:

    • For odd jumps: The smallest value >= current value (lower bound search)
    • For even jumps: The largest value <= current value (upper bound search minus 1)
  3. Why SortedDict? We need to:

    • Find values in sorted order (for the jump rules)
    • Map those values back to their indices
    • Handle duplicates by keeping the leftmost index

    A SortedDict with values as keys and indices as values perfectly handles this. When we encounter duplicates, we naturally overwrite with the leftmost (most recent in right-to-left traversal) index.

  4. The two-state nature: Each position has two states - whether we can reach the end starting with an odd jump or an even jump. This naturally leads to:

    • g[i][0]: where to jump from index i on an even jump
    • g[i][1]: where to jump from index i on an odd jump
  5. Dynamic programming emerges naturally: Once we know where each position jumps to, checking if a position is "good" becomes a graph traversal problem. We alternate between odd and even jumps (k ^ 1 flips between 0 and 1) and use memoization to avoid recalculating paths.

The elegance is in recognizing that the complex jumping rules can be preprocessed into a simple directed graph, where each node has at most two outgoing edges (one for odd, one for even), and then the problem reduces to path-finding with memoization.

Learn more about Stack, Dynamic Programming, Sorting and Monotonic Stack patterns.

Solution Approach

The solution builds a jump graph preprocessing all possible jumps, then uses dynamic programming with memoization to count good starting positions.

Step 1: Initialize the jump graph

g = [[0] * 2 for _ in range(n)]

Create a 2D array where g[i][0] stores the next position for an even jump from index i, and g[i][1] stores the next position for an odd jump. Initialize with 0, but will use -1 to indicate no valid jump exists.

Step 2: Build the jump graph (right to left)

sd = SortedDict()
for i in range(n - 1, -1, -1):

Process the array from right to left, maintaining a SortedDict where keys are array values and values are their indices. This ensures we always have the leftmost index for duplicates.

Step 3: Find odd jump destination

j = sd.bisect_left(arr[i])
g[i][1] = sd.values()[j] if j < len(sd) else -1

For odd jumps, we need the smallest value >= arr[i]. The bisect_left finds the insertion point for arr[i], which gives us the smallest value >= arr[i]. If j is within bounds, we get the index from sd.values()[j], otherwise set to -1 (no valid jump).

Step 4: Find even jump destination

j = sd.bisect_right(arr[i]) - 1
g[i][0] = sd.values()[j] if j >= 0 else -1

For even jumps, we need the largest value <= arr[i]. The bisect_right finds the insertion point after all occurrences of arr[i], so j - 1 gives us the largest value <= arr[i]. If j >= 0, we have a valid jump.

Step 5: Update the SortedDict

sd[arr[i]] = i

Add current element to the SortedDict. If the value already exists, it updates to the current (leftmost in original array) index.

Step 6: Dynamic programming with memoization

@cache
def dfs(i: int, k: int) -> bool:
    if i == n - 1:
        return True
    if g[i][k] == -1:
        return False
    return dfs(g[i][k], k ^ 1)

The dfs function checks if we can reach the end from position i with jump type k (1 for odd, 0 for even):

  • Base case: If at the last index, return True
  • If no valid jump exists (g[i][k] == -1), return False
  • Otherwise, jump to the next position and flip the jump type using XOR (k ^ 1)

Step 7: Count good starting positions

return sum(dfs(i, 1) for i in range(n))

Check each index as a starting position. Since the first jump is always odd, we call dfs(i, 1) for each index and sum up the results.

Time Complexity: O(n log n) due to the sorted dictionary operations and O(n) for the DFS with memoization.

Space Complexity: O(n) for the jump graph and memoization cache.

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: arr = [5, 1, 3, 4, 2]

Step 1: Build the jump graph (process right to left)

Starting with an empty SortedDict, we process each index:

Index 4 (value 2):

  • SortedDict is empty
  • Odd jump: bisect_left(2) = 0, but SortedDict is empty → g[4][1] = -1
  • Even jump: bisect_right(2) - 1 = -1 → g[4][0] = -1
  • Add to SortedDict: {2: 4}

Index 3 (value 4):

  • SortedDict: {2: 4}
  • Odd jump: bisect_left(4) = 1, out of bounds → g[3][1] = -1
  • Even jump: bisect_right(4) - 1 = 0, get index at position 0 → g[3][0] = 4
  • Add to SortedDict: {2: 4, 4: 3}

Index 2 (value 3):

  • SortedDict: {2: 4, 4: 3}
  • Odd jump: bisect_left(3) = 1, get index at position 1 → g[2][1] = 3
  • Even jump: bisect_right(3) - 1 = 0, get index at position 0 → g[2][0] = 4
  • Add to SortedDict: {2: 4, 3: 2, 4: 3}

Index 1 (value 1):

  • SortedDict: {2: 4, 3: 2, 4: 3}
  • Odd jump: bisect_left(1) = 0, get index at position 0 → g[1][1] = 4
  • Even jump: bisect_right(1) - 1 = -1 → g[1][0] = -1
  • Add to SortedDict: {1: 1, 2: 4, 3: 2, 4: 3}

Index 0 (value 5):

  • SortedDict: {1: 1, 2: 4, 3: 2, 4: 3}
  • Odd jump: bisect_left(5) = 4, out of bounds → g[0][1] = -1
  • Even jump: bisect_right(5) - 1 = 3, get index at position 3 → g[0][0] = 3
  • Final SortedDict: {1: 1, 2: 4, 3: 2, 4: 3, 5: 0}

Final jump graph:

g = [[3, -1],  # Index 0: even→3, odd→none
     [-1, 4],  # Index 1: even→none, odd→4
     [4, 3],   # Index 2: even→4, odd→3
     [4, -1],  # Index 3: even→4, odd→none
     [-1, -1]] # Index 4: even→none, odd→none

Step 2: Check each starting position using DFS

Starting from index 0:

  • dfs(0, 1) - start with odd jump
  • g[0][1] = -1 - no valid odd jump
  • Return False

Starting from index 1:

  • dfs(1, 1) - start with odd jump
  • g[1][1] = 4 - jump to index 4
  • dfs(4, 0) - now even jump from index 4
  • Index 4 is the last index
  • Return True

Starting from index 2:

  • dfs(2, 1) - start with odd jump
  • g[2][1] = 3 - jump to index 3
  • dfs(3, 0) - now even jump from index 3
  • g[3][0] = 4 - jump to index 4
  • dfs(4, 1) - now odd jump from index 4
  • Index 4 is the last index
  • Return True

Starting from index 3:

  • dfs(3, 1) - start with odd jump
  • g[3][1] = -1 - no valid odd jump
  • Return False

Starting from index 4:

  • dfs(4, 1) - start with odd jump
  • Index 4 is the last index
  • Return True

Result: 3 good starting positions (indices 1, 2, and 4)

Solution Implementation

1class Solution:
2    def oddEvenJumps(self, arr: List[int]) -> int:
3        from sortedcontainers import SortedDict
4        from functools import cache
5      
6        @cache
7        def can_reach_end(index: int, jump_type: int) -> bool:
8            """
9            Check if we can reach the end from given index with given jump type.
10          
11            Args:
12                index: Current position in array
13                jump_type: 1 for odd jump, 0 for even jump
14          
15            Returns:
16                True if we can reach the last index, False otherwise
17            """
18            # Base case: already at the last index
19            if index == array_length - 1:
20                return True
21          
22            # No valid next jump exists
23            if next_jump_indices[index][jump_type] == -1:
24                return False
25          
26            # Recursively check from next position with alternating jump type
27            return can_reach_end(next_jump_indices[index][jump_type], jump_type ^ 1)
28      
29        array_length = len(arr)
30      
31        # Build next jump indices table
32        # next_jump_indices[i][0] = next index for even jump from i
33        # next_jump_indices[i][1] = next index for odd jump from i
34        next_jump_indices = [[0] * 2 for _ in range(array_length)]
35      
36        # SortedDict to maintain sorted order of values and their indices
37        sorted_dict = SortedDict()
38      
39        # Process array from right to left to build jump table
40        for current_index in range(array_length - 1, -1, -1):
41            current_value = arr[current_index]
42          
43            # Find next index for odd jump (smallest value >= current_value)
44            position = sorted_dict.bisect_left(current_value)
45            if position < len(sorted_dict):
46                next_jump_indices[current_index][1] = sorted_dict.values()[position]
47            else:
48                next_jump_indices[current_index][1] = -1
49          
50            # Find next index for even jump (largest value <= current_value)
51            position = sorted_dict.bisect_right(current_value) - 1
52            if position >= 0:
53                next_jump_indices[current_index][0] = sorted_dict.values()[position]
54            else:
55                next_jump_indices[current_index][0] = -1
56          
57            # Add current value and index to sorted dict
58            sorted_dict[current_value] = current_index
59      
60        # Count how many starting indices can reach the end
61        # All jumps start with odd jump (jump_type = 1)
62        return sum(can_reach_end(start_index, 1) for start_index in range(array_length))
63
1class Solution {
2    private int arrayLength;
3    private Integer[][] memoization;  // memoization[i][jumpType]: can reach end from index i with jumpType
4    private int[][] nextJumpIndex;    // nextJumpIndex[i][jumpType]: next index to jump from i with jumpType
5  
6    public int oddEvenJumps(int[] arr) {
7        // TreeMap to efficiently find ceiling and floor values
8        TreeMap<Integer, Integer> valueToIndexMap = new TreeMap<>();
9        arrayLength = arr.length;
10        memoization = new Integer[arrayLength][2];
11        nextJumpIndex = new int[arrayLength][2];
12      
13        // Build the next jump indices from right to left
14        // This ensures when we process index i, all indices after i are already in the TreeMap
15        for (int i = arrayLength - 1; i >= 0; i--) {
16            // For odd jumps (jumpType = 1): find smallest value >= arr[i]
17            Map.Entry<Integer, Integer> ceilingEntry = valueToIndexMap.ceilingEntry(arr[i]);
18            nextJumpIndex[i][1] = (ceilingEntry == null) ? -1 : ceilingEntry.getValue();
19          
20            // For even jumps (jumpType = 0): find largest value <= arr[i]
21            Map.Entry<Integer, Integer> floorEntry = valueToIndexMap.floorEntry(arr[i]);
22            nextJumpIndex[i][0] = (floorEntry == null) ? -1 : floorEntry.getValue();
23          
24            // Add current element to TreeMap for future lookups
25            valueToIndexMap.put(arr[i], i);
26        }
27      
28        // Count how many starting positions can reach the end
29        int validStartingPositions = 0;
30        for (int startIndex = 0; startIndex < arrayLength; startIndex++) {
31            // Start with odd jump (jumpType = 1) from each position
32            validStartingPositions += canReachEnd(startIndex, 1);
33        }
34      
35        return validStartingPositions;
36    }
37  
38    /**
39     * DFS with memoization to check if we can reach the last index
40     * @param currentIndex - current position in the array
41     * @param jumpType - 1 for odd jump, 0 for even jump
42     * @return 1 if can reach the end, 0 otherwise
43     */
44    private int canReachEnd(int currentIndex, int jumpType) {
45        // Base case: already at the last index
46        if (currentIndex == arrayLength - 1) {
47            return 1;
48        }
49      
50        // No valid next jump from current position
51        if (nextJumpIndex[currentIndex][jumpType] == -1) {
52            return 0;
53        }
54      
55        // Return memoized result if already computed
56        if (memoization[currentIndex][jumpType] != null) {
57            return memoization[currentIndex][jumpType];
58        }
59      
60        // Recursively check from next position with alternating jump type
61        // XOR with 1 toggles between 0 and 1 (even and odd jumps)
62        int nextIndex = nextJumpIndex[currentIndex][jumpType];
63        return memoization[currentIndex][jumpType] = canReachEnd(nextIndex, jumpType ^ 1);
64    }
65}
66
1class Solution {
2public:
3    int oddEvenJumps(vector<int>& arr) {
4        int n = arr.size();
5      
6        // Map to store value -> index pairs for finding next valid jumps
7        map<int, int> valueToIndex;
8      
9        // Memoization table: memo[i][jumpType] 
10        // jumpType: 0 = even jump, 1 = odd jump
11        int memo[n][2];
12        memset(memo, 0, sizeof(memo));
13      
14        // Next jump indices: nextJump[i][jumpType]
15        // Stores the index we jump to from position i
16        int nextJump[n][2];
17      
18        // Build next jump indices from right to left
19        for (int i = n - 1; i >= 0; --i) {
20            // For odd jumps (jump to smallest value >= current)
21            auto oddJumpIt = valueToIndex.lower_bound(arr[i]);
22            nextJump[i][1] = (oddJumpIt == valueToIndex.end()) ? -1 : oddJumpIt->second;
23          
24            // For even jumps (jump to largest value <= current)
25            auto evenJumpIt = valueToIndex.upper_bound(arr[i]);
26            nextJump[i][0] = (evenJumpIt == valueToIndex.begin()) ? -1 : prev(evenJumpIt)->second;
27          
28            // Update map with current position
29            valueToIndex[arr[i]] = i;
30        }
31      
32        // DFS with memoization to check if we can reach the end
33        // i: current position, jumpType: 0 for even jump, 1 for odd jump
34        function<int(int, int)> canReachEnd = [&](int i, int jumpType) -> int {
35            // Base case: reached the last index
36            if (i == n - 1) {
37                return 1;
38            }
39          
40            // No valid jump from this position
41            if (nextJump[i][jumpType] == -1) {
42                return 0;
43            }
44          
45            // Return memoized result if already computed
46            if (memo[i][jumpType] != 0) {
47                return memo[i][jumpType];
48            }
49          
50            // Make the jump and alternate jump type (XOR with 1)
51            return memo[i][jumpType] = canReachEnd(nextJump[i][jumpType], jumpType ^ 1);
52        };
53      
54        // Count all starting positions that can reach the end
55        // First jump is always odd (jumpType = 1)
56        int result = 0;
57        for (int i = 0; i < n; ++i) {
58            result += canReachEnd(i, 1);
59        }
60      
61        return result;
62    }
63};
64
1function oddEvenJumps(arr: number[]): number {
2    const n = arr.length;
3  
4    // Map to store value -> index pairs for finding next valid jumps
5    const valueToIndex = new Map<number, number>();
6  
7    // Memoization table: memo[i][jumpType]
8    // jumpType: 0 = even jump, 1 = odd jump
9    const memo: number[][] = Array(n).fill(null).map(() => Array(2).fill(0));
10  
11    // Next jump indices: nextJump[i][jumpType]
12    // Stores the index we jump to from position i
13    const nextJump: number[][] = Array(n).fill(null).map(() => Array(2).fill(-1));
14  
15    // Build next jump indices from right to left
16    for (let i = n - 1; i >= 0; i--) {
17        // Get all entries from the map and sort them
18        const sortedEntries = Array.from(valueToIndex.entries()).sort((a, b) => a[0] - b[0]);
19      
20        // For odd jumps (jump to smallest value >= current)
21        // Find the first value that is >= arr[i]
22        let oddJumpIndex = -1;
23        for (const [value, index] of sortedEntries) {
24            if (value >= arr[i]) {
25                oddJumpIndex = index;
26                break;
27            }
28        }
29        nextJump[i][1] = oddJumpIndex;
30      
31        // For even jumps (jump to largest value <= current)
32        // Find the last value that is <= arr[i]
33        let evenJumpIndex = -1;
34        for (const [value, index] of sortedEntries) {
35            if (value <= arr[i]) {
36                evenJumpIndex = index;
37            } else {
38                break;
39            }
40        }
41        nextJump[i][0] = evenJumpIndex;
42      
43        // Update map with current position
44        valueToIndex.set(arr[i], i);
45    }
46  
47    // DFS with memoization to check if we can reach the end
48    // i: current position, jumpType: 0 for even jump, 1 for odd jump
49    const canReachEnd = (i: number, jumpType: number): number => {
50        // Base case: reached the last index
51        if (i === n - 1) {
52            return 1;
53        }
54      
55        // No valid jump from this position
56        if (nextJump[i][jumpType] === -1) {
57            return 0;
58        }
59      
60        // Return memoized result if already computed
61        if (memo[i][jumpType] !== 0) {
62            return memo[i][jumpType];
63        }
64      
65        // Make the jump and alternate jump type (XOR with 1)
66        memo[i][jumpType] = canReachEnd(nextJump[i][jumpType], jumpType ^ 1);
67        return memo[i][jumpType];
68    };
69  
70    // Count all starting positions that can reach the end
71    // First jump is always odd (jumpType = 1)
72    let result = 0;
73    for (let i = 0; i < n; i++) {
74        result += canReachEnd(i, 1);
75    }
76  
77    return result;
78}
79

Time and Space Complexity

Time Complexity: O(n log n)

The time complexity breaks down as follows:

  • Building the graph g requires iterating through the array once in reverse order (O(n)), and for each element:
    • Performing bisect_left operation on SortedDict: O(log n)
    • Performing bisect_right operation on SortedDict: O(log n)
    • Inserting into SortedDict: O(log n)
    • Total for graph building: O(n log n)
  • The DFS with memoization visits each state (i, k) at most once, where i ranges from 0 to n-1 and k is either 0 or 1, giving us O(2n) = O(n) possible states
  • Each DFS call does O(1) work (excluding recursive calls)
  • The final sum calls DFS for each starting position: O(n)
  • Overall: O(n log n) + O(n) = O(n log n)

Space Complexity: O(n)

The space complexity consists of:

  • Graph g: O(n × 2) = O(n) for storing two values per position
  • SortedDict sd: O(n) in the worst case when all elements are unique
  • Cache for memoization: O(n × 2) = O(n) for storing results of all possible states
  • Recursion stack: O(n) in the worst case when jumps form a chain
  • Overall: O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Handling Duplicate Values

Pitfall: When multiple indices have the same value, forgetting that we must jump to the leftmost (earliest) index among them.

Wrong approach:

# This might store the rightmost index for duplicates
sorted_dict[current_value] = current_index

Why it fails: If we process left-to-right and continuously update the SortedDict, we'd end up with the rightmost index for duplicate values, violating the "jump to leftmost" rule.

Solution: Process the array from right-to-left. When we encounter a duplicate value, the current index (being further left) naturally overwrites the previous one:

# Process right to left
for current_index in range(array_length - 1, -1, -1):
    # Later in the loop:
    sorted_dict[current_value] = current_index  # Automatically keeps leftmost

2. Confusion Between bisect_left and bisect_right

Pitfall: Using the wrong bisect function or incorrectly adjusting the index for even/odd jumps.

Wrong approach for even jumps:

# Incorrect: This finds smallest value >= current_value
position = sorted_dict.bisect_left(current_value)
next_jump_indices[current_index][0] = sorted_dict.values()[position]

Why it fails: Even jumps need the largest value ≤ current_value, not the smallest value ≥ current_value.

Solution: Use bisect_right and subtract 1 for even jumps:

# Correct for even jumps (largest value <= current_value)
position = sorted_dict.bisect_right(current_value) - 1
if position >= 0:
    next_jump_indices[current_index][0] = sorted_dict.values()[position]

3. Forgetting the Jump Type Alternation

Pitfall: Not properly alternating between odd and even jumps, or starting with the wrong jump type.

Wrong approach:

# Incorrect: Always using the same jump type
return can_reach_end(next_jump_indices[index][1], 1)  # Always odd

# Or forgetting first jump is always odd
return sum(can_reach_end(start_index, 0) for start_index in range(array_length))

Solution: Use XOR to alternate jump types and always start with odd (1):

# Alternate jump types using XOR
return can_reach_end(next_jump_indices[index][jump_type], jump_type ^ 1)

# Always start with odd jump
return sum(can_reach_end(start_index, 1) for start_index in range(array_length))

4. Not Handling Edge Cases Properly

Pitfall: Forgetting that the last index is always a valid ending position, or not handling arrays with only one element.

Wrong approach:

# Forgetting to check if already at last index
def can_reach_end(index: int, jump_type: int) -> bool:
    if next_jump_indices[index][jump_type] == -1:
        return False
    # Missing base case check

Solution: Always check the base case first:

def can_reach_end(index: int, jump_type: int) -> bool:
    # Check if already at destination
    if index == array_length - 1:
        return True
    # Then check for valid jumps
    if next_jump_indices[index][jump_type] == -1:
        return False

5. Inefficient Data Structure Choice

Pitfall: Using a regular dictionary or list to maintain sorted order, leading to O(n²) complexity.

Wrong approach:

# Inefficient: manually sorting after each insertion
values_dict = {}
for i in range(n-1, -1, -1):
    sorted_values = sorted(values_dict.keys())  # O(n log n) each time!
    # Find next jump position...

Solution: Use SortedDict which maintains sorted order with O(log n) operations:

from sortedcontainers import SortedDict
sorted_dict = SortedDict()
# O(log n) for bisect operations and insertions
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

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

Load More