1. 动态规划算法核心思想解析
动态规划(Dynamic Programming)作为算法设计中的经典方法论,本质上是通过将复杂问题分解为相互重叠的子问题,并存储子问题的解来避免重复计算。在解决LeetCode热题100中的动态规划问题时,我们需要掌握三个关键特征识别方法:
最优子结构:问题的最优解包含子问题的最优解。例如爬楼梯问题中,到达第n阶的方案数取决于第n-1阶和第n-2阶的方案数之和。
重叠子问题:递归求解时会重复计算相同子问题。以斐波那契数列为例,传统递归会重复计算fib(3)、fib(2)等子问题。
无后效性:当前状态只与之前状态有关,与后续状态无关。打家劫舍问题中,当前房屋的选择只与前一个房屋的决策相关。
实际解题时建议先画出递归树,观察是否存在大量重复计算的节点。这是判断是否适用DP的重要依据。
2. 基础模型实战:爬楼梯与杨辉三角
2.1 爬楼梯问题(LeetCode 70)
这是最经典的入门级DP问题,题目要求计算爬到n阶楼梯的不同方法数,每次可以爬1或2个台阶。
状态转移方程推导过程:
- 定义dp[i]表示到达第i阶的方案数
- 由于每次只能跨1或2阶,所以dp[i] = dp[i-1] + dp[i-2]
- 边界条件:dp[0]=1(地面),dp[1]=1
Python实现代码示例:
def climbStairs(n): if n <= 1: return 1 dp = [0]*(n+1) dp[0], dp[1] = 1, 1 for i in range(2, n+1): dp[i] = dp[i-1] + dp[i-2] return dp[n]空间优化技巧:实际上只需要维护前两个状态,可将空间复杂度从O(n)降到O(1):
def climbStairs(n): a, b = 1, 1 for _ in range(2, n+1): a, b = b, a + b return b2.2 杨辉三角(LeetCode 118)
虽然题目看似简单,但能很好训练二维DP的思维。要求生成前numRows行杨辉三角。
关键观察点:
- 每行首尾元素始终为1
- 其余元素等于上一行同列与前列元素之和
- 状态方程:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
典型错误:直接修改列表时未考虑前一行数据会被覆盖的问题。正确做法应新建临时列表:
def generate(numRows): res = [] for i in range(numRows): row = [1]*(i+1) for j in range(1, i): row[j] = res[i-1][j-1] + res[i-1][j] res.append(row) return res3. 线性DP进阶:打家劫舍系列
3.1 基础版打家劫舍(LeetCode 198)
问题描述:不能连续抢劫相邻房屋,求最大收益。
状态设计要点:
- dp[i]表示前i个房屋能获得的最大金额
- 对于第i个房屋有两种选择:
- 抢劫:dp[i] = nums[i] + dp[i-2]
- 不抢:dp[i] = dp[i-1]
- 取两者较大值:dp[i] = max(dp[i-1], nums[i] + dp[i-2])
边界条件处理:
- dp[0] = nums[0]
- dp[1] = max(nums[0], nums[1])
空间优化版实现:
def rob(nums): prev, curr = 0, 0 for num in nums: prev, curr = curr, max(curr, prev + num) return curr3.2 环形房屋变种(LeetCode 213)
新增约束条件:房屋环形排列,首尾视为相邻。
解题技巧:将问题拆分为两个子问题:
- 不抢第一个房屋:求nums[1:]的最大值
- 不抢最后一个房屋:求nums[:-1]的最大值 最终取两者较大值
def rob(nums): def helper(arr): prev, curr = 0, 0 for num in arr: prev, curr = curr, max(curr, prev + num) return curr if len(nums) == 1: return nums[0] return max(helper(nums[1:]), helper(nums[:-1]))4. 完全背包类问题实战
4.1 完全平方数(LeetCode 279)
问题:将正整数n表示为完全平方数的和,求最少需要几个数。
关键突破点:
- 将问题转化为背包问题:物品是1,4,9...等平方数,背包容量为n
- 完全背包特性:每个平方数可重复使用
- dp[i]表示组成i需要的最少平方数
状态转移方程: dp[i] = min(dp[i], dp[i - jj] + 1) 对所有jj <= i
实现时注意初始化dp数组为极大值:
def numSquares(n): dp = [float('inf')]*(n+1) dp[0] = 0 for i in range(1, n+1): j = 1 while j*j <= i: dp[i] = min(dp[i], dp[i - j*j] + 1) j += 1 return dp[n]4.2 零钱兑换(LeetCode 322)
与完全平方数类似,但硬币面额不固定。给定不同面额的硬币和总金额,求凑成总金额所需的最少硬币数。
易错点:
- 需要处理无法凑出的情况(返回-1)
- 初始化时dp[0]=0,其余为inf
Python实现:
def coinChange(coins, amount): dp = [float('inf')]*(amount + 1) dp[0] = 0 for coin in coins: for i in range(coin, amount+1): dp[i] = min(dp[i], dp[i - coin] + 1) return dp[amount] if dp[amount] != float('inf') else -15. 动态规划解题方法论
5.1 四步解题框架
- 定义状态:明确dp数组的含义
- 确定转移方程:分析状态间的递推关系
- 初始化边界条件:处理初始状态和特殊情况
- 确定计算顺序:自底向上或自顶向下
5.2 调试技巧
- 打印DP表:二维问题可打印矩阵观察填充过程
- 小规模测试:先用简单用例验证正确性
- 边界检查:特别注意n=0,1等特殊情况
5.3 复杂度优化方向
空间优化:
- 滚动数组(如斐波那契数列)
- 状态压缩(如背包问题降维)
时间优化:
- 预处理数据
- 剪枝策略
- 数学公式推导
6. 高频错误与解决方案
数组越界:
- 确保dp数组大小足够(通常是n+1)
- 检查循环边界条件
初始化错误:
- 明确dp[0]的物理意义
- 处理特殊输入(如空数组)
转移方程错误:
- 用具体例子手动推导验证
- 对比经典模型找差异点
顺序错误:
- 完全背包问题内层循环正序
- 0-1背包问题内层循环逆序
建议建立错题本,记录每种错误类型及对应的修正方法。动态规划问题往往调试困难,积累经验尤为重要。