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

287 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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] 结尾的最长递增子序列长度`。
转移方程:
$$dp[i] = \max \{ dp[j] \} + 1, \quad j < i,\; nums[j] < nums[i]$$
回溯时,从 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
}
```