一、动态规划核心思想
动态规划本质:拆分大问题为重叠子问题,保存子问题结果,避免重复计算
- 适用场景:问题可拆解、子问题重复、存在最优子结构
- 核心优势:把暴力递归指数级复杂度,优化为多项式复杂度
- 两大关键点:状态定义 、状态转移方程
二、DP 解题标准四步(必按顺序分析)
- 状态定义 :定义 dp 数组含义,
dp[i]代表什么 - 初始状态:确定 dp 数组初始值、边界条件
- 状态转移:推导递推公式,建立前后状态关系
- 结果输出:确定最终答案对应 dp 数组位置
三、入门例题 1:斐波那契数列
题目描述
F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2),求第 n 项数值。
1. 暴力递归(缺点:大量重复计算)
int fib(int n)
{
if(n == 0) return 0;
if(n == 1) return 1;
return fib(n-1) + fib(n-2);
}
2. 基础 DP 数组写法
#include <iostream>
#include <vector>
using namespace std;
int fibDP(int n)
{
if(n <= 1) return n;
// 1. 状态定义:dp[i] 表示第i个斐波那契数
vector<int> dp(n+1);
// 2. 初始状态
dp[0] = 0;
dp[1] = 1;
// 3. 状态转移方程
for(int i = 2; i <= n; i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
// 4. 输出结果
return dp[n];
}
3. 空间优化版(滚动变量,O (1) 空间)
仅依赖前两个值,无需完整数组,刷题常用:
int fibOpt(int n)
{
if(n <= 1) return n;
int a = 0, b = 1, c;
for(int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return b;
}
四、入门例题 2:爬楼梯(LeetCode 70)
题目描述
每次只能爬 1 阶或 2 阶楼梯,求爬到第 n 阶共有多少种不同方法。
解题推导
- 状态定义 :
dp[i]= 爬到第 i 阶楼梯的方法数 - 初始状态 :
- dp 1 = 1(1 阶只有 1 种)
- dp 2 = 2(2 阶:1+1 / 2)
- 状态转移 :到第 i 阶,只能从 i-1 或 i-2 过来
dp[i] = dp[i-1] + dp[i-2]
完整代码
int climbStairs(int n)
{
if(n <= 2) return n;
vector<int> dp(n+1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
空间优化版
int climbStairsOpt(int n)
{
if(n <= 2) return n;
int pre1 = 1, pre2 = 2, cur;
for(int i = 3; i <= n; i++)
{
cur = pre1 + pre2;
pre1 = pre2;
pre2 = cur;
}
return pre2;
}
五、DP 与递归、贪心简单区分
- 递归:自顶向下求解,重复计算多,效率低
- 动态规划:自底向上递推,缓存子问题结果,效率高
- 贪心:每一步选局部最优,不一定能得到全局最优;DP 一定求全局最优
六、动态规划常见分类
- 一维 DP:斐波那契、爬楼梯、打家劫舍(今日内容)
- 二维 DP:最长公共子序列、背包问题、路径问题
- 区间 DP、状态压缩 DP、树形 DP(进阶难点)
七、新手高频易错点
- 状态定义模糊,无法推导转移方程(DP 最大难点)
- 初始边界值设置错误,结果整体偏差
- 循环起止下标写错,漏算 / 重复计算
- 盲目套用模板,不分析题目状态关系
八、今日总结
- DP 核心:存子问题答案,避免重复计算
- 解题四步走:定义状态 → 初始化 → 状态转移 → 输出结果
- 斐波那契、爬楼梯是一维 DP 入门标杆,转移方程形式一致
- 可通过滚动变量优化空间,面试加分技巧