算法|动态规划及例题思路
一、动态规划
动态规划(Dynamic Programming,简称DP) 是一种解决复杂问题的算法思想,它通过将问题分解为更小的子问题,并存储子问题的解来避免重复计算,从而提高效率。
1. 核心思想
动态规划的核心是 “记住求过的解”:
- 分治思想:将大问题分解为小问题
- 记忆化:保存已解决的子问题的答案
- 避免重复计算:同一个子问题只计算一次
2. 适用场景
动态规划通常用于解决具有以下特征的问题:
(1) 最优子结构
问题的最优解包含子问题的最优解
例子1:最短路径问题
从A到C的最短路径 = 从A到B的最短路径 + 从B到C的最短路径
A --10-- B --15-- C\ /\--20-- D --5--/
如果 A→B→C
是最短路径,那么 A→B
和 B→C
也必须分别是A到B和B到C的最短路径。
例子2:矩阵连乘
6个矩阵的最优计算顺序,一定包含前k
个矩阵和后(6-k)
个矩阵的最优计算顺序。
判断方法
如果一个问题可以通过 “组合子问题的最优解” 来得到原问题的最优解,那么它就具有最优子结构。
(2) 重叠子问题
在递归求解过程中相同的子问题被多次重复计算
例子1:斐波那契数列
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2)
│ │ └── fib(1)
│ └── fib(2)
└── fib(3) ← 重复计算!├── fib(2) ← 重复计算!└── fib(1)
例子2:矩阵连乘
计算 dp[1][4]
和 dp[2][5]
时,都可能需要 dp[3][4]
:
dp[1][4] = min(dp[1][k] + dp[k+1][4] + ...)
需要 dp[3][4]
dp[2][5] = min(dp[2][k] + dp[k+1][5] + ...)
也需要 dp[3][4]
判断方法
如果画出递归树,发现有很多相同的节点,就存在重叠子问题。
(3) 无后效性
当前状态一旦确定,后续决策不受之前决策的影响
例子1:最短路径问题(无后效性)
A --10-- B --15-- C\ /\--20-- D --5--/
从B到C的最短路径就是15,无论你是从A→B
还是D→B
到达B
点的。
状态:当前所在位置
决策:下一步去哪里
无后效性:✓ 在B点时,到C的最短路径与如何到达B无关
例子2:背包问题(无后效性)
状态:(当前考虑的物品i, 剩余容量w)
决策:选或不选当前物品
无后效性:✓ 在状态(i,w)
时,后续最大价值只与剩余物品和剩余容量有关,与之前选了哪些物品无关
例子3:带有状态依赖的问题(有后效性)
问题:在游戏中,某些技能有冷却时间
状态:(当前位置, 技能冷却状态)
有后效性:✗ 能否使用技能取决于之前是否使用过该技能
3. 动态规划解题步骤
步骤1:定义状态
- 确定
dp[i]
或dp[i][j]
表示什么含义 - 例子:在最大子数组问题中,
dp[i]
表示以第 i 个元素结尾的最大子数组和
步骤2:状态转移方程
- 找出
dp[i]
与dp[i-1]
、dp[i-2]
等的关系 - 例子:
dp[i] = max(nums[i], dp[i-1] + nums[i])
步骤3:初始化
- 确定基础情况的初始值
- 例子:
dp[0] = nums[0]
步骤4:确定计算顺序
- 从基础情况开始,逐步计算更复杂的情况
步骤5:返回结果
- 从 dp 数组中找出最终答案
二、 动态规划类型
类型1:线性DP
问题1:爬楼梯
例题:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶?
示例:n = 3 → 输出:3(1+1+1, 1+2, 2+1)
核心思想:
- 要到达第 i 阶,可以从第 i-1 阶爬1步,或从第 i-2 阶爬2步
- 因此到达第 i 阶的方法数 = 到达第 i-1 阶的方法数 + 到达第 i-2 阶的方法数
关键步骤:
- 定义状态:
dp[i]
表示到达第 i 阶楼梯的方法数 - 初始状态:
dp[0] = 1
,dp[1] = 1
(或dp[1] = 1
,dp[2] = 2
) - 状态转移:
dp[i] = dp[i-1] + dp[i-2]
- 计算顺序:从 i=2 到 i=n
优化:可以只用两个变量dp[0] = 1
, dp[1] = 1
,空间复杂度 O(1)
问题2:最长递增子序列 (LIS)
例题:给定数组 [10,9,2,5,3,7,101,18],找到最长递增子序列的长度
示例:输出:4(子序列 [2,3,7,101] 或 [2,5,7,101])
核心思想:
- 对于每个位置 i,检查所有 j < i,如果 nums[j] < nums[i],则可以扩展以 j 结尾的递增序列
关键步骤:
- 定义状态:
dp[i]
表示以第 i 个元素结尾的最长递增子序列长度 - 初始状态:所有
dp[i] = 1
(每个元素本身构成长度为1的序列) - 状态转移:
dp[i] = max(dp[j] + 1)
,其中 j < i 且 nums[j] < nums[i] - 结果:
max(dp)
时间复杂度:O(n²),可以用二分优化到 O(n log n)
类型2:区间DP
问题3:矩阵连乘
例题:给定矩阵维度 [30×35, 35×15, 15×5, 5×10, 10×20, 20×25],求最少乘法次数
核心思想:
- 矩阵连乘 A₁A₂…Aₙ,不同的结合方式计算量不同
- 通过区间长度从小到大计算最优分割点
关键步骤:
- 定义状态:
dp[i][j]
表示计算矩阵 Aᵢ 到 Aⱼ 连乘的最少乘法次数 - 初始状态:
dp[i][i] = 0
(单个矩阵不需要乘法) - 状态转移:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]×p[k]×p[j])
,其中 i ≤ k < j - 计算顺序:按区间长度从小到大
类型3:背包DP
问题4:0-1背包
例题:背包容量 W=10,物品重量 [2,3,4,5],价值 [3,4,5,6],求最大价值
核心思想:
- 每个物品要么选要么不选
- 对于每个物品,考虑放或不放到背包中的两种情况
关键步骤:
- 定义状态:
dp[i][w]
表示前 i 个物品在容量 w 下的最大价值 - 初始状态:
dp[0][w] = 0
(没有物品时价值为0) - 状态转移:
- 不选第 i 个物品:
dp[i][w] = dp[i-1][w]
- 选第 i 个物品:
dp[i][w] = dp[i-1][w-weight[i]] + value[i]
- 取两者最大值
- 不选第 i 个物品:
- 结果:
dp[n][W]
空间优化:可以只用一维数组,从后往前更新
类型4:树形DP
问题5:二叉树中的最大路径和
例题:给定二叉树,找到路径(节点序列)使得路径和最大
核心思想:
- 对于每个节点,计算以该节点为根的子树中,包含该节点的最大路径和
- 路径可能:左子树路径 + 当前节点,右子树路径 + 当前节点,或单独当前节点
关键步骤:
- 定义状态:递归返回以当前节点为起点的最大路径和
- 状态转移:
max_gain(node) = node.val + max(0, max_gain(node.left), max_gain(node.right))
- 更新全局最大值:
max_sum = max(max_sum, node.val + left_gain + right_gain)
总结对比
问题类型 | 状态定义 | 状态转移关键 | 计算顺序 |
---|---|---|---|
线性DP | 一维数组 | 与前1-2个状态相关 | 顺序计算 |
区间DP | 二维数组 | 区间分割点选择 | 区间长度从小到大 |
背包DP | 二维数组 | 选择/不选择物品 | 物品顺序,容量从小到大 |
树形DP | 递归返回值 | 子树结果组合 | 后序遍历 |