vault backup: 2026-05-18 12:24:21
This commit is contained in:
@@ -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²)?
|
||||
|
||||
Reference in New Issue
Block a user