1. 动态规划入门:从爬楼梯问题开始
动态规划(Dynamic Programming)是算法学习中最重要也最令人头疼的部分之一。我第一次接触动态规划时,完全被那些状态转移方程和重叠子问题搞晕了。直到遇到LeetCode第70题爬楼梯问题,才真正理解了动态规划的精髓。
爬楼梯问题的描述很简单:假设你正在爬楼梯,需要n阶才能到达楼顶。每次你可以爬1或2个台阶。问有多少种不同的方法可以爬到楼顶?当n=2时,有2种方法(1+1或直接2);当n=3时,有3种方法(1+1+1、1+2、2+1)。
这个问题的关键在于发现其中的递推关系。要到达第n阶台阶,你只能从第n-1阶跨1步上来,或者从第n-2阶跨2步上来。因此,到达第n阶的方法数等于到达第n-1阶和第n-2阶方法数之和。这正是斐波那契数列的定义!
python复制def climbStairs(n: int) -> int:
if n <= 2:
return n
dp = [0]*(n+1)
dp[1] = 1
dp[2] = 2
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[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
注意:很多初学者会尝试用回溯法解决这个问题,但当n较大时(如n=45),回溯法会因指数级时间复杂度而超时。动态规划将时间复杂度降到了O(n),这正是它的优势所在。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 杨辉三角:理解二维动态规划
LeetCode第118题杨辉三角是理解二维动态规划的绝佳入门题。杨辉三角的每个数字等于它上方两个数字之和,这个性质本身就是动态规划的状态转移方程。
给定一个非负整数numRows,生成杨辉三角的前numRows行。例如,输入5,输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
python复制def generate(numRows: int) -> List[List[int]]:
triangle = []
for row_num in range(numRows):
row = [None for _ in range(row_num+1)]
row[0], row[-1] = 1, 1
for j in range(1, len(row)-1):
row[j] = triangle[row_num-1][j-1] + triangle[row_num-1][j]
triangle.append(row)
return triangle
这个问题的关键在于理解二维数组的填充顺序。我们从上到下逐行构建,每行的首尾元素都是1,中间元素等于上一行相邻两个元素之和。这种逐层构建的方式正是动态规划中"自底向上"方法的体现。
在实际应用中,杨辉三角与组合数学密切相关。第n行第k个数字(从0开始计数)就是组合数C(n,k)。这个性质在概率统计中经常用到。
3. 打家劫舍问题:决策型动态规划
LeetCode第198题打家劫舍是经典的决策型动态规划问题。题目描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
例如,对于[2,7,9,3,1],最优解是2+9+1=12。
这个问题的关键在于定义状态和状态转移方程。我们可以定义dp[i]为考虑前i个房屋时能获得的最大金额。对于第i个房屋,我们有两个选择:
- 偷窃它,那么不能偷窃第i-1个房屋,总金额为nums[i] + dp[i-2]
- 不偷窃它,总金额为dp[i-1]
因此状态转移方程为:dp[i] = max(dp[i-1], nums[i] + dp[i-2])
python复制def rob(nums: List[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0]*len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], nums[i] + dp[i-2])
return dp[-1]
同样,这个解法可以优化空间复杂度到O(1):
python复制def rob(nums: List[int]) -> int:
prev, curr = 0, 0
for num in nums:
prev, curr = curr, max(curr, prev + num)
return curr
实战技巧:这类"取舍"型动态规划问题通常的状态定义是"考虑前i个元素时的最优解",状态转移则考虑是否选择当前元素。类似的问题还有股票买卖、工作调度等。
4. 完全平方数:最优化动态规划
LeetCode第279题完全平方数要求我们找到最少数量的完全平方数,使得它们的和等于给定的正整数n。例如,n=12,返回3(因为12=4+4+4);n=13,返回2(因为13=4+9)。
这个问题可以转化为动态规划问题,其中dp[i]表示组成整数i所需的最少完全平方数数量。初始条件是dp[0]=0,因为组成0不需要任何完全平方数。
状态转移方程是:对于每个i,我们尝试所有可能的完全平方数jj(jj ≤ i),然后取最小值:
dp[i] = min(dp[i], dp[i - j*j] + 1)
python复制def numSquares(n: int) -> int:
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]
这个解法的时间复杂度是O(n√n),因为对于每个i,我们需要检查最多√i个完全平方数。
这个问题还有一个数学解法,基于拉格朗日四平方定理:任何自然数都可以表示为四个整数平方的和。实际上,除了形如4^k(8m+7)的数需要四个平方数外,大多数数可以用不超过三个平方数表示。但动态规划解法更通用,也更容易理解和实现。
5. 零钱兑换:背包型动态规划
LeetCode第322题零钱兑换是经典的背包问题变种。题目描述:给定不同面额的硬币coins和一个总金额amount,计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1。
例如,coins = [1, 2, 5], amount = 11,返回3(11 = 5 + 5 + 1)。
这个问题与完全平方数问题类似,但硬币面额不一定是完全平方数。我们可以定义dp[i]为组成金额i所需的最少硬币数。初始化dp[0]=0,其他为无穷大。
状态转移方程是:对于每个金额i,遍历所有硬币面额coin,如果coin ≤ i,则:
dp[i] = min(dp[i], dp[i - coin] + 1)
python复制def coinChange(coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
这个解法的时间复杂度是O(amount * n),其中n是硬币种类数。
常见错误:初学者可能会先排序coins并从大到小贪心地选择,这在某些情况下(如coins=[1,3,4], amount=6)会得到次优解(4+1+1=3个硬币),而最优解其实是3+3=2个硬币。动态规划能保证找到全局最优解。
6. 动态规划解题框架总结
通过以上五个经典问题,我们可以总结出动态规划解题的一般步骤:
-
定义子问题:明确dp数组的含义,如dp[i]通常表示考虑前i个元素时的最优解。
-
确定初始条件:如dp[0]、dp[1]等的初始值。
-
建立状态转移方程:找出dp[i]与之前状态的关系,这是最核心也最难的部分。
-
确定计算顺序:通常是自底向上,从小问题逐步解决大问题。
-
考虑空间优化:如果当前状态只依赖于有限的前几个状态,可以用滚动数组等方法优化空间。
对于不同类型的动态规划问题,有一些常见模式:
- 线性问题:如爬楼梯、打家劫舍,状态转移通常只涉及前一个或前几个状态。
- 二维问题:如杨辉三角、编辑距离,需要二维dp数组。
- 背包问题:如零钱兑换、完全平方数,需要考虑"选择"与"不选择"当前元素的两种情况。
在实际面试中,建议先写出基础解法,再考虑优化。清晰地解释你的思路比直接写出最优解更重要。动态规划问题往往有多种解法,理解其本质才能灵活应对各种变种。
