DP 进阶与回溯模板
本节目标
- 掌握 LIS(最长递增子序列)与 LCS(最长公共子序列)
- 理解回溯的"选择—递归—撤销"模板
- 能手写全排列、子集等经典回溯题
// 运行环境:Node.js 14+
// 保存为 dsa-l21.js,执行:node dsa-l21.js
// 最长递增子序列 LIS:dp[i]=以 i 结尾的 LIS 长度
function lengthOfLIS(nums) {
const dp = new Array(nums.length).fill(1)
let max = 1
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++)
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1)
max = Math.max(max, dp[i])
}
return max
}
// 最长公共子序列 LCS(二维 DP)
function longestCommonSubsequence(a, b) {
const m = a.length, n = b.length
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
}
// 回溯模板:选择 → 递归 → 撤销
function permutations(nums) {
const res = [], path = [], used = new Array(nums.length).fill(false)
const backtrack = () => {
if (path.length === nums.length) { res.push([...path]); return }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue
used[i] = true; path.push(nums[i]) // 选择
backtrack() // 递归
path.pop(); used[i] = false // 撤销
}
}
backtrack()
return res
}
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])) // 4 ([2,3,7,101])
console.log(longestCommonSubsequence('abcde', 'ace')) // 3
console.log(permutations([1, 2, 3]).length) // 6名词解释
- 最长递增子序列(LIS):在原序列中删掉若干元素后,得到的"严格递增且最长"的子序列。DP O(n²),用二分可优化到 O(n log n)。
- 最长公共子序列(LCS):两个序列都含有的、顺序一致(可不连续)的最长子序列。二维 DP 经典题,也是"编辑距离"的近亲。
- 回溯(Backtracking):深度优先试探所有选择,走不通就"撤销(回溯)"回到上一步换条路。模板是"选择→递归→撤销",适合排列/组合/子集/棋盘类穷举。
课后练习
- 回溯为什么必须"撤销"(path.pop())?
- 答案:递归返回后要恢复现场,否则
path会残留上一条分支的元素,污染后续分支;used也要复位,保证每个元素每分支只用一次。
- 答案:递归返回后要恢复现场,否则
- LCS 的转移
a[i-1]===b[j-1] ? dp[i-1][j-1]+1 : max(...)含义?- 答案:字符相等则接上对角(长度+1);不等则取"去掉 a 末位"或"去掉 b 末位"两种情况的最大值,体现"可不连续"。
总结
DP 进阶与回溯,是算法"内功"的两座高峰,但思维截然不同:DP 是"自底向上用状态表累积答案",回溯是"自顶向下穷举所有选择再剪枝"。LIS 和 LCS 是 DP 二维思维的入门——LIS 教你看"以每个位置结尾",LCS 教你建二维表表示"两序列前缀的关系"。而回溯的精髓全在"选择—递归—撤销"这六字真言:它本质上是在解空间树上 DFS,靠 used/path 记录现场、靠撤销回退。很多人写回溯乱,是因为没把"撤销"当回事——记住,递归返回时世界必须和进入时一模一样。我的建议:DP 先练"状态定义"的语感,回溯先背熟模板再往里填"选择条件",两套思维都熟了,你就能对付绝大多数中高难度题。