1. 动态规划刷题进阶指南
刷算法题就像打游戏升级,动态规划(DP)就是那个让你又爱又恨的终极Boss。作为算法面试中的常青树,动态规划问题在各大厂技术面中的出现频率高达60%以上。但很多人在刷完基础题后,遇到稍微复杂点的DP问题就束手无策。
我在准备面试时刷了300+道动态规划题目,从最初连状态转移方程都写不出来,到现在能快速拆解复杂DP问题。今天就来分享动态规划刷题第三阶段的进阶心得,这些经验帮助我在面试中解决了多个hard级别的DP难题。
2. 动态规划问题分类与特征识别
2.1 典型DP问题分类
动态规划问题大致可以分为以下几类:
- 线性DP:最长递增子序列(LIS)、最大子数组和等
- 区间DP:石子合并、括号生成等
- 树形DP:二叉树中的最大路径和等
- 状态压缩DP:旅行商问题(TSP)、铺砖问题等
- 数位DP:数字1的个数统计等
- 概率DP:骰子点数概率计算等
2.2 DP问题识别特征
一个题目适合用动态规划解决,通常具备以下特征:
- 最优子结构:问题的最优解包含子问题的最优解
- 重叠子问题:递归求解时会重复计算相同的子问题
- 无后效性:当前状态一旦确定,后续决策不受之前决策影响
识别技巧:当题目要求"最大/最小/最多/最少"且暴力解法时间复杂度很高时,优先考虑DP
3. 动态规划解题框架详解
3.1 标准解题四步法
-
定义状态:明确dp数组的含义
- 一维:dp[i]表示前i个元素的最优解
- 二维:dp[i][j]表示区间i到j的最优解
-
状态转移方程:找出dp[i]与之前状态的关系
- 常见形式:dp[i] = max/min(dp[j] + cost, ...)
-
初始化:设置边界条件
- 通常需要初始化dp[0]或dp[0][0]等
-
确定遍历顺序:
- 线性DP通常从前向后
- 区间DP通常从小区间到大区间
3.2 状态设计进阶技巧
-
状态升维:当一维状态不够时增加维度
- 例:股票问题中加入持有/不持有状态
-
状态压缩:用位运算减少空间复杂度
- 适用于状态数较少的情况
-
滚动数组优化:只保留必要的前驱状态
- 可将O(n)空间优化为O(1)
4. 经典难题解析与实现
4.1 最长递增子序列(LIS)变种
问题:给定数组nums,找到最长的严格递增子序列的长度,且子序列中相邻元素的差值不超过k。
python复制def lengthOfLIS(nums, k):
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j] and nums[i] - nums[j] <= k:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp) if nums else 0
优化思路:使用线段树或树状数组将时间复杂度从O(n²)优化到O(nlogn)
4.2 背包问题进阶
多重背包问题:每种物品有数量限制
python复制def multiple_knapsack(W, wt, val, cnt):
dp = [0] * (W + 1)
for i in range(len(wt)):
for j in range(W, wt[i] - 1, -1):
for k in range(1, min(cnt[i], j // wt[i]) + 1):
dp[j] = max(dp[j], dp[j - k * wt[i]] + k * val[i])
return dp[W]
二进制优化:将多重背包转化为01背包,时间复杂度从O(VΣn[i])降到O(VΣlog n[i])
5. 动态规划优化技巧
5.1 单调队列优化
适用于状态转移方程形如:
dp[i] = max/min(dp[j] + cost(j)) + f(i),其中j在某个滑动窗口内
例题:滑动窗口最大值问题
python复制from collections import deque
def maxSlidingWindow(nums, k):
q = deque()
res = []
for i, num in enumerate(nums):
while q and nums[q[-1]] <= num:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
res.append(nums[q[0]])
return res
5.2 四边形不等式优化
适用于区间DP问题,可以将时间复杂度从O(n³)优化到O(n²)
适用条件:
- 区间包含单调性
- 四边形不等式成立
6. 常见错误与调试技巧
6.1 典型错误类型
- 状态定义不当:导致无法正确转移
- 边界条件错误:特别是dp[0]或空输入情况
- 遍历顺序错误:如区间DP未按区间长度遍历
- 空间优化错误:滚动数组覆盖了还需使用的状态
6.2 调试方法
- 打印DP表:可视化整个状态转移过程
- 小数据测试:手动计算预期结果进行比对
- 边界测试:空输入、单个元素等特殊情况
- 压力测试:大数据量检查是否超时或内存溢出
7. 动态规划刷题路线建议
7.1 分阶段刷题计划
-
基础阶段(2周):
- 斐波那契数列
- 爬楼梯
- 最小路径和
- 背包问题
-
进阶阶段(3周):
- 股票买卖系列
- 打家劫舍系列
- 子序列问题
- 区间DP
-
高手阶段(4周):
- 状态压缩DP
- 树形DP
- 数位DP
- 概率DP
7.2 推荐题目清单
-
必刷经典:
-
- 编辑距离
-
- 戳气球
-
- 正则表达式匹配
-
- 买卖股票的最佳时机 IV
-
-
面试高频:
-
- 最长递增子序列
-
- 零钱兑换
-
- 最长公共子序列
-
- 俄罗斯套娃信封问题
-
-
挑战题目:
-
- 奇怪的打印机
-
- 播放列表的数量
-
- 安排邮筒
-
8. 个人实战经验分享
在刷动态规划题目时,我总结出几个关键心得:
- 先写暴力递归:理解问题本质后再优化
- 画状态转移图:帮助理清状态间关系
- 从特殊到一般:先考虑简单情况再推广
- 记录错题本:分类整理易错题型
最难掌握的是状态设计,我通常会问自己:
- 这个状态能否完整描述当前局面?
- 能否从这个状态转移到下一个状态?
- 状态空间是否在合理范围内?
最后提醒一点:动态规划没有银弹,多练习才是王道。我建议至少刷50道不同类别的DP题目,才能真正掌握其精髓。