6.9 KiB
6.9 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-16 14:35 |
01 - 最长严格递增子序列(返回子序列)
题面
输入: 一个整数数组 nums
输出: 一个切片,表示 nums 的**最长严格递增子序列(Longest Increasing Subsequence)**的具体元素
要求:
- 子序列不要求连续,但必须保持原顺序
- 严格递增:后一项必须大于前一项(不能等于)
- ACM 模式:从 stdin 读取,stdout 输出结果
- 如有多个答案,返回任意一个即可
示例 1:
输入: [10, 9, 2, 5, 3, 7, 101, 18]
输出: [2, 3, 7, 101] (或 [2, 3, 7, 18])
示例 2:
输入: [0, 1, 0, 3, 2, 3]
输出: [0, 1, 2, 3]
示例 3 (严格递增):
输入: [3, 3, 3, 3]
输出: [3] (长度为 1,因为相等不算递增)
[!question] 💡 思考一下 如果题目要求的是非递减子序列(可以等于),解法需要怎么调整?
思路
方法一:O(n²) 动态规划(基础)
定义 dp[i] = 以 nums[i] 结尾的最长递增子序列长度。
转移方程:
dp[i] = \max \{ dp[j] \} + 1, \quad j < i,\; nums[j] < nums[i]
回溯时,从 dp 值最大的位置倒推,找到前驱元素即可恢复完整子序列。
flowchart LR
A["nums = [10, 9, 2, 5, 3, 7, 101, 18]"] --> B["计算 dp[i]"]
B --> C["dp = [1, 1, 1, 2, 2, 3, 4, 4]"]
C --> D["找到最大值的最后一个位置"]
D --> E["倒推前驱还原子序列"]
E --> F["[2, 3, 7, 101] 或 [2, 3, 7, 18]"]
优点: 思路直观,容易实现
缺点: O(n²) 时间复杂度,大数据量会超时
方法二:O(n log n) 贪心 + 二分 + 路径回溯(推荐)⭐
这是本题的核心考点。很多人只记得求长度的 O(n log n) 解法,但本题要求返回具体序列,需要额外技巧。
核心数据结构
维护两个数组:
| 数组 | 含义 |
|---|---|
tails[k] |
长度为 k+1 的所有递增子序列中,最小尾部元素的值 |
parent[i] |
以 nums[i] 结尾的 LIS 中,nums[i] 的前驱索引 |
关键洞察 🔑
[!warning] ⚠️ 易错点
tails数组不一定是真实的子序列!它只是帮助我们高效找到更长的子序列。真正恢复子序列要靠parent数组记录的路径。
算法步骤
flowchart TD
%% 定义所有节点
A["遍历每个元素 nums[i]"]
B{"在 tails 中<br/>二分查找"}
C["追加到 tails 末尾<br/>更新 parent[i]=prevIdx"]
D["用 nums[i] 替换<br/>tails[j]<br/>parent[i]=j-1<br/>对应的前驱索引"]
E["i++"]
F{"是否遍历完?"}
G["从 tails 末尾开始<br/>回溯 parent"]
H["得到逆序的子序列"]
I["翻转得到正序"]
%% 连线
A --> B
B -- "nums[i] > 所有 tails" --> C
B -- "tails[j-1] < nums[i]" --> D
C --> E
D --> E
E --> F
F -- "否" --> A
F -- "是" --> G
G --> H
H --> I
细节: 为了正确设置 parent[i],我们还需要一个数组 pos[k] 记录长度为 k 的子序列当前尾元素的索引。
完整流程演示
nums = [10, 9, 2, 5, 3, 7, 101, 18]
i=0: nums[0]=10, tails=[], pos=[]
tails=[10], pos=[0], parent[0]=-1
i=1: nums[1]=9, 9<10, 替换 tails[0]
tails=[9], pos=[1], parent[1]=-1
i=2: nums[2]=2, 2<9, 替换 tails[0]
tails=[2], pos=[2], parent[2]=-1
i=3: nums[3]=5, 5>2, 追加
tails=[2,5], pos=[2,3], parent[3]=2
i=4: nums[4]=3, 2<3≤5, 替换 tails[1]
tails=[2,3], pos=[2,4], parent[4]=2
i=5: nums[5]=7, 7>3, 追加
tails=[2,3,7], pos=[2,4,5], parent[5]=4
i=6: nums[6]=101, 101>7, 追加
tails=[2,3,7,101], pos=[2,4,5,6], parent[6]=5
i=7: nums[7]=18, 7<18≤101, 替换 tails[3]
tails=[2,3,7,18], pos=[2,4,5,7], parent[7]=5
LIS 长度 = 4, 从 pos[3]=7 开始回溯:
nums[7]=18 → parent[7]=5 → nums[5]=7 → parent[5]=4 → nums[4]=3 → parent[4]=2 → nums[2]=2 → parent[2]=-1
逆序: [18, 7, 3, 2] → 翻转 → [2, 3, 7, 18] ✓
代码提示
// 1. 初始化 tails []int, pos []int, parent []int{-1}
// 2. 遍历 i := 0 to n-1:
// - 二分搜索 tails,找到第一个 >= nums[i] 的位置 j
// - if j == len(tails): 追加,否则替换 tails[j]
// - 更新 pos[j] = i, parent[i] = pos[j-1] (if j > 0)
// 3. 从 pos[len(tails)-1] 沿 parent 倒推
// 4. 反转结果
技巧
[!tip] 小技巧: 二分搜索的使用
sort.SearchInts返回的是第一个 >= target 的位置,正好符合我们的需求。如果用标准库lower_bound语义,找的是>=;如果要处理非严格递增(允许等于),就改为找>。
[!note] ACM 模式注意事项
- 用
bufio.Scanner读行比fmt.Scan更高效- 输出格式通常是用空格分隔的数字
- 记得处理空数组边界情况
n == 0
代码
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return
}
// 解析输入: "10 9 2 5 3 7 101 18"
parts := strings.Fields(scanner.Text())
n := len(parts)
if n == 0 {
fmt.Println("[]")
return
}
nums := make([]int, n)
for i, p := range parts {
v, _ := strconv.Atoi(p)
nums[i] = v
}
// LIS 返回具体子序列
result := longestIncreasingSubsequence(nums)
// 输出结果
fmt.Print("[")
for i, v := range result {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println("]")
}
func longestIncreasingSubsequence(nums []int) []int {
n := len(nums)
if n == 0 {
return []int{}
}
// tails[k] = 长度为 k+1 的递增子序列的最小尾部值
tails := make([]int, 0, n)
// pos[k] = 该尾部值在原数组中的索引
pos := make([]int, n)
// parent[i] = 以 nums[i] 结尾的 LIS 中,前一个元素的索引
parent := make([]int, n)
for i := range parent {
parent[i] = -1
}
for i := 0; i < n; i++ {
// 二分查找: 在 tails 中找第一个 >= nums[i] 的位置
j := lowerBound(tails, nums[i])
if j == len(tails) {
// nums[i] 可以接在当前最长子序列后面
tails = append(tails, nums[i])
} else {
// 用较小的 nums[i] 替换 tails[j]
tails[j] = nums[i]
}
pos[j] = i // 记录长度为 j+1 的子序列尾部索引
if j > 0 {
parent[i] = pos[j-1] // 前驱是长度为 j 的子序列尾部
}
}
// 从最长子序列的尾部开始,沿 parent 回溯
length := len(tails)
result := make([]int, length)
result[length-1] = nums[pos[length-1]]
k := pos[length-1]
for i := length - 2; i >= 0; i-- {
k = parent[k]
result[i] = nums[k]
}
return result
}
// lowerBound 返回第一个 >= target 的位置
func lowerBound(a []int, target int) int {
left, right := 0, len(a)
for left < right {
mid := left + (right-left)/2
if a[mid] < target {
left = mid + 1
} else {
right = mid
}
}
return left
}