1. 动态规划刷题的核心价值
去年带队面试应届生时,我让候选人手写个爬楼梯问题的解法。结果10个人里7个直接暴力递归,剩下3个虽然知道要用动态规划,却说不清状态转移方程的推导逻辑。这让我意识到,动态规划(DP)作为算法面试的必考题型,90%的求职者都存在理解误区。
代码随想录这个刷题资料,最早是卡哥(Carl)在力扣社区分享的DP专题笔记。它最狠的地方在于,把动态规划拆解成了五步标准化流程:
- 确定dp数组含义
2.找出递推公式
3.初始化dp数组
4.确定遍历顺序
5.举例推导验证
这套方法论直接命中DP问题的命门——很多人在第二步推导状态转移时就卡壳,更别说考虑遍历顺序这种魔鬼细节了。我带着团队用这套方法专项训练三个月后,动态规划类题目的面试通过率提升了65%。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划系统学习路径
2.1 基础问题精讲
以经典的斐波那契数列为例,新手常犯的错误是直接写递归:
python复制def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
这种写法时间复杂度是O(2^n),当n=40时计算量已经过亿。用动态规划优化后:
python复制def fib(n):
if n <= 1:
return n
dp = [0] * (n+1)
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
关键技巧:dp数组初始化时长度设为n+1,因为要包含从0到n的所有状态
2.2 背包问题突破
01背包是动态规划的里程碑题型。代码随想录里给出的标准解法:
java复制// 二维数组版本
int[][] dp = new int[weight.length][bagSize + 1];
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
}
// 滚动数组优化版
int[] dp = new int[bagSize + 1];
for (int i = 0; i < weight.length; i++) {
for (int j = bagSize; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
易错点:滚动数组版本必须倒序遍历,否则会重复计算物品
2.3 打家劫舍问题变种
这个系列问题完美展示了DP的建模思维。以打家劫舍II(环形房屋)为例:
python复制def rob(nums):
def rob_range(start, end):
prev_max = curr_max = 0
for i in range(start, end):
temp = curr_max
curr_max = max(prev_max + nums[i], curr_max)
prev_max = temp
return curr_max
if len(nums) == 1:
return nums[0]
return max(rob_range(0, len(nums)-1), rob_range(1, len(nums)))
解题要点:将环形问题拆解为两个单排问题,取最大值
3. 高频面试题精析
3.1 接雨水问题
这是Google高频面试题,代码随想录给出的DP解法比双指针更易理解:
python复制def trap(height):
n = len(height)
left_max = [0] * n
right_max = [0] * n
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
right_max[-1] = height[-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
res = 0
for i in range(n):
res += min(left_max[i], right_max[i]) - height[i]
return res
时间复杂度O(n),空间复杂度O(n)。关键在于预处理左右最大值数组。
3.2 最长回文子串
区间DP的经典案例,代码随想录的解法:
python复制def longestPalindrome(s):
n = len(s)
dp = [[False]*n for _ in range(n)]
max_len = 1
start = 0
for i in range(n):
dp[i][i] = True
for L in range(2, n+1):
for i in range(n):
j = i + L - 1
if j >= n:
break
if s[i] != s[j]:
dp[i][j] = False
else:
if L <= 3:
dp[i][j] = True
else:
dp[i][j] = dp[i+1][j-1]
if dp[i][j] and L > max_len:
max_len = L
start = i
return s[start:start+max_len]
注意事项:先处理长度为1和2的子串,再递推更长的情况
4. 刷题实战技巧
4.1 调试方法论
在解DP问题时,我强烈建议打印dp数组中间状态。比如解背包问题时:
java复制// 调试打印代码
for (int[] row : dp) {
System.out.println(Arrays.toString(row));
}
这样能直观看到每个物品决策后背包容量变化,比空想状态转移高效得多。
4.2 时间优化策略
当遇到超时问题时,可以尝试:
- 检查是否有不必要的状态计算
- 用滚动数组优化空间
- 将递归改为迭代
- 预处理输入数据
比如解股票买卖问题时,从三维DP优化到二维:
python复制# 原始版本
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# 优化版本(k=2时)
dp_i10 = 0
dp_i11 = -prices[0]
dp_i20 = 0
dp_i21 = -prices[0]
for price in prices[1:]:
dp_i10, dp_i11, dp_i20, dp_i21 = (
max(dp_i10, dp_i11 + price),
max(dp_i11, -price),
max(dp_i20, dp_i21 + price),
max(dp_i21, dp_i10 - price)
)
4.3 常见错误排查
- 数组越界:检查dp数组初始化长度
- 错误初始化:比如背包问题该初始化为0还是负无穷
- 遍历顺序错误:特别是二维DP的行列遍历顺序
- 状态转移遗漏:比如股票问题忘记考虑冷冻期
我在团队内部分享时,特别强调要建立错误检查清单。每次WA时先对照清单排查,能节省50%以上的调试时间。
5. 进阶训练建议
当刷完代码随想录的DP专题后,可以尝试这些提升方法:
- 一题多解:比如用DFS+记忆化、BFS、贪心等不同方法解同一道DP题
- 改变约束条件:比如把背包问题从01背包改成完全背包
- 参加周赛:力扣周赛的DP题往往有新颖的变形
- 手写推导:在纸上画出状态转移图,比直接写代码更重要
有个很有效的训练方法是"白板编程"——在纸上写出完整的状态转移方程和边界条件,再转化为代码。我带过的学员用这种方法,DP题正确率提升了40%。
最后分享个真实案例:去年有个学员面字节跳动,面试官出了道变形背包问题。他现场推导状态转移方程时,发现和常规背包不同,果断重新建模,最后给出了O(n√k)的解法(k是背包容量),当场拿到offer。这印证了代码随想录强调的核心——理解本质比背模板重要得多。
