用动态规划解决爬楼梯、最大子数组和与最长递增子序列。
先理解原理和示例,再练习完整口述与追问。
回答要点
动态规划先定义状态、写出转移方程、确定初始值,再考虑压缩空间。爬楼梯 f(n) = f(n-1) + f(n-2),只需两个变量;最大子数组和(Kadane):以 i 结尾的最大和等于 max(nums[i], 前一个 + nums[i]);最长递增子序列用“各长度子序列的最小结尾”数组加二分查找,O(n log n)。
原理拆解
爬楼梯每次走 1 或 2 阶,到达第 n 阶的最后一步要么来自 n-1,要么来自 n-2,方法数相加;f(0) = 1(什么都不走也是一种方式),f(1) = 1。直接递归会重复计算,复杂度指数级,自底向上迭代是 O(n)。
Kadane 算法中,若前面的累加和为负,带上它只会让结果更小,不如从当前元素重新开始。空数组没有子数组,抛出 RangeError 比返回 0 或 -Infinity 更明确。
LIS 中 tails[len] 表示长度为 len + 1 的递增子序列的最小可能结尾。新元素大于所有结尾就延长;否则用二分找到第一个不小于它的位置替换,使该长度的结尾更小、更有潜力延长。tails 本身不一定是一个真实的子序列,但长度就是答案。
示例与推演
function climbStairs(n) {
if (!Number.isInteger(n) || n < 0) throw new RangeError('n 必须是非负整数')
let a = 1
let b = 1
for (let i = 2; i <= n; i++) {
const next = a + b
a = b
b = next
}
return b
}
function maxSubArray(nums) {
if (nums.length === 0) throw new RangeError('数组不能为空')
let current = nums[0]
let best = nums[0]
for (let i = 1; i < nums.length; i++) {
current = Math.max(nums[i], current + nums[i])
best = Math.max(best, current)
}
return best
}
function lengthOfLIS(nums) {
const tails = []
for (const value of nums) {
let lo = 0
let hi = tails.length
while (lo < hi) {
const mid = (lo + hi) >> 1
if (tails[mid] < value) lo = mid + 1
else hi = mid
}
tails[lo] = value
}
return tails.length
}
推演:maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) 返回 6(子数组 [4, -1, 2, 1]);lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]) 中 tails 依次变化,最终为 [2, 3, 7, 18],长度 4。
易错点
爬楼梯 n 较大时结果会超出安全整数,需要 BigInt 或取模;LIS 二分写成“第一个大于”会把非严格递增也算进去,题目要求严格递增时要找第一个不小于的位置。
参考回答可直接练习口述
动态规划我先定义状态和转移,再优化空间。爬楼梯是 f(n) 等于 f(n-1) 加 f(n-2),用两个变量滚动即可。最大子数组和用 Kadane:以当前元素结尾的最大和,要么是自己,要么是前一个最大和加上自己,再记录全局最大值,空数组要单独处理。最长递增子序列用 tails 数组记录每个长度的最小结尾,新元素二分找到位置替换或追加,复杂度 O(n log n)。