38. Longest Increasing Subsequence Length
Origin: HackerRank Prep Kit #38
题目描述
给定一个整数数组 quality,返回最长严格递增子序列的长度。
示例
Example 1
Input: quality = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
Explanation: 最长严格递增子序列是 [2, 5, 7, 101] 或 [2, 3, 7, 18],长度 4。
题目给了 tails 数组的逐步过程:
- 10: tails = [10]
- 9 替换 10: tails = [9]
- 2 替换 9: tails = [2]
- 5 扩展: tails = [2, 5]
- 3 替换 5: tails = [2, 3]
- 7 扩展: tails = [2, 3, 7]
- 101 扩展: tails = [2, 3, 7, 101]
- 18 替换 101: tails = [2, 3, 7, 18]
Example 2
Input: quality = [-2, -1, 0, 1, -3, 2, 2, 3]
Output: 6
Explanation: 最长是 [-2, -1, 0, 1, 2, 3] 或 [-3, -1, 0, 1, 2, 3],长度 6。
Sample Input 0
1 1 5
Output: 1(数组只有一个元素 5)
约束
- 0 <= n <= 100000
- -10^9 <= quality[i] <= 10
- 时间复杂度必须 O(n log n) 或更好(n 最大 100000,O(n²) 会超时)
关键信息
- 严格递增(不能有相等元素)
- n 最大 100000 → 必须用 O(n log n) 的 tails + 二分解法
- 题目 explanation 直接给了 tails 数组的过程,提示用这个方法
Resolution
我的解法(findIndex 版本,O(n²),AC 但可能超时)
function computeLongestIncreasingSubsequenceLength(n: number, quality: number[]): number {
if (n === 0) return 0
const tails: number[] = []
let maxLength = 0
for (let i = 0; i < n; i++) {
const cur = quality[i]
if (tails.length > 0) {
let index = tails.findIndex((v) => cur < v)
if (index !== -1) {
tails[index] = cur
}
}
if (tails.length === 0 || cur > tails[tails.length - 1]) {
tails.push(cur)
}
maxLength = Math.max(maxLength, tails.length)
}
return maxLength
}标准解法(二分查找,O(n log n) AC)
function computeLongestIncreasingSubsequenceLength(n: number, quality: number[]): number {
if (n === 0) return 0
const tails: number[] = []
for (let i = 0; i < n; i++) {
const cur = quality[i]
if (tails.length === 0 || cur > tails[tails.length - 1]) {
tails.push(cur)
} else {
// 二分查找第一个 >= cur 的位置
let left = 0
let right = tails.length - 1
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (tails[mid] >= cur) {
right = mid
} else {
left = mid + 1
}
}
tails[left] = cur
}
}
return tails.length
}解题思路
经典 LIS(LeetCode 300)。用 tails 数组 + 二分查找,O(n log n)。
tails[i] 表示”长度为 i+1 的递增子序列的最小末尾值”。遍历每个元素:
- 比 tails 末尾大 → push(扩展)
- 否则 → 二分查找第一个 >= cur 的位置,替换它
为什么替换不影响长度正确性(用 [3, 4, 1, X] 推导):
遍历到 1 时 tails = [3, 4],1 替换 3 → tails = [1, 4]。此时:
- 长度 1 的末尾从 3 变成 1(更小,未来更容易扩展)
- 长度 2 的记录(4)没被碰,“存在长度 2 的子序列”这个事实没丢
后续遍历 X 时:
- X = 3:3 > 1 能接在 [1] 后面,3 < 4 替换 4 → tails = [1, 3],末尾更优
- X = -1:-1 < 1 替换 1 → tails = [-1, 4],长度 1 的末尾更优
- X = 5:5 > 4 扩展 → tails = [1, 4, 5],真实子序列是 [3, 4, 5],但 1 只是占位
核心理解:tails 是一个”末尾存当前子序列信息 + 前面存后续可能更优的子序列信息”的结构。每个位置不存真实子序列,存的是那个长度的最优末尾值。替换只动一个位置,其他位置的长度记录原封不动。
踩坑
- findIndex 是 O(n),题目要求 O(n log n),n=100000 会超时。必须用二分查找
- 严格递增:二分查找条件用
>=(第一个大于等于 cur 的位置),不是>
复杂度
- 时间:O(n log n),每个元素做一次二分查找
- 空间:O(n),tails 数组最长 n