目录
步骤 4:JS 实现(两种方式:递归 + 记忆化、迭代 DP 数组)
实例 2:最大子数组和(LeetCode 53 题,经典 DP 问题)

一、动态规划是什么?
动态规划(Dynamic Programming,简称 DP)是一种将复杂问题拆解为若干个重叠的子问题,通过解决子问题并存储子问题的解,最终推导出原问题解的算法思想。
可以用一个生活中的例子理解:假设你要爬一座 10 级的楼梯,每次只能爬 1 级或 2 级,问有多少种不同的爬法?
- 如果你直接暴力枚举所有可能的爬法(比如 1+1+…+1、1+1+2、1+2+1…),会重复计算很多情况(比如爬 3 级的方法会被爬 4 级、5 级的情况反复用到),效率极低。
- 而动态规划的思路是:先算 “爬 1 级的方法数”“爬 2 级的方法数”,再通过这两个结果算 “爬 3 级的方法数”(爬 1 级后再爬 2 级 + 爬 2 级后再爬 1 级),以此类推,把每个子问题的结果存起来,后面直接用,避免重复计算。
这就像你考试时,先把简单的小题答案记在草稿纸上,做大题时直接引用小题的结果,而不是重新算一遍 —— 既省时间,又不容易错。
二、动态规划的三大核心特征
1️⃣ 最优子结构 - "整体最优 → 局部最优"
大问题的最优解包含小问题的最优解
比喻:
-
找到北京到上海的最短路径
-
这个路径一定包含北京到济南的最短路径 + 济南到上海的最短路径
-
如果中间某段不是最短的,整体就不可能最短
最优子结构:原问题的最优解可以由子问题的最优解推导而来(比如爬 10 级的最优解 = 爬 9 级的解 + 爬 8 级的解)。
2️⃣ 重叠子问题 - "相同的小问题反复出现"
在求解过程中,同样的子问题会被多次计算
比喻:
-
计算斐波那契数列:
fib(5) = fib(4) + fib(3) -
fib(4) = fib(3) + fib(2),fib(3)被计算了两次! -
动态规划会记住
fib(3)的结果,避免重复计算
重叠子问题:子问题会被反复计算,因此需要用记忆化(缓存) 或dp 数组存储子问题的解。
3️⃣ 无后效性 - "过去不影响未来"
未来状态只依赖于当前状态,与如何到达当前状态无关
比喻:
-
你现在的位置决定了你能去哪里
-
但不管你是一路跑来的,还是坐车来的,都不影响下一步的选择
三、实例讲解
实例 1:爬楼梯问题
问题:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
步骤 1:定义状态
dp[n]:爬到第 n 级楼梯的方法数。
步骤 2:初始状态
- 当 n=1 时,只有 1 种方法(爬 1 级),即
dp[1] = 1。 - 当 n=2 时,有 2 种方法(1+1 或 2),即
dp[2] = 2。
步骤 3:状态转移方程
要爬到第 n 级,最后一步只能是:
- 从第 n-1 级爬 1 级上来(方法数为 dp [n-1]);
- 从第 n-2 级爬 2 级上来(方法数为 dp [n-2])。因此:
dp[n] = dp[n-1] + dp[n-2]。
步骤 4:JS 实现(两种方式:递归 + 记忆化、迭代 DP 数组)
方式 1:递归 + 记忆化(自顶向下)
递归的问题是会重复计算子问题,因此用一个数组 / 对象缓存已经计算过的结果。
// 记忆化缓存
const memo = {};
function climbStairs(n) {
// 边界条件
if (n === 1) return 1;
if (n === 2) return 2;
// 如果缓存中有,直接返回
if (memo[n]) return memo[n];
// 计算并缓存结果
memo[n] = climbStairs(n - 1) + climbStairs(n - 2);
return memo[n];
}
// 测试
console.log(climbStairs(3)); // 3
console.log(climbStairs(5)); // 8
console.log(climbStairs(10)); // 89
方式 2:迭代 DP 数组(自底向上,空间优化前)
从基础子问题开始,逐步计算到 n。
function climbStairs(n) {
if (n === 1) return 1;
if (n === 2) return 2;
// 定义dp数组
const dp = new Array(n + 1);
// 初始状态
dp[1] = 1;
dp[2] = 2;
// 循环计算
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// 测试
console.log(climbStairs(10)); // 89
方式 3:迭代(空间优化后)
观察发现,计算 dp [i] 只需要 dp [i-1] 和 dp [i-2],因此不需要存储整个数组,只用两个变量即可。
function climbStairs(n) {
if (n === 1) return 1;
if (n === 2) return 2;
// a表示dp[i-2],b表示dp[i-1]
let a = 1, b = 2;
let res = 0;
for (let i = 3; i <= n; i++) {
res = a + b;
// 更新变量,为下一次循环做准备
a = b;
b = res;
}
return res;
}
// 测试
console.log(climbStairs(10)); // 89
实例 2:最大子数组和(LeetCode 53 题,经典 DP 问题)
问题:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
比如:nums = [-2,1,-3,4,-1,2,1,-5,4],最大子数组是 [4,-1,2,1],和为 6。
步骤 1:定义状态
dp[i]:以第 i 个元素结尾的连续子数组的最大和。
步骤 2:初始状态
dp[0] = nums[0](第一个元素的最大子数组和就是它自己)。
步骤 3:状态转移方程
对于第 i 个元素,有两种选择:
- 把它加入前面的子数组(和为 dp [i-1] + nums [i]);
- 以它自己为起点重新开始(和为 nums [i])。因此:
dp[i] = Math.max(dp[i-1] + nums[i], nums[i])。
最终结果是 dp 数组中的最大值。
步骤 4:JS 实现(两种方式)
方式 1:DP 数组
function maxSubArray(nums) {
const n = nums.length;
if (n === 0) return 0;
// 定义dp数组
const dp = new Array(n);
// 初始状态
dp[0] = nums[0];
// 记录最大值
let max = dp[0];
// 循环计算
for (let i = 1; i < n; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
// 更新最大值
max = Math.max(max, dp[i]);
}
return max;
}
// 测试
const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
console.log(maxSubArray(nums)); // 6
方式 2:空间优化(只用一个变量)
计算 dp [i] 只需要 dp [i-1],因此用一个变量代替数组。
function maxSubArray(nums) {
const n = nums.length;
if (n === 0) return 0;
// pre表示dp[i-1]
let pre = nums[0];
let max = pre;
for (let i = 1; i < n; i++) {
pre = Math.max(pre + nums[i], nums[i]);
max = Math.max(max, pre);
}
return max;
}
// 测试
const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
console.log(maxSubArray(nums)); // 6
四、动态规划的适用场景
- 问题可以拆解为重叠子问题(子问题被反复计算);
- 问题具有最优子结构(原问题的最优解由子问题最优解组成);
- 常见场景:最值问题(最大和、最长子序列)、计数问题(爬楼梯、不同路径)、存在性问题(能否分割)等。
2万+

被折叠的 条评论
为什么被折叠?



