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
0
toi
- 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 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 indices1
tox
. This gives us the longest course among all heights ≤ current height.update(x, v)
: Updates positionx
with valuev
ifv
is 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
i
with 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
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
94
1class 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}
81
1class 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};
65
1// 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}
79
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, wheren
is the number of obstacles - The main loop iterates through all
n
obstacles - 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:
nums
array stores at mostn
unique values:O(n)
- Binary Indexed Tree stores an array
c
of sizen + 1
:O(n)
ans
array storesn
results: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 most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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 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
Want a Structured Path to Master System Design Too? Don’t Miss This!