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] > 所有 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] ✓
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 代码提示
|
|
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|