文章目录
- 动态规划算法全面解析
- 一、核心思想与基本概念
- 二、动态规划与其他算法的区别
- 三、动态规划的解题步骤
- 四、经典案例解析
- 1. **斐波那契数列(Fibonacci)**
- 2. **0-1背包问题(0-1 Knapsack)**
- 3. **最长公共子序列(LCS)**
- **五、动态规划的优化技巧**
- **六、动态规划的应用场景**
- **七、动态规划与贪心的对比**
- **八、学习建议**
动态规划算法全面解析
动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题,并利用子问题的解来高效解决原问题的算法思想。它与分治法类似,但更注重对重复子问题的优化,避免重复计算,从而大幅提升算法效率。
一、核心思想与基本概念
-
重叠子问题(Overlapping Subproblems)
问题可以分解为多次重复出现的子问题。例如斐波那契数列中,计算fib(n)
时需要重复计算fib(n-1)
和fib(n-2)
。 -
最优子结构(Optimal Substructure)
问题的最优解包含子问题的最优解。例如最短路径问题中,从A到C的最短路径必然包含A到B的最短路径(若B是路径中的节点)。 -
状态转移方程
用数学公式定义子问题之间的关系,是动态规划的核心。例如斐波那契数列的状态转移方程为:
f i b ( n ) = { 0 , n = 0 1 , n = 1 f i b ( n − 1 ) + f i b ( n − 2 ) , n > 1 fib(n) = \begin{cases} 0, & n=0 \\ 1, & n=1 \\ fib(n-1) + fib(n-2), & n>1 \end{cases} fib(n)=⎩ ⎨ ⎧0,1,fib(n−1)+fib(n−2),n=0n=1n>1
二、动态规划与其他算法的区别
算法类型 | 核心策略 | 重复子问题处理 | 典型案例 |
---|---|---|---|
动态规划 | 利用子问题解存储与复用 | 存储结果避免重复计算 | 背包问题、最长公共子序列 |
分治法 | 将问题分解为独立子问题 | 不存储子问题解 | 归并排序、快速幂 |
贪心算法 | 每一步选择局部最优解 | 不考虑子问题重叠 | 霍夫曼编码、活动选择问题 |
三、动态规划的解题步骤
-
定义状态
明确dp[i]
表示什么,通常与问题的规模或阶段相关。例如:dp[i]
:长度为i
的序列的最优解dp[i][j]
:二维问题中第i
行第j
列的状态
-
推导状态转移方程
找出dp[i]
与dp[i-1]
、dp[i-2]
等子状态的关系,例如:- 背包问题:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
- 最长递增子序列(LIS):
dp[i] = max(dp[j] + 1) ,其中j < i且nums[j] < nums[i]
- 背包问题:
-
确定初始条件
处理最小规模的子问题,例如:dp[0] = 0
(空序列的解)dp[i][0] = 0
(背包容量为0时无法装物品)
-
确定计算顺序
确保计算dp[i]
时,其依赖的子状态已被计算,通常采用自底向上(迭代)的方式。 -
获取最终结果
根据问题要求,从状态数组中提取答案(可能是dp[n]
或max(dp[1..n])
等)。
四、经典案例解析
1. 斐波那契数列(Fibonacci)
- 问题:计算第
n
个斐波那契数。 - 暴力递归:时间复杂度
O(2^n)
,存在大量重复计算。 - 动态规划解法:
def fib_dp(n):if n <= 1:return ndp = [0] * (n + 1)dp[0], dp[1] = 0, 1for i in range(2, n + 1):dp[i] = dp[i-1] + dp[i-2]return dp[n]
- 优化:只需存储前两个状态,空间复杂度从
O(n)
降为O(1)
:def fib_optimized(n):if n <= 1:return na, b = 0, 1for _ in range(2, n + 1):a, b = b, a + breturn b
2. 0-1背包问题(0-1 Knapsack)
- 问题:有
n
个物品,每个物品重量w[i]
、价值v[i]
,背包容量W
,求能装的最大价值。 - 状态定义:
dp[i][j]
表示前i
个物品放入容量为j
的背包的最大价值。 - 状态转移:
- 不选第
i
个物品:dp[i][j] = dp[i-1][j]
- 选第
i
个物品(若j >= w[i]
):dp[i][j] = dp[i-1][j-w[i]] + v[i]
- 最终:
dp[i][j] = max(不选, 选)
- 不选第
- 代码实现:
def knapsack(w, v, W):n = len(w)dp = [[0] * (W + 1) for _ in range(n + 1)]for i in range(1, n + 1):for j in range(W + 1):if w[i-1] <= j:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])else:dp[i][j] = dp[i-1][j]return dp[n][W]
- 空间优化:使用一维数组,逆序更新(避免重复使用当前物品):
def knapsack_optimized(w, v, W):n = len(w)dp = [0] * (W + 1)for i in range(n):for j in range(W, w[i] - 1, -1):dp[j] = max(dp[j], dp[j - w[i]] + v[i])return dp[W]
3. 最长公共子序列(LCS)
- 问题:求两个字符串
A
和B
的最长公共子序列长度。 - 状态定义:
dp[i][j]
表示A[0..i-1]
和B[0..j-1]
的LCS长度。 - 状态转移:
- 若
A[i-1] == B[j-1]
:dp[i][j] = dp[i-1][j-1] + 1
- 否则:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 若
- 代码示例:
def lcs_length(text1, text2):m, n = len(text1), len(text2)dp = [[0] * (n + 1) for _ in range(m + 1)]for i in range(1, m + 1):for j in range(1, n + 1):if text1[i-1] == text2[j-1]:dp[i][j] = dp[i-1][j-1] + 1else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])return dp[m][n]
五、动态规划的优化技巧
-
空间压缩
- 一维DP:将二维状态数组压缩为一维(如0-1背包的优化)。
- 滚动数组:仅保留与当前状态相关的前几个子状态(如斐波那契数列)。
-
状态转移优化
- 利用数据结构(如平衡树、单调队列)加速状态转移。
- 矩阵快速幂:将线性递推关系转化为矩阵乘法,适用于大规模数据。
-
记忆化搜索(自顶向下)
- 用递归+缓存的方式避免重复计算,适合子问题依赖关系复杂的场景。
def fib_memo(n, memo={}):if n in memo:return memo[n]if n <= 1:memo[n] = nelse:memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)return memo[n]
六、动态规划的应用场景
- 组合优化问题:背包问题、旅行商问题(TSP)、最短路径。
- 字符串处理:编辑距离、最长回文子串、正则表达式匹配。
- 数学问题:硬币找零、整数拆分、矩阵链乘法。
- 概率与统计:股票买卖最佳时机、骰子点数组合。
- 图论问题:状态压缩DP(如哈密顿路径)。
七、动态规划与贪心的对比
- 动态规划:考虑所有可能的子问题,确保全局最优,但时间复杂度较高。
- 贪心算法:每一步选局部最优,可能无法得到全局最优,但效率更高。
- 案例对比:
- 活动选择问题:贪心可直接选结束时间最早的活动,无需DP。
- 背包问题:贪心(按单位重量价值选)无法得到最优解,必须用DP。
八、学习建议
- 从基础案例入手:先掌握斐波那契、背包、LCS等经典问题。
- 多练习状态定义:动态规划的核心难点在于状态转移方程的推导。
- 注意边界条件:初始状态的设定直接影响结果正确性。
- 分析时间与空间复杂度:优化算法时需平衡两者。
- 参考解题模板:许多DP问题有固定的状态设计模式(如区间DP、树形DP)。
动态规划是算法设计中最具挑战性的思想之一,但通过大量练习和总结,能够有效提升解决复杂问题的能力。