--- 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 中
二分查找"} C["追加到 tails 末尾
更新 parent[i]=prevIdx"] D["用 nums[i] 替换
tails[j]
parent[i]=j-1
对应的前驱索引"] E["i++"] F{"是否遍历完?"} G["从 tails 末尾开始
回溯 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 } ```