动态规划第一章:引论

动态规划基础讲解

动态规划(Dynamic Programming,简称 DP)是一种通过把大问题拆成小问题,并保存小问题答案来避免重复计算的算法思想。它常用于求最值、计数、可行性判断等问题。


一、什么时候可以用动态规划?

一个问题通常需要满足:

  1. 最优子结构

    大问题的最优解可以由小问题的最优解推出来。

    例如:爬到第 n 阶的方法数,可以由第 n-1 阶和第 n-2 阶推出。

  2. 重叠子问题

    不同的大问题会反复用到相同的小问题。

    例如斐波那契数列中,f(5) 会反复用到 f(3)、f(2)。

  3. 无后效性

    当前状态确定后,后面的决策只依赖当前状态,不依赖"怎么走到这里"的历史路径。


二、动态规划解题五步法

1. 定义状态

明确 dp[i] 或 dp[i][j] 表示什么。

例如爬楼梯:

dp[i] 表示爬到第 i 阶的方法数。

状态定义是动态规划最核心的一步。

2. 推导状态转移方程

找 dp[i] 和之前状态的关系。

例如爬楼梯,每次可以走 1 阶或 2 阶:

dp[i] = dp[i-1] + dp[i-2]

3. 初始化

确定最小状态的初始值。

例如:

dp[0] = 1,dp[1] = 1

4. 确定遍历顺序

保证计算 dp[i] 时,它依赖的状态已经算出来。

例如爬楼梯从小到大遍历:

python 复制代码
for i in range(2, n + 1):
    dp[i] = dp[i - 1] + dp[i - 2]

5. 返回答案

根据题意返回 dp[n]、dp[n][m] 或所有状态中的最值等。


三、经典例子:爬楼梯

题目:每次可以爬 1 阶或 2 阶,问爬到第 n 阶有多少种方法。

状态定义

dp[i]:爬到第 i 阶的方法数。

转移方程

最后一步可能从 i-1 阶走 1 阶上来,也可能从 i-2 阶走 2 阶上来:

text 复制代码
dp[i] = dp[i-1] + dp[i-2]

初始化

text 复制代码
dp[0] = 1
dp[1] = 1

代码

python 复制代码
def climbStairs(n):
    if n <= 1:
        return 1

    dp = [0] * (n + 1)
    dp[0] = 1
    dp[1] = 1

    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]

    return dp[n]

空间优化

因为 dp[i] 只依赖前两个状态,所以可以只用两个变量:

python 复制代码
def climbStairs(n):
    if n <= 1:
        return 1

    a, b = 1, 1
    for i in range(2, n + 1):
        a, b = b, a + b

    return b

四、经典例子:0-1 背包

题目:有 n 个物品,第 i 个物品重量为 w[i],价值为 v[i],背包容量为 C。每个物品最多选一次,求最大价值。

状态定义

dp[j]:容量为 j 时能获得的最大价值。

转移方程

对于每个物品:

text 复制代码
dp[j] = max(dp[j], dp[j - w[i]] + v[i])

含义:

  • 不选当前物品:价值是 dp[j]
  • 选当前物品:价值是 dp[j - w[i]] + v[i]

关键点:倒序遍历容量

0-1 背包中每个物品只能用一次,所以容量要倒序遍历:

