vault backup: 2026-05-18 12:24:21

This commit is contained in:
2026-05-18 12:24:21 +08:00
parent f0c7163858
commit 74cae35e51
6 changed files with 3211 additions and 0 deletions
+84
View File
@@ -240,6 +240,90 @@ Go 的 `sort.SearchInts` 返回目标值在有序切片中的插入位置(索
>
> 如果需要**输出具体的 LIS**,方法一更容易扩展:维护一个 `parent[i]` 数组记录每个 dp[i] 对应的上一个元素下标,最后从 dp 最大处回溯即可。方法二较难还原(可以用额外数据结构实现但不常见)。
#### 方法一:DP + parent 数组回溯(推荐 ✅)
核心思想:**在转移时同时记录"父节点"**——就像链表一样,从终点一步步往回走就能拼出整条链。
```go
// lengthOfLISWithSequence returns both the length and the actual LIS.
func lengthOfLISWithSequence(nums []int) (int, []int) {
n := len(nums)
if n == 0 {
return 0, nil
}
dp := make([]int, n) // dp[i] = 以 nums[i] 结尾的 LIS 长度
parent := make([]int, n) // parent[i] = dp[i] 对应的前驱下标,-1 表示无前驱
for i := range dp {
dp[i] = 1
parent[i] = -1
}
// 自底向上填表
for i := 1; i < n; i++ {
for j := 0; j < i; j++ {
if nums[j] < nums[i] && dp[j]+1 > dp[i] {
dp[i] = dp[j] + 1
parent[i] = j // ★ 关键:记录父节点
}
}
}
// 找到 dp 最大值及其索引
maxIdx := 0
for i := 1; i < n; i++ {
if dp[i] > dp[maxIdx] {
maxIdx = i
}
}
// ★ 从 maxIdx 沿 parent 链回溯,逆序拼出 LIS
result := make([]int, 0, dp[maxIdx])
for cur := maxIdx; cur != -1; cur = parent[cur] {
result = append(result, nums[cur])
}
// 此时 result 是逆序的(从后往前),需要反转
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return dp[maxIdx], result
}
```
**走一遍演示**(`nums = [10,9,2,5,3,7,101,18]`):
| i | nums[i] | dp[i] | parent[i] | 说明 |
|---|---------|-------|-----------|------|
| 0 | 10 | 1 | -1 | 单独起点 |
| 1 | 9 | 1 | -1 | 无前驱 |
| 2 | 2 | 1 | -1 | 无前驱 |
| 3 | 5 | 2 | 2 | 接在 nums[2]=2 后面 |
| 4 | 3 | 2 | 2 | 接在 nums[2]=2 后面 |
| 5 | 7 | 3 | 3 | 接在 nums[3]=5 后面(dp[3]+1=3 > dp[4]+1=3 不更优,取先更新的) |
| 6 | 101 | 4 | 5 | 接在 nums[5]=7 后面 |
| 7 | 18 | 4 | 5 | 同样长度为 4,接在 nums[5]=7 后面 |
**回溯过程**:`maxIdx = 6`(dp[6]=4 是第一个出现的最大值)
```
第1步: cur=6, nums[6]=101 → result=[101] parent[6]=5
第2步: cur=5, nums[5]=7 → result=[101,7] parent[5]=3
第3步: cur=3, nums[3]=5 → result=[101,7,5] parent[3]=2
第4步: cur=2, nums[2]=2 → result=[101,7,5,2] parent[2]=-1 → 停止
反转后: [2,5,7,101] ✅
```
> [!tip] ⚡ 设计要点
> - **为什么要等全部填完再回溯?** 因为只有在所有状态都计算完后,才能确定全局最优解的终点在哪
> - **如果有多个相同长度的 LIS 怎么办?** 上面的实现返回的是最先达到的那个。如果只需要任意一个,这就够了;如果需要全部枚举,则需要在遍历结束时收集所有 `dp[i]==maxLen` 的 i 分别回溯
> - **空间代价**:多了 O(n) 的 `parent` 数组,整体仍然是 O(n²) 时间、O(n) 空间
> [!warning] ⚠️ 为什么不推荐方法二还原?
> `tails` 数组中的值会被反复替换,它只是各长度的"最优末尾候选",不是真实路径。虽然可以通过额外维护一棵前驱树来还原,但实现复杂且容易出错,面试中很少要求。用方法一的 parent 数组回溯才是标准做法。
### 何时选哪种方法?
> [!question] 💡 面试官追问:为什么要用 O(n log n) 而不是 O(n²)?