Hard
题目描述
你想要建造一些障碍赛跑路线。给你一个下标从 0 开始、长度为 n 的整数数组 obstacles,其中 obstacles[i] 表示第 i 个障碍的高度。
对于每个介于 0 和 n - 1 之间(包括 0 和 n - 1)的下标 i,在满足下述条件的前提下,请你找出 obstacles 能构成的最长障碍路线的长度:
- 你可以选择下标 0 到 i 之间(包括 0 和 i)的任意个障碍。
- 你必须按障碍在 obstacles 中的出现顺序布置这些障碍。
- 除第一个障碍外,路线中每个障碍的高度都必须和前一个障碍的高度相同或者更高。
返回长度为 n 的答案数组 ans,其中 ans[i] 是上面所述的下标 i 处的最长障碍路线的长度。
示例 1:
输入:obstacles = [1,2,3,2]
输出:[1,2,3,3]
解释:每个位置的最长有效障碍路线如下:
- i = 0: [1],[1] 的长度为 1
- i = 1: [1,2],[1,2] 的长度为 2
- i = 2: [1,2,3],[1,2,3] 的长度为 3
- i = 3: [1,2,3,2],[1,2,2] 的长度为 3
示例 2:
输入:obstacles = [2,2,1]
输出:[1,2,1]
解释:每个位置的最长有效障碍路线如下:
- i = 0: [2],[2] 的长度为 1
- i = 1: [2,2],[2,2] 的长度为 2
- i = 2: [2,2,1],[1] 的长度为 1
示例 3:
输入:obstacles = [3,1,5,6,4,2]
输出:[1,1,2,3,2,2]
提示:
- n == obstacles.length
- 1 <= n <= 10^5
- 1 <= obstacles[i] <= 10^7
解题思路
这道题本质上是求最长非严格递增子序列(LIS 变种),对于每个位置 i,我们需要找出以 obstacles[i] 结尾的最长非递减子序列长度。
核心思路:
- 维护一个数组
tails,其中tails[len-1]表示长度为len的非递减子序列的最小结尾值 - 对于每个障碍
obstacles[i],使用二分查找在tails中找到第一个大于obstacles[i]的位置 - 如果找到这样的位置,说明可以延长现有序列;否则开始一个新的更长序列
- 更新
tails数组并记录当前位置的答案
关键点:
- 由于允许相等高度,我们需要找第一个大于当前元素的位置(而不是大于等于)
- 使用
upper_bound或手写二分查找来实现 tails数组始终保持有序,便于二分查找
时间复杂度分析:
- 对每个元素进行一次二分查找:O(n log n)
- 整体时间复杂度:O(n log n),空间复杂度:O(n)
这种方法相比动态规划的 O(n²) 解法有显著性能提升,特别适合处理大规模数据。
代码实现
class Solution {
public:
vector<int> longestObstacleCourseAtEachPosition(vector<int>& obstacles) {
vector<int> tails;
vector<int> result;
for (int obstacle : obstacles) {
auto it = upper_bound(tails.begin(), tails.end(), obstacle);
int pos = it - tails.begin();
if (it == tails.end()) {
tails.push_back(obstacle);
} else {
*it = obstacle;
}
result.push_back(pos + 1);
}
return result;
}
};
class Solution:
def longestObstacleCourseAtEachPosition(self, obstacles: List[int]) -> List[int]:
import bisect
tails = []
result = []
for obstacle in obstacles:
pos = bisect.bisect_right(tails, obstacle)
if pos == len(tails):
tails.append(obstacle)
else:
tails[pos] = obstacle
result.append(pos + 1)
return result
public class Solution {
public int[] LongestObstacleCourseAtEachPosition(int[] obstacles) {
var tails = new List<int>();
var result = new int[obstacles.Length];
for (int i = 0; i < obstacles.Length; i++) {
int obstacle = obstacles[i];
int pos = BinarySearch(tails, obstacle);
if (pos == tails.Count) {
tails.Add(obstacle);
} else {
tails[pos] = obstacle;
}
result[i] = pos + 1;
}
return result;
}
private int BinarySearch(List<int> tails, int target) {
int left = 0, right = tails.Count;
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
}
/**
* @param {number[]} obstacles
* @return {number[]}
*/
var longestObstacleCourseAtEachPosition = function(obstacles) {
const n = obstacles.length;
const result = new Array(n);
const tails = [];
for (let i = 0; i < n; i++) {
const height = obstacles[i];
// Binary search for the position to insert/replace
let left = 0, right = tails.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (tails[mid] <= height) {
left = mid + 1;
} else {
right = mid;
}
}
// If left equals tails.length, we append
if (left === tails.length) {
tails.push(height);
} else {
tails[left] = height;
}
result[i] = left + 1;
}
return result;
};
复杂度分析
| 复杂度类型 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 对每个元素进行二分查找 |
| 空间复杂度 | O(n) | tails 数组最多存储 n 个元素 |
相关题目
- . Longest Increasing Subsequence (Medium)