动态规划算法

2025博客之星年度评选已开启 10w+人浏览 1.5k人参与

目录

一、动态规划是什么?

二、动态规划的三大核心特征

1️⃣ 最优子结构 - "整体最优 → 局部最优"

2️⃣ 重叠子问题 - "相同的小问题反复出现"

3️⃣ 无后效性 - "过去不影响未来"

三、实例讲解

实例 1:爬楼梯问题

步骤 1:定义状态

步骤 2:初始状态

步骤 3:状态转移方程

步骤 4:JS 实现(两种方式:递归 + 记忆化、迭代 DP 数组)

方式 1:递归 + 记忆化(自顶向下)

方式 2:迭代 DP 数组(自底向上,空间优化前)

方式 3:迭代(空间优化后)

实例 2:最大子数组和(LeetCode 53 题,经典 DP 问题)

步骤 1:定义状态

步骤 2:初始状态

步骤 3:状态转移方程

步骤 4:JS 实现(两种方式)

方式 1:DP 数组

方式 2:空间优化(只用一个变量)

四、动态规划的适用场景


一、动态规划是什么?

动态规划(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

四、动态规划的适用场景

  • 问题可以拆解为重叠子问题(子问题被反复计算);
  • 问题具有最优子结构(原问题的最优解由子问题最优解组成);
  • 常见场景:最值问题(最大和、最长子序列)、计数问题(爬楼梯、不同路径)、存在性问题(能否分割)等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山楂树の

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值