动态规划
1. 核心思想
拆分重复子问题,保存中间结果避免重复计算。把大问题拆解成规模更小的子问题,子问题会反复出现;用数组 / 表格记录子问题答案,后续直接查表,不用重复递归暴力求解,大幅降低时间复杂度。
2. 两大必要条件
- 最优子结构大问题的最优解 = 若干更小子问题的最优解组合。例:最短路径、背包、最长递增子序列。
- 重叠子问题递归过程中会重复计算一模一样的子问题,缓存结果才有优化意义。
3. 两种实现方式
- **自顶向下(记忆化递归)**递归求解,遇到算过的子问题直接取缓存,没算过再计算并存起来。
- **自底向上(递推 DP 表,最常用)**从小规模子问题开始循环计算,逐步推到大问题,用 dp 数组存储状态。
4. 标准解题四步
- 定义 dp 数组含义:
dp[i]代表什么状态; - 推导状态转移方程:当前状态如何由前面状态推出;
- 初始化边界条件:dp 0、dp 1 等初始值;
- 确定遍历顺序,循环填表,输出目标结果。
一.70. 爬楼梯
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
解题思路
想要到达第 x 阶楼梯,只有两种最后一步走法:
- 最后一步走 1 阶 :前面需要先走到第
x-1阶 - 最后一步走 2 阶 :前面需要先走到第
x-2阶
所以总走法满足公式:f(x) = f(x-1) + f(x-2)边界条件(递归出口):f(1) = 1,f(2) = 2
普通暴力递归缺陷:大量重复计算,比如算 f (5) 要算 f (4)+f (3),算 f (4) 又要重复算 f (3),数值大了会超时。记忆化优化 :用数组 memo 记录每一个阶数对应的答案,计算过一次就存起来,下次需要直接查表,不再递归。
完整步骤:
- 创建 memo 数组,长度 n+1,下标对应楼梯阶数,初始全为 0(0 代表未计算)
- 定义递归函数 dfs (x):
- 基线条件:x=1 返回 1,x=2 返回 2
- 优化判断:如果 memo x≠0,代表之前算过,直接返回缓存值
- 未计算:套用公式
memo[x] = dfs(x-1)+dfs(x-2),存入缓存再返回
- 调用 dfs (n) 从目标台阶开始递归求解
代码实现
Go
func climbStairs(n int) int {
//动态规划思路:用数组存已经算过的值
memo := make([]int, n + 1)//缓存子问题结果
var dfs func(x int) int;//先在作用域注册dfs这个函数变量名
dfs = func(x int) int { //再实现递归调用
if x == 1 {
return 1
}
if x == 2 {
return 2
}
if memo[x] != 0 {
return memo[x] //直接查表,不用再递归
}
//递推公式:x阶的走法 = x-1阶走法 + x-2阶走法
memo[x] = dfs(x - 1) + dfs(x - 2)
return memo[x]
}
return dfs(n)
}
总结
本题使用动态规划中的记忆特性,暴力求解时递推f(n) = f(n - 1) + f(n - 2),会导致超时,使用记忆特性就可以查找memo,减少重复,减小时间复杂度。
二.509. 斐波那契数
题目描述
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
解题思路
递归会导致时间复杂度过大,所以可以用dp动态规划来解题
代码实现
Go
func fib(n int) int {
if n == 0 {
return 0
}
dp := make([]int, n + 1)
dp[0] = 0
dp[1] = 1
for i := 2; i <= n; i++ {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n]
}
总结
递归,动态规划
三.1137. 第 N 个泰波那契数
题目描述
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
示例 1:
输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
示例 2:
输入:n = 25
输出:1389537
解题思路
和上一题几乎相同,递归思路+动态规划dp
代码实现
Go
func tribonacci(n int) int {
//dp
if n == 0 {
return 0
}
dp := make([]int, n + 1)
dp[1] = 1
if n >= 2 {
dp[2] = 1
}
for i := 3; i <= n; i++ {
dp[i] = dp[i - 1] + dp[i - 2]+ dp[i - 3]
}
return dp[n]
}
总结
利用dp缩减纯递归的时间复杂度
四.面试题 08.01. 三步问题
题目描述
三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。
示例 1:
输入:n = 3
输出:4
说明:有四种走法
示例 2:
输入:n = 5
输出:13
提示:
- n 范围在1, 1000000之间
解题思路
利用动态规划,递推,先看最后一步可以走1/2/3阶,所以实现dpi = dpi-1 + dpi-2 + dpi-3
又由于初始条件
dp[1] = 1(1 阶只有 1 种走法)dp[2] = 2(2 阶有 2 种走法:1+1、2)dp[3] = 4(3 阶有 4 种走法)
实现走法相加
代码实现
Go
func waysToStep(n int) int {
const mod = 1000000007
//看最后一步,上到n之前有三种情况
//分别是n-1, n-2, n-3,对此进行倒推
if n == 1 {
return 1
}
if n == 2 {
return 2
}
if n == 3 {
return 4
}
//定义dp
dp := make([]int, n)
dp[0] = 1
dp[1] = 2
dp[2] = 4
//从第4阶开始循环
for i := 3; i < n; i++ {
dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % mod
}
//返回第n阶的答案
return dp[n - 1]
}
总结
动态规划+递推