Facebook Pixel

1964. Find the Longest Valid Obstacle Course at Each Position

HardBinary Indexed TreeArrayBinary Search
Leetcode Link

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 to i
  • 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:

  1. It finds all previously seen obstacles with height ≤ current obstacle height
  2. Queries the maximum length among those sequences
  3. Extends that sequence by 1 to include the current obstacle
  4. 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.

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

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:

  1. Query the maximum value in a range (all heights ≤ current height)
  2. 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 indices 1 to x. This gives us the longest course among all heights ≤ current height.
  • update(x, v): Updates position x with value v if v is greater than the current value at x.

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:

  1. Find its compressed index using binary search: bisect_left(nums, x) + 1. We add 1 because BIT uses 1-based indexing.
  2. Query the maximum length among all heights ≤ x: tree.query(i). This gives us the longest valid course we can extend.
  3. The length at current position is query_result + 1 (adding current obstacle).
  4. 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 Evaluator

Example 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)) takes O(n log n) time, where n 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, taking O(log n) time
    • tree.query(i) traverses up the BIT using the operation x -= x & -x, which visits at most O(log n) nodes
    • tree.update(i, ans[-1]) traverses up the BIT using the operation x += x & -x, which also visits at most O(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 most n unique values: O(n)
  • Binary Indexed Tree stores an array c of size n + 1: O(n)
  • ans array stores n 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More