1964. Find the Longest Valid Obstacle Course at Each Position
Problem Description
You are given an array obstacles where each element represents the height of an obstacle. Your task is to find the longest obstacle course that can be built ending at each position.
For each index i in the array, you need to determine the longest sequence of obstacles that:
- Ends with the obstacle at position
i(this obstacle must be included) - Only includes obstacles from positions
0toi - Maintains the original order of obstacles from the array
- Each obstacle in the sequence is greater than or equal to the previous one (non-decreasing heights)
The output should be an array where ans[i] represents the length of the longest valid obstacle course ending at position i.
For example, if obstacles = [1, 2, 3, 2]:
- At index 0: The longest course is just
[1], length = 1 - At index 1: The longest course is
[1, 2], length = 2 - At index 2: The longest course is
[1, 2, 3], length = 3 - At index 3: The longest course is
[1, 2, 2]or[2, 2], length = 3
The solution uses a Binary Indexed Tree (Fenwick Tree) to efficiently track the longest increasing subsequence ending at or before each height value. For each obstacle:
- It finds all previously seen obstacles with height ≤ current obstacle height
- Queries the maximum length among those sequences
- Extends that sequence by 1 to include the current obstacle
- Updates the tree with this new length
The coordinate compression technique (sorting unique values) is used to map obstacle heights to indices in the Binary Indexed Tree, allowing efficient range queries and updates in O(log n) time.
Intuition
This problem is essentially asking for the Longest Non-Decreasing Subsequence (LIS variant) ending at each position. The key insight is that for each obstacle at position i, we need to find the longest valid course among all previous obstacles that have height ≤ current obstacle's height.
The naive approach would be to check all previous obstacles for each position, giving us O(n²) complexity. However, we can optimize this by recognizing a pattern: we're repeatedly asking "what's the maximum length among all sequences ending with height ≤ h?"
This leads us to think about maintaining some data structure that can:
- Query the maximum value in a range (all heights ≤ current height)
- Update a value at a specific position (update the length for current height)
A Binary Indexed Tree (BIT) is perfect for this because it supports both range maximum queries and point updates in O(log n) time.
The next challenge is handling the range of possible heights. Since obstacle heights could be large, we use coordinate compression - mapping the actual heights to indices 1, 2, 3, ... based on their relative order. This ensures our BIT only needs to be as large as the number of unique heights.
The algorithm flow becomes:
- For each obstacle, find its compressed index (position in sorted unique values)
- Query BIT for maximum length among all heights up to this index
- The answer for current position is this maximum + 1
- Update BIT at this index with the new length
This way, we maintain a running record of the best possible length for each height value, allowing us to build upon previous results efficiently.
Learn more about Binary Search patterns.
Solution Implementation
1from typing import List 2from bisect import bisect_left 3 4 5class BinaryIndexedTree: 6 """ 7 Binary Indexed Tree (Fenwick Tree) modified for range maximum queries. 8 Instead of sum operations, this BIT maintains maximum values. 9 """ 10 __slots__ = ["size", "tree"] 11 12 def __init__(self, size: int): 13 """ 14 Initialize the Binary Indexed Tree. 15 16 Args: 17 size: The size of the tree (number of elements) 18 """ 19 self.size = size 20 self.tree = [0] * (size + 1) # 1-indexed array for BIT operations 21 22 def update(self, index: int, value: int) -> None: 23 """ 24 Update the tree by propagating the maximum value upwards. 25 26 Args: 27 index: The position to update (1-indexed) 28 value: The value to compare and potentially update with 29 """ 30 while index <= self.size: 31 # Update current position with maximum value 32 self.tree[index] = max(self.tree[index], value) 33 # Move to next index affected by this position 34 # index & -index gives the lowest set bit 35 index += index & -index 36 37 def query(self, index: int) -> int: 38 """ 39 Query the maximum value in range [1, index]. 40 41 Args: 42 index: The right boundary of the query range (1-indexed) 43 44 Returns: 45 The maximum value in the range [1, index] 46 """ 47 max_value = 0 48 while index > 0: 49 # Take maximum of current value and tree value at this index 50 max_value = max(max_value, self.tree[index]) 51 # Move to previous range by removing lowest set bit 52 index -= index & -index 53 return max_value 54 55 56class Solution: 57 def longestObstacleCourseAtEachPosition(self, obstacles: List[int]) -> List[int]: 58 """ 59 Find the length of the longest obstacle course ending at each position. 60 An obstacle course is a subsequence where each element is >= the previous one. 61 62 Args: 63 obstacles: List of obstacle heights 64 65 Returns: 66 List where each element represents the length of the longest 67 non-decreasing subsequence ending at that position 68 """ 69 # Create a sorted list of unique obstacle heights for coordinate compression 70 unique_heights = sorted(set(obstacles)) 71 num_unique = len(unique_heights) 72 73 # Initialize Binary Indexed Tree with size equal to number of unique heights 74 fenwick_tree = BinaryIndexedTree(num_unique) 75 76 result = [] 77 78 for obstacle_height in obstacles: 79 # Map the obstacle height to its compressed coordinate (1-indexed) 80 # bisect_left finds the position where height would be inserted 81 compressed_index = bisect_left(unique_heights, obstacle_height) + 1 82 83 # Query the maximum length of subsequence ending with height <= current 84 max_length_before = fenwick_tree.query(compressed_index) 85 86 # Current position extends the best subsequence by 1 87 current_length = max_length_before + 1 88 result.append(current_length) 89 90 # Update the tree with the new length at this height 91 fenwick_tree.update(compressed_index, current_length) 92 93 return result 941class BinaryIndexedTree { 2 private int size; 3 private int[] tree; 4 5 /** 6 * Initialize a Binary Indexed Tree with given size 7 * @param size The size of the tree 8 */ 9 public BinaryIndexedTree(int size) { 10 this.size = size; 11 this.tree = new int[size + 1]; // 1-indexed array for BIT 12 } 13 14 /** 15 * Update the tree by setting maximum value at index and propagating upwards 16 * @param index The index to update (1-based) 17 * @param value The value to update with (takes maximum) 18 */ 19 public void update(int index, int value) { 20 // Traverse upwards in the tree using BIT indexing 21 while (index <= size) { 22 tree[index] = Math.max(tree[index], value); 23 index += index & -index; // Move to next index affected by this update 24 } 25 } 26 27 /** 28 * Query the maximum value from indices 1 to index 29 * @param index The ending index for the query (1-based) 30 * @return The maximum value in range [1, index] 31 */ 32 public int query(int index) { 33 int maxValue = 0; 34 // Traverse downwards in the tree to collect all relevant values 35 while (index > 0) { 36 maxValue = Math.max(maxValue, tree[index]); 37 index -= index & -index; // Move to parent index in BIT 38 } 39 return maxValue; 40 } 41} 42 43class Solution { 44 /** 45 * Find the length of the longest obstacle course ending at each position 46 * where each course must have non-decreasing heights 47 * @param obstacles Array of obstacle heights 48 * @return Array where ans[i] is the length of longest course ending at position i 49 */ 50 public int[] longestObstacleCourseAtEachPosition(int[] obstacles) { 51 // Create a sorted copy to map values to compressed indices 52 int[] sortedObstacles = obstacles.clone(); 53 Arrays.sort(sortedObstacles); 54 55 int n = obstacles.length; 56 int[] result = new int[n]; 57 58 // Initialize BIT for tracking maximum course lengths 59 BinaryIndexedTree fenwickTree = new BinaryIndexedTree(n); 60 61 // Process each obstacle in order 62 for (int i = 0; i < n; ++i) { 63 int currentHeight = obstacles[i]; 64 65 // Find compressed index using binary search (1-based for BIT) 66 int compressedIndex = Arrays.binarySearch(sortedObstacles, currentHeight) + 1; 67 68 // Query maximum course length for all heights <= current height 69 int maxPreviousLength = fenwickTree.query(compressedIndex); 70 71 // Current position extends the best previous course by 1 72 result[i] = maxPreviousLength + 1; 73 74 // Update BIT with new course length at this height 75 fenwickTree.update(compressedIndex, result[i]); 76 } 77 78 return result; 79 } 80} 811class BinaryIndexedTree { 2private: 3 int size; 4 vector<int> tree; 5 6public: 7 // Constructor: Initialize BIT with given size 8 BinaryIndexedTree(int n) : size(n), tree(n + 1, 0) {} 9 10 // Update the BIT by setting maximum value at index x 11 void update(int index, int value) { 12 // Propagate the update upwards in the tree 13 while (index <= size) { 14 tree[index] = max(tree[index], value); 15 // Move to next index by adding the lowest set bit 16 index += index & (-index); 17 } 18 } 19 20 // Query the maximum value from indices 1 to x 21 int query(int index) { 22 int maxValue = 0; 23 // Traverse downwards in the tree to get the maximum 24 while (index > 0) { 25 maxValue = max(maxValue, tree[index]); 26 // Move to parent by removing the lowest set bit 27 index -= index & (-index); 28 } 29 return maxValue; 30 } 31}; 32 33class Solution { 34public: 35 vector<int> longestObstacleCourseAtEachPosition(vector<int>& obstacles) { 36 // Create a sorted copy of obstacles for coordinate compression 37 vector<int> sortedObstacles = obstacles; 38 sort(sortedObstacles.begin(), sortedObstacles.end()); 39 40 int n = obstacles.size(); 41 vector<int> result(n); 42 43 // Initialize Binary Indexed Tree for range maximum queries 44 BinaryIndexedTree fenwickTree(n); 45 46 // Process each obstacle in original order 47 for (int i = 0; i < n; ++i) { 48 int currentObstacle = obstacles[i]; 49 50 // Find the position of current obstacle in sorted array (1-indexed) 51 auto iterator = lower_bound(sortedObstacles.begin(), sortedObstacles.end(), currentObstacle); 52 int compressedIndex = distance(sortedObstacles.begin(), iterator) + 1; 53 54 // Query maximum length ending with values <= current obstacle 55 int maxLength = fenwickTree.query(compressedIndex); 56 result[i] = maxLength + 1; 57 58 // Update the BIT with the new length at this position 59 fenwickTree.update(compressedIndex, result[i]); 60 } 61 62 return result; 63 } 64}; 651// Binary Indexed Tree (Fenwick Tree) for range maximum queries 2// Array to store the tree values (1-indexed) 3let treeSize: number; 4let treeArray: number[]; 5 6// Initialize the Binary Indexed Tree with given size 7function initializeTree(n: number): void { 8 treeSize = n; 9 treeArray = Array(n + 1).fill(0); 10} 11 12// Update the tree at position x with maximum value v 13// Uses the BIT update pattern: move to next index by adding lowbit 14function update(position: number, value: number): void { 15 while (position <= treeSize) { 16 treeArray[position] = Math.max(treeArray[position], value); 17 // Move to next position by adding the lowest set bit 18 position += position & -position; 19 } 20} 21 22// Query the maximum value from index 1 to x 23// Uses the BIT query pattern: move to previous index by removing lowbit 24function query(position: number): number { 25 let maxValue = 0; 26 while (position > 0) { 27 maxValue = Math.max(maxValue, treeArray[position]); 28 // Move to previous position by removing the lowest set bit 29 position -= position & -position; 30 } 31 return maxValue; 32} 33 34// Binary search to find the leftmost position where sortedArray[pos] >= target 35function binarySearch(sortedArray: number[], target: number): number { 36 let left = 0; 37 let right = sortedArray.length; 38 39 while (left < right) { 40 const mid = Math.floor((left + right) / 2); 41 if (sortedArray[mid] >= target) { 42 right = mid; 43 } else { 44 left = mid + 1; 45 } 46 } 47 48 return left; 49} 50 51// Main function: Find the length of the longest obstacle course at each position 52// For each obstacle, find the longest non-decreasing subsequence ending at that position 53function longestObstacleCourseAtEachPosition(obstacles: number[]): number[] { 54 // Create a sorted copy of obstacles for coordinate compression 55 const sortedObstacles: number[] = [...obstacles]; 56 sortedObstacles.sort((a, b) => a - b); 57 58 const n: number = sortedObstacles.length; 59 const result: number[] = []; 60 61 // Initialize the Binary Indexed Tree 62 initializeTree(n); 63 64 // Process each obstacle in the original order 65 for (let i = 0; i < n; i++) { 66 // Find the compressed coordinate (1-indexed) for current obstacle 67 const compressedIndex: number = binarySearch(sortedObstacles, obstacles[i]) + 1; 68 69 // Query the maximum length of obstacle course ending with values <= current obstacle 70 const maxLength: number = query(compressedIndex) + 1; 71 result[i] = maxLength; 72 73 // Update the tree with the new maximum length at this compressed coordinate 74 update(compressedIndex, maxLength); 75 } 76 77 return result; 78} 79Solution Approach
The solution uses a Binary Indexed Tree (Fenwick Tree) with coordinate compression to efficiently solve this problem.
Step 1: Coordinate Compression
nums = sorted(set(obstacles))
We extract unique obstacle heights and sort them. This maps potentially large height values to indices [1, 2, ..., n]. For example, if obstacles contain heights [5, 100, 5, 50], the unique sorted values would be [5, 50, 100], which map to indices [1, 2, 3].
Step 2: Binary Indexed Tree Setup
tree = BinaryIndexedTree(n)
We initialize a BIT of size n (number of unique heights). The BIT maintains the maximum length of obstacle courses for each height level.
The BIT implementation provides two key operations:
query(x): Returns the maximum value from indices1tox. This gives us the longest course among all heights ≤ current height.update(x, v): Updates positionxwith valuevifvis greater than the current value atx.
Step 3: Process Each Obstacle
for x in obstacles: i = bisect_left(nums, x) + 1 ans.append(tree.query(i) + 1) tree.update(i, ans[-1])
For each obstacle height x:
- Find its compressed index using binary search:
bisect_left(nums, x) + 1. We add 1 because BIT uses 1-based indexing. - Query the maximum length among all heights ≤
x:tree.query(i). This gives us the longest valid course we can extend. - The length at current position is
query_result + 1(adding current obstacle). - Update the BIT at index
iwith this new length, so future obstacles can build upon it.
Time Complexity: O(n log n) where n is the number of obstacles. The sorting takes O(n log n), and for each obstacle, we perform O(log n) operations (binary search, BIT query, and BIT update).
Space Complexity: O(n) for storing the unique heights and the BIT structure.
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 walk through the solution with obstacles = [2, 5, 1, 5, 3].
Step 1: Coordinate Compression
- Extract unique heights:
{2, 5, 1, 3} - Sort them:
[1, 2, 3, 5] - These map to BIT indices:
1, 2, 3, 4(1-based indexing)
Step 2: Initialize BIT
- Create BIT of size 4 (for 4 unique heights)
- Initially all values are 0:
BIT = [0, 0, 0, 0]
Step 3: Process Each Obstacle
Position 0: height = 2
- Find compressed index: 2 is at position 1 in
[1, 2, 3, 5], so index = 2 - Query BIT(2): maximum among indices 1-2 = 0
- Length at position 0 = 0 + 1 = 1
- Update BIT at index 2 with value 1
BIT state: [0, 1, 0, 0],ans = [1]
Position 1: height = 5
- Find compressed index: 5 is at position 3 in
[1, 2, 3, 5], so index = 4 - Query BIT(4): maximum among indices 1-4 = 1 (from height 2)
- Length at position 1 = 1 + 1 = 2
- Update BIT at index 4 with value 2
BIT state: [0, 1, 0, 2],ans = [1, 2]
Position 2: height = 1
- Find compressed index: 1 is at position 0 in
[1, 2, 3, 5], so index = 1 - Query BIT(1): maximum among indices 1-1 = 0
- Length at position 2 = 0 + 1 = 1
- Update BIT at index 1 with value 1
BIT state: [1, 1, 0, 2],ans = [1, 2, 1]
Position 3: height = 5
- Find compressed index: 5 is at position 3 in
[1, 2, 3, 5], so index = 4 - Query BIT(4): maximum among indices 1-4 = 2 (previous sequence ending at height 5)
- Length at position 3 = 2 + 1 = 3
- Update BIT at index 4 with value 3
BIT state: [1, 1, 0, 3],ans = [1, 2, 1, 3]
Position 4: height = 3
- Find compressed index: 3 is at position 2 in
[1, 2, 3, 5], so index = 3 - Query BIT(3): maximum among indices 1-3 = 1 (can build on height 1 or 2)
- Length at position 4 = 1 + 1 = 2
- Update BIT at index 3 with value 2
BIT state: [1, 1, 2, 3],ans = [1, 2, 1, 3, 2]
Final Result: [1, 2, 1, 3, 2]
This means:
- Position 0: Course
[2]has length 1 - Position 1: Course
[2, 5]has length 2 - Position 2: Course
[1]has length 1 - Position 3: Course
[2, 5, 5]or[1, 5, 5]has length 3 - Position 4: Course
[2, 3]or[1, 3]has length 2
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity analysis breaks down as follows:
- Creating the sorted unique values:
sorted(set(obstacles))takesO(n log n)time, wherenis the number of obstacles - The main loop iterates through all
nobstacles - For each obstacle:
bisect_left()performs binary search on the sorted array, takingO(log n)timetree.query(i)traverses up the BIT using the operationx -= x & -x, which visits at mostO(log n)nodestree.update(i, ans[-1])traverses up the BIT using the operationx += x & -x, which also visits at mostO(log n)nodes
- Total for the loop:
n × O(log n) = O(n log n) - Overall time complexity:
O(n log n) + O(n log n) = O(n log n)
Space Complexity: O(n)
The space complexity analysis:
numsarray stores at mostnunique values:O(n)- Binary Indexed Tree stores an array
cof sizen + 1:O(n) ansarray storesnresults:O(n)- Total space complexity:
O(n) + O(n) + O(n) = O(n)
Where n is the number of obstacles in the input array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect BIT Indexing (0-based vs 1-based)
One of the most common mistakes is mixing up 0-based and 1-based indexing. Binary Indexed Trees traditionally use 1-based indexing because the bit manipulation operations (index & -index) don't work correctly with index 0.
Incorrect Implementation:
# Wrong: Using 0-based indexing compressed_index = bisect_left(unique_heights, obstacle_height) # Returns 0-based index fenwick_tree.query(compressed_index) # BIT expects 1-based index
Correct Implementation:
# Correct: Convert to 1-based indexing compressed_index = bisect_left(unique_heights, obstacle_height) + 1 fenwick_tree.query(compressed_index)
2. Using Standard BIT Sum Operations Instead of Maximum
The traditional BIT is designed for prefix sum queries. This problem requires finding the maximum value in a range, not the sum.
Incorrect Implementation:
class BinaryIndexedTree: def update(self, index: int, value: int) -> None: while index <= self.size: self.tree[index] += value # Wrong: Adding instead of taking maximum index += index & -index def query(self, index: int) -> int: sum_value = 0 while index > 0: sum_value += self.tree[index] # Wrong: Summing instead of taking maximum index -= index & -index return sum_value
Correct Implementation:
class BinaryIndexedTree: def update(self, index: int, value: int) -> None: while index <= self.size: self.tree[index] = max(self.tree[index], value) # Take maximum index += index & -index def query(self, index: int) -> int: max_value = 0 while index > 0: max_value = max(max_value, self.tree[index]) # Take maximum index -= index & -index return max_value
3. Forgetting Coordinate Compression for Large Values
Without coordinate compression, if obstacle heights have large values (e.g., up to 10^7), creating a BIT of that size would cause memory issues and timeout.
Incorrect Approach:
# Wrong: Using actual height values as indices max_height = max(obstacles) fenwick_tree = BinaryIndexedTree(max_height) # Could be 10^7! for obstacle_height in obstacles: fenwick_tree.query(obstacle_height) # Direct use of height
Correct Approach:
# Correct: Compress coordinates first unique_heights = sorted(set(obstacles)) fenwick_tree = BinaryIndexedTree(len(unique_heights)) # Size is at most n for obstacle_height in obstacles: compressed_index = bisect_left(unique_heights, obstacle_height) + 1 fenwick_tree.query(compressed_index) # Use compressed index
4. Handling Duplicate Heights Incorrectly
When multiple obstacles have the same height, they should all be able to extend sequences ending at that height. The BIT update should use max to preserve the best result.
Potential Issue: If you mistakenly overwrite instead of taking the maximum during updates, you might lose optimal solutions for duplicate heights.
Prevention: The current implementation correctly uses max(self.tree[index], value) in the update method, ensuring that we keep the best (longest) sequence length for each height level.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!