动态规划(Dynamic Programming,简称 DP)是一种非常重要的算法思想,用于解决具有重叠子问题和最优子结构性质的问题,能大幅优化时间复杂度。下面详细介绍其解题思路和通用框架:
一、解题核心思路
动态规划的核心是拆分问题、利用子问题结果推导原问题,关键在于找到状态转移的规律,核心要点包括:
重叠子问题:问题可分解为多个重复出现的子问题(比如爬楼梯中,计算
dp[5]
会用到dp[4]
和dp[3]
,而dp[4]
又依赖dp[3]
等 ),避免重复计算子问题是 DP 优化的关键。最优子结构:原问题的最优解包含子问题的最优解(比如最短路径问题中,全局最短路径必然由局部最短路段组成 ),保证能用子问题结果推导原问题。
状态定义:抽象出
dp[i]
(或多维dp[i][j]
等 ),清晰描述 “当前状态” 对应的子问题,是动态规划的基础。状态转移方程:明确
dp[i]
与子问题dp[...
(如dp[i-1]
、dp[i-2]
)的推导关系,这是动态规划的核心逻辑。
二、通用解题框架(以一维 DP 为例)
动态规划解题通常遵循以下步骤,可根据问题复杂度扩展为多维:
1. 定义状态(核心第一步)
根据问题场景,定义 dp[i]
代表的具体含义。
例 1(爬楼梯):
dp[i]
表示爬到第i
阶楼梯的方法总数。例 2(最大子数组和):
dp[i]
表示以第i
个元素结尾的最大子数组和。
2. 推导状态转移方程
分析 dp[i]
如何由更小的子问题推导而来,写出递推公式。
爬楼梯:
dp[i] = dp[i-1] + dp[i-2]
(第i
阶可由i-1
阶走 1 步,或i-2
阶走 2 步到达 )。最大子数组和:
dp[i] = max(dp[i-1] + nums[i], nums[i])
(以i
结尾的最大和,要么是 “前面子数组延续”,要么是 “重新以nums[i]
开头” )。
3. 初始化状态(边界条件)
确定最小子问题的结果,为递推提供起点。
爬楼梯:
dp[1] = 1
(1 阶只有 1 种走法 ),dp[2] = 2
(2 阶有 “1+1” 或 “2” 两种 )。最大子数组和:
dp[0] = nums[0]
(单个元素的子数组和就是自身 )。
4. 遍历计算(填充 DP 表)
按照状态转移方程,从初始状态开始依次计算 dp[i]
,直到覆盖原问题的规模。
爬楼梯按
i
从 3 到n
遍历;最大子数组和按i
从 1 到len(nums)-1
遍历。
5. 返回结果(原问题解)
根据状态定义,返回对应 dp
数组中的值。
爬楼梯返回
dp[n]
;最大子数组和返回max(dp)
(因为dp[i]
是 “以i
结尾” 的最大和,全局最大需遍历找 )。
三、动态规划 vs 其他算法
与暴力递归相比,动态规划通过存储子问题结果(避免重复计算),将时间复杂度从指数级(如爬楼梯暴力递归是 \(O(2^n)\) )优化到多项式级(爬楼梯 DP 是 O(n) )。
但动态规划也有局限:
空间开销:如果直接用数组存所有子问题,可能占用较多内存(不过多数情况可优化,如爬楼梯用变量滚动优化空间到 \(O(1)\) )。
状态定义难度:复杂问题(如二维 DP、三维 DP )的状态抽象和转移方程推导较难,需要大量练习积累。
四、经典案例巩固(二维 DP 示例:最长公共子序列)
问题:给定两个字符串 text1
和 text2
,找它们最长公共子序列的长度(子序列不要求连续)。
状态定义:
dp[i][j]
表示text1
前i
个字符、text2
前j
个字符的最长公共子序列长度。转移方程:
若
text1[i-1] == text2[j-1]
(注意字符串索引),则dp[i][j] = dp[i-1][j-1] + 1
(公共字符加入子序列 )。否则,
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
(取 “不包含text1[i-1]
” 或 “不包含text2[j-1]
” 的最大值 )。
初始化:
dp[0][j] = 0
(text1
为空时,公共子序列长度为 0 ),dp[i][0] = 0
(同理 )。遍历计算:双重循环遍历
i
(1 到len(text1)
)和j
(1 到len(text2)
)。返回结果:
dp[len(text1)][len(text2)]
。
掌握动态规划的关键是多练习、多拆解问题,从简单的一维 DP(爬楼梯、斐波那契)入手,再过渡到二维(最长公共子序列、背包问题),逐步理解状态定义和转移的精髓~