动态规划算法详解与应用
动态规划(Dynamic Programming,简称DP)是一种用于解决最优化问题和计数问题的高效算法设计方法。它通过将复杂问题分解为子问题,并存储子问题的解(避免重复计算),最终组合出原问题的解。以下是动态规划的详细讲解:
核心思想
- 分治思想:将大问题分解为重叠的子问题。
- 记忆化存储:保存子问题的解(通常用数组或哈希表),避免重复计算。
- 递推关系:找到子问题与原问题之间的状态转移方程。
适用条件
- 重叠子问题:问题可以被分解为多次重复计算的子问题。
- 例如斐波那契数列:
F(n) = F(n-1) + F(n-2)
。
- 例如斐波那契数列:
- 最优子结构:问题的最优解包含子问题的最优解。
- 例如最短路径问题:A→B→C的最短路径包含A→B的最短路径。
动态规划的步骤
- 定义状态
- 明确问题的状态变量(例如
dp[i]
表示第i
个斐波那契数)。
- 明确问题的状态变量(例如
- 确定状态转移方程
- 描述状态之间的关系(例如
dp[i] = dp[i-1] + dp[i-2]
)。
- 描述状态之间的关系(例如
- 初始化边界条件
- 设置初始值(例如
dp[0] = 0, dp[1] = 1
)。
- 设置初始值(例如
- 选择计算顺序
- 自顶向下(递归+记忆化)或自底向上(迭代填表)。
- 输出结果
- 从最终状态提取答案(例如
dp[n]
)。
- 从最终状态提取答案(例如
经典问题示例
1. 斐波那契数列
- 问题:计算第
n
个斐波那契数。 - 状态转移方程:
dp[i] = dp[i-1] + dp[i-2]
- 代码实现(自底向上):
def fib(n): dp = [0, 1] + [0] * (n-1) for i in range(2, n+1): dp[i] = dp[i-1] + dp[i-2] return dp[n]
2. 背包问题(0-1 Knapsack)
- 问题:在容量限制下选择物品,使总价值最大。
- 状态定义:
dp[i][w]
表示前i
个物品在容量w
下的最大价值。 - 状态转移方程:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])
3. 最长递增子序列(LIS)
- 问题:找到数组中最长的严格递增子序列长度。
- 状态转移方程:
dp[i] = max(dp[j] + 1 for j in range(i) if nums[j] < nums[i])
动态规划的两种实现方式
-
自顶向下(Top-Down)
- 递归 + 记忆化(备忘录法),适合子问题较少的情况。
- 示例(斐波那契数列):
memo = {0: 0, 1: 1} def fib(n): if n not in memo: memo[n] = fib(n-1) + fib(n-2) return memo[n]
-
自底向上(Bottom-Up)
- 迭代填表,通常更高效(避免递归开销)。
- 示例(斐波那契数列见前文)。
常见优化技巧
- 空间优化:
- 如果状态转移仅依赖前几个状态,可压缩DP表(例如斐波那契中用两个变量代替数组)。
- 状态压缩:
- 用位运算或滚动数组减少空间(例如0-1背包问题的一维数组解法)。
- 剪枝:
- 提前终止不必要的计算(例如某些问题中
dp[i]
达到阈值后停止)。
- 提前终止不必要的计算(例如某些问题中
动态规划 vs 贪心算法
- 动态规划:考虑所有子问题,保证全局最优。
- 贪心算法:每一步局部最优,但不一定全局最优(例如部分背包问题可用贪心,0-1背包不行)。