Before attempting this problem, you should be comfortable with:
For each position, we want the longest increasing subsequence ending at that position where each element is less than or equal to the next. This is a variant of the classic LIS problem. We can use memoization: for each index and the previous element chosen, we either include the current element in our sequence (if valid) or skip it.
dp[i][prev] representing the longest valid sequence considering elements from 0 to i, where prev is the index of the last chosen element.dfs(i, prev):i < 0, return 0.dfs(i - 1, prev).dfs(i - 1, i).dfs(n - 1, n) to fill the table, then construct the result array from the memoized values.class Solution:
def longestObstacleCourseAtEachPosition(self, obstacles: List[int]) -> List[int]:
n = len(obstacles)
dp = [[-1] * (n + 1) for _ in range(n)]
def dfs(i, prev):
if i < 0:
return 0
if dp[i][prev] != -1:
return dp[i][prev]
res = dfs(i - 1, prev)
if prev == n or obstacles[prev] >= obstacles[i]:
res = max(res, 1 + dfs(i - 1, i))
dp[i][prev] = res
return res
dfs(n - 1, n)
return [1] + [1 + dp[i - 1][i] for i in range(1, n)]We can optimize the LIS approach using binary search. Maintain a dp array where dp[i] represents the smallest ending value of all increasing subsequences of length i + 1. For each new element, we find where it fits using binary search (finding the first element greater than it), which tells us the longest subsequence we can extend. We then update that position with the current value to keep subsequences as extensible as possible.
dp array of size n + 1 filled with a large value (representing "no sequence of this length yet").dp where the value is greater than the current obstacle.dp[index] with the current obstacle value.index + 1 as the result for this position.This is a space-optimized version of the previous approach. Instead of preallocating an array of size n + 1, we start with an empty array and only grow it as needed. When a new element extends the longest sequence found so far, we append it; otherwise, we update an existing position. This uses only as much space as the length of the longest subsequence.
dp array.dp where the value is greater than the current obstacle.dp, append the obstacle (new longest length).dp[index] with the current obstacle.index + 1 as the result for this position.class Solution:
def longestObstacleCourseAtEachPosition(self, obstacles: List[int]) -> List[int]:
res = []
dp = []
for num in obstacles:
index = bisect.bisect_right(dp, num)
res.append(index + 1)
if index == len(dp):
dp.append(num)
else:
dp[index] = num
return resThis problem allows equal elements in the subsequence (non-strictly increasing), so we need upper_bound (first element greater than target), not lower_bound (first element greater than or equal to target). Using lower bound would treat equal elements as strictly increasing and produce incorrect results.
Standard Longest Increasing Subsequence requires strictly increasing elements and uses lower_bound. This problem requires non-decreasing elements (allowing duplicates), which changes the binary search condition. Applying the standard LIS algorithm directly will undercount valid sequences containing equal consecutive elements.
After finding the insertion position using binary search, the dp array must be updated with the current element. Forgetting this update means the algorithm does not track the optimal ending values for each sequence length, leading to incorrect results for subsequent elements.
The binary search returns an index in the dp array, but the answer is the length of the longest valid course. The result for each position is index + 1 (since dp is 0-indexed and the index represents sequences of length index + 1). Returning index directly gives answers that are off by one.
The first obstacle always has a course length of 1 since there are no previous obstacles. Some implementations may incorrectly initialize or skip this case, especially when using recursive approaches that need careful base case handling.