Files
leetcode-go/笔试/微派 Test1/01-LIS返回子序列.md
T

287 lines
6.9 KiB
Markdown
Raw Normal View History

2026-05-16 14:15:58 +08:00
---
tags: [笔试, 微派, DP, 贪心, 二分查找, Go]
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] 结尾的最长递增子序列长度`。
转移方程:
2026-05-16 14:33:21 +08:00
$$dp[i] = \max \{ dp[j] \} + 1, \quad j < i,\; nums[j] < nums[i]$$
2026-05-16 14:15:58 +08:00
回溯时,从 dp 值最大的位置倒推,找到前驱元素即可恢复完整子序列。
```mermaid
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` 数组记录的路径。
#### 算法步骤
```mermaid
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] &gt; 所有 tails" --> C
B -- "tails[j-1] &lt; 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] ✓
```
---
## 代码提示
```go
// 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`
---
## 代码
```go
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
}
```