python 复制代码
def knapsack(C, weights, values):
    dp = [0] * (C + 1)

    for i in range(len(weights)):
        for j in range(C, weights[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

    return dp[C]

如果正序遍历,就变成完全背包,表示每个物品可以选无限次。


五、动态规划和递归、贪心的区别

方法 特点
递归 直接拆问题,但可能大量重复计算
记忆化搜索 递归 + 缓存,是动态规划的一种写法
动态规划 保存子问题结果,通常用表格或数组递推
贪心 每步选局部最优,不保证全局最优;DP 会综合考虑多个状态

很多题可以写成:

  • 自顶向下:记忆化搜索
  • 自底向上:递推 DP

两者本质相同。


六、常见动态规划类型

  1. 线性 DP:爬楼梯、打家劫舍、最长递增子序列
  2. 背包 DP:0-1 背包、完全背包、多重背包
  3. 区间 DP:石子合并、最长回文子序列
  4. 树形 DP:树上的最大独立集、树形背包
  5. 状态压缩 DP:用二进制表示状态,如旅行商问题
  6. 数位 DP:统计满足数位条件的数字个数

七、动态规划常见误区

  1. 状态定义不清楚

    先想清楚 dp[i] 到底表示什么。

  2. 转移方程漏情况

    要枚举"最后一步"或"当前决策"的所有可能。

  3. 初始化错误

    初始值往往决定结果,尤其是计数题和背包题。

  4. 遍历顺序错误

    要保证依赖状态先被计算。背包问题中正序和倒序含义不同。

  5. 空间优化后逻辑混乱

    一维滚动数组优化前,最好先写二维版本验证。


八、动态规划做题模板

text 复制代码
1. 明确 dp 数组含义
2. 写出状态转移方程
3. 初始化 dp
4. 确定遍历顺序
5. 返回目标答案
6. 考虑是否能压缩空间

简单记:

定义状态 → 找转移 → 初始化 → 按序计算 → 返回答案

动态规划的核心不是背代码,而是练习"如何定义状态"和"如何从子状态推出当前状态"。建议从爬楼梯、打家劫舍、0-1 背包、最长递增子序列这几类题开始练习。

学习动态规划前,建议补充的知识

动态规划本身并不要求你先学很多高深内容,但下面这些前置知识会显著降低学习难度。可以分成三类:必须会 、强烈建议会 、按题型补充。


一、必须会的基础

1. 编程语言基础

至少熟练掌握一种语言中的:

  • 变量、条件判断、循环
  • 函数定义与调用
  • 数组 / 列表 / 二维数组
  • 字符串基本操作
  • 索引和边界处理

例如 Python 中要熟练写:

python 复制代码
dp = [0] * (n + 1)
dp = [[0] * (m + 1) for _ in range(n + 1)]

因为动态规划大量使用数组保存状态。


2. 数组与二维表

要理解:

  • 一维数组 dp[i] 表示某个状态
  • 二维数组 dp[i][j] 表示两个维度下的状态
  • 如何初始化数组
  • 如何遍历数组
  • 如何避免下标越界

很多 DP 题本质就是"填一张表"。

例如 0-1 背包的一维数组:

python 复制代码
dp = [0] * (C + 1)

区间 DP 的二维数组:

python 复制代码
dp = [[0] * n for _ in range(n)]

3. 循环与遍历顺序

动态规划非常依赖遍历顺序。

你需要理解:

  • 正序遍历和倒序遍历的区别
  • 外层循环和内层循环分别代表什么
  • 为什么计算 dp[i] 时,它依赖的状态必须已经算好

例如 0-1 背包容量必须倒序:

python 复制代码
for j in range(C, w - 1, -1):
    dp[j] = max(dp[j], dp[j - w] + v)

如果这个不熟,后面很容易把 0-1 背包和完全背包写混。


4. 递归与函数调用

动态规划经常从递归演化而来,所以要会:

  • 递归函数怎么写
  • 递归终止条件
  • 递归树大概长什么样
  • 什么是重复子问题

例如斐波那契递归:

python 复制代码
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

要能看出它为什么慢:fib(3) 被重复计算很多次。


5. 递推思想

递推是动态规划最直接的前置知识。

要理解:

已知前面的结果,如何推出后面的结果。

例如:

text 复制代码
f(0) = 0
f(1) = 1
f(i) = f(i-1) + f(i-2)

这其实就是最简单的动态规划。

建议先练:

  • 斐波那契数列
  • 爬楼梯
  • 杨辉三角
  • 简单数列求和

二、强烈建议补充的知识

1. 复杂度分析

至少会估算:

  • 时间复杂度:O(n)、O(n^2)、O(nm)
  • 空间复杂度
  • 为什么二维 DP 可以优化成一维

例如:

python 复制代码
for i in range(n):
    for j in range(m):
        ...

这是 O(nm)。

动态规划题通常要判断状态数 × 每个状态的转移代价。


2. 记忆化搜索

记忆化搜索是"递归 + 缓存",是理解 DP 的绝佳桥梁。

例如:

python 复制代码
memo = {}

def dfs(n):
    if n <= 1:
        return n
    if n in memo:
        return memo[n]
    memo[n] = dfs(n - 1) + dfs(n - 2)
    return memo[n]

学会它之后,你会更容易理解:

  • 什么是状态
  • 什么是重复子问题
  • 为什么 DP 能优化递归

3. 暴力枚举与搜索

动态规划很多时候是在优化暴力搜索。

建议了解:

  • 枚举所有选择
  • 子集、排列、组合的基本概念
  • DFS 搜索的基本写法
  • 搜索树的概念

例如 0-1 背包的暴力思路:

每个物品选或不选,枚举所有组合,再取最大价值。

理解暴力后,才能理解 DP 在优化什么。


4. 数学归纳思想

动态规划的状态转移和数学归纳很像:

  1. 验证初始状态成立
  2. 假设前面的状态成立
  3. 推出当前状态成立

例如:

如果知道爬到第 i-1 阶和第 i-2 阶的方法数,就能推出第 i 阶。

不需要形式化证明,但要有这种"由小推大"的思维。


5. 边界与初始化

很多 DP 错误不是转移方程错,而是初始化错。

要习惯思考:

  • dp[0] 应该是什么?
  • 空集、0 容量、0 个物品怎么处理?
  • 不可能状态用什么值表示?
  • 计数题初始值是 0 还是 1?
  • 求最小值时初始值用多大?

例如:

python 复制代码
dp = [0] * (n + 1)
dp[0] = 1

计数题里 dp[0] = 1 通常表示"空方案也是一种方案"。


三、按题型补充的知识

不是一开始都要学,学到对应 DP 类型前再补即可。

1. 学背包 DP 前

补充:

  • 二维数组
  • 正序 / 倒序遍历
  • 最大最小值更新
  • 容量、重量、价值这类建模

2. 学线性 DP 前

补充:

  • 一维数组递推
  • 前缀思想
  • 序列下标处理

3. 学区间 DP 前

补充:

  • 二维数组
  • 区间 [l, r]
  • 按区间长度遍历
  • 子区间划分

4. 学树形 DP 前

补充:

  • 树的基本概念
  • 邻接表
  • DFS 遍历树
  • 后序遍历思想

5. 学状态压缩 DP 前

补充:

  • 二进制
  • 位运算:&、|、^、<<、>>
  • 用二进制表示集合

6. 学数位 DP 前

补充:

  • 数字按位拆分
  • 前缀统计思想
  • 递归枚举数位

四、建议的学习顺序

比较平滑的路线是:

text 复制代码
编程基础
→ 数组与循环
→ 递归
→ 递推
→ 记忆化搜索
→ 复杂度分析
→ 简单线性 DP
→ 0-1 背包
→ 完全背包
→ 区间 DP / 树形 DP / 状压 DP

具体练习可以按这个顺序:

  1. 斐波那契数列
  2. 爬楼梯
  3. 杨辉三角
  4. 打家劫舍
  5. 最长递增子序列
  6. 0-1 背包
  7. 完全背包

五、最小前置清单

如果只想尽快开始学动态规划,至少先掌握:

  • 数组 / 列表
  • 二维数组
  • for 循环与倒序遍历
  • 函数和递归
  • 递归终止条件
  • 斐波那契式递推
  • 基本复杂度 O(n)、O(n^2)
  • 边界和初始化意识

一句话总结:

学 DP 前最该补的是:数组、循环、递归、递推、记忆化和复杂度分析。

其他如树、位运算、区间思想,可以等到对应题型再补。

相关推荐
CRMEB系统商城1 小时前
家电零售的供应商协同与物流提效
java·开发语言·人工智能·小程序·1024程序员节
xiaoniu1428571 小时前
命题的映射化
数学·1024程序员节·高中数学·文化课
jason.zeng@15022071 小时前
Nginx 配置详解(静态 HTML、下载目录、API 反向代理三套场景)
java·vue.js·1024程序员节
星夜离尘561 小时前
MATLAB 因果采样保持:用历史匹配和数据年龄防止未来信息泄漏
数据结构·算法·matlab
鬼手点金1 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节
kaixin_learn_qt_ing1 小时前
插件式框架学习(1)
1024程序员节·插件式框架
在所不辞兄1 小时前
【零基础学智能仿真-61】DFT数值收敛与计算预算
人工智能·深度学习·神经网络·机器学习·1024程序员节
Shadow(⊙o⊙)1 小时前
LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路
java·服务器·前端·1024程序员节
Pointer Pursuit1 小时前
Linux环境变量
linux·服务器·c语言·链表·1024程序员节