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 indexj
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 indexj
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
toj
wherei < 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.
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:
-
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. -
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)
-
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. -
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 indexi
on an even jumpg[i][1]
: where to jump from indexi
on an odd jump
-
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
), returnFalse
- 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 EvaluatorExample 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 jumpg[0][1] = -1
- no valid odd jump- Return
False
❌
Starting from index 1:
dfs(1, 1)
- start with odd jumpg[1][1] = 4
- jump to index 4dfs(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 jumpg[2][1] = 3
- jump to index 3dfs(3, 0)
- now even jump from index 3g[3][0] = 4
- jump to index 4dfs(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 jumpg[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)
- Performing
- The DFS with memoization visits each state
(i, k)
at most once, wherei
ranges from0
ton-1
andk
is either0
or1
, giving usO(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
In a binary min heap, the maximum element can be found in:
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!