1. 动态规划基础回顾与Hot100定位
动态规划(Dynamic Programming)作为算法领域的核心方法论,在各类编程面试和竞赛中占据着不可撼动的地位。我初次接触动态规划是在准备校招时,面对"爬楼梯"问题百思不得其解的经历至今记忆犹新。Hot100作为LeetCode精选的百大高频面试题集合,其动态规划专题涵盖了从入门到进阶的典型场景,是检验DP掌握程度的绝佳试金石。
动态规划的本质是通过将原问题分解为相互重叠的子问题,并存储子问题的解来避免重复计算。这种思想在解决最优化问题时尤为有效。在Hot100的DP专题中,问题类型主要分为以下几类:
- 线性DP(如打家劫舍系列)
- 区间DP(如最长回文子串)
- 背包问题及其变种
- 状态机DP(如买卖股票系列)
- 树形DP(如二叉树中的最大路径和)
提示:动态规划解题的关键在于识别问题是否具有"最优子结构"和"重叠子问题"这两个特性。如果一个问题可以被分解为若干子问题,且子问题之间存在大量重复计算,那么DP就是潜在的解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Hot100经典DP问题精讲
2.1 爬楼梯问题(70. Climbing Stairs)
作为动态规划的"Hello World",这道题完美展示了DP的核心思想。题目要求计算爬到n阶楼梯的不同方法数,每次可以爬1或2阶。
python复制def climbStairs(n: int) -> int:
if n <= 2: return n
dp = [0]*(n+1)
dp[1], dp[2] = 1, 2
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
这个解法的时间复杂度为O(n),空间复杂度也是O(n)。但通过观察可以发现,当前状态只依赖于前两个状态,因此可以优化到O(1)空间:
python复制def climbStairs(n: int) -> int:
if n <= 2: return n
a, b = 1, 2
for _ in range(3, n+1):
a, b = b, a+b
return b
2.2 打家劫舍系列(198. House Robber)
这个系列问题展示了如何设计状态转移方程。基础版要求不能抢劫相邻房屋的情况下获得最大收益。
python复制def rob(nums: List[int]) -> int:
n = len(nums)
if n == 1: return nums[0]
dp = [0]*n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
return dp[-1]
进阶版中,房屋排成环形(213. House Robber II),这时我们可以将问题分解为两个子问题:抢劫第一家不抢最后一家,或者不抢第一家抢劫最后一家,取两者最大值。
2.3 买卖股票系列(121. Best Time to Buy and Sell Stock)
这个系列展示了状态机DP的应用。基础版只允许一次买卖:
python复制def maxProfit(prices: List[int]) -> int:
min_price = float('inf')
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
当允许无限次买卖时(122. Best Time to Buy and Sell Stock II),策略变为在每次价格上涨时都进行买卖:
python复制def maxProfit(prices: List[int]) -> int:
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
3. DP问题解题框架与技巧
3.1 四步解题法
根据我的刷题经验,解决DP问题可以遵循以下步骤:
-
定义DP数组的含义
- 明确dp[i]或dp[i][j]表示什么
- 例如在最长递增子序列问题中,dp[i]表示以nums[i]结尾的最长子序列长度
-
找出状态转移方程
- 这是DP问题的核心,需要分析问题如何从子问题构建
- 例如在编辑距离问题中:
python复制if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
-
初始化边界条件
- 处理初始状态,避免数组越界
- 例如在背包问题中,dp[0][j]和dp[i][0]通常初始化为0
-
确定遍历顺序
- 根据状态转移方程决定是正序还是逆序
- 例如在零钱兑换问题中,完全背包需要正序,而01背包需要逆序
3.2 空间优化技巧
很多DP问题可以进行空间优化,常见方法包括:
- 滚动数组:当当前状态只依赖于有限的前几个状态时,可以用固定大小的数组代替整个DP表
- 状态压缩:用位运算等方式减少状态存储空间
- 原地修改:直接在输入数组上进行DP计算(如三角形最小路径和问题)
以斐波那契数列为例,优化前后的空间对比:
python复制# 未优化
dp = [0]*(n+1)
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
# 优化后
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a+b
4. Hot100中值得深入研究的DP问题
4.1 最长递增子序列(300. Longest Increasing Subsequence)
这道题展示了如何将O(n²)的DP解法优化到O(nlogn):
python复制def lengthOfLIS(nums: List[int]) -> int:
tails = []
for num in nums:
idx = bisect.bisect_left(tails, num)
if idx == len(tails):
tails.append(num)
else:
tails[idx] = num
return len(tails)
这个解法利用了贪心思想:维护一个tails数组,其中tails[i]表示长度为i+1的所有递增子序列的最小末尾。通过二分查找确定每个数字应该插入的位置。
4.2 编辑距离(72. Edit Distance)
这道题展示了二维DP的典型应用,计算将一个单词转换成另一个单词所需的最少操作次数(插入、删除、替换):
python复制def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
return dp[m][n]
4.3 正则表达式匹配(10. Regular Expression Matching)
这道hard题目展示了如何处理复杂的匹配规则:
python复制def isMatch(s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False]*(n+1) for _ in range(m+1)]
dp[0][0] = True
for j in range(1, n+1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
for i in range(1, m+1):
for j in range(1, n+1):
if p[j-1] == s[i-1] or p[j-1] == '.':
dp[i][j] = dp[i-1][j-1]
elif p[j-1] == '*':
dp[i][j] = dp[i][j-2]
if p[j-2] == s[i-1] or p[j-2] == '.':
dp[i][j] |= dp[i-1][j]
else:
dp[i][j] = False
return dp[m][n]
5. 动态规划问题调试技巧
在解决DP问题时,我总结了一些实用的调试方法:
-
可视化DP表格
- 打印出二维DP表格,观察状态转移是否正确
- 对于一维DP,可以记录历史状态变化
-
边界条件测试
- 专门测试空输入、单元素输入等边界情况
- 例如在背包问题中测试容量为0的情况
-
小规模手动计算
- 选择小测试用例手动计算预期结果
- 对比程序输出与手动计算结果
-
状态转移验证
- 对于每个状态,验证其转移来源是否正确
- 例如在编辑距离问题中,检查三种操作的最小值选择
-
空间优化前的基准测试
- 在实施空间优化前,保留原始DP解法作为基准
- 确保优化后的结果与原始解法一致
注意:DP问题的调试往往比普通算法更困难,因为状态之间存在复杂的依赖关系。建议从简单案例入手,逐步增加复杂度,并在每个步骤验证中间结果。
