很多人刚学动态规划的时候都会有一种感觉:
看答案好像能懂,让自己写就完全不会。
我一开始也是这样。后来慢慢发现,问题不在于代码,而在于------你根本不知道"该怎么想"。
这篇文章不打算讲一堆抽象定义,而是带你从最原始的思考出发,一步一步走到动态规划。
一、先别急着学 DP,先搞清楚你为什么需要它
来看一个很简单的问题:
有 n 阶楼梯,每次可以走 1 步或 2 步,一共有多少种走法?
很多人第一反应是:试试递归。
int f(int n){
if(n==1) return 1;
if(n==2) return 2;
return f(n-1) + f(n-2);
}
这段代码没问题,但如果 n=40,你会发现程序明显变慢。
为什么?
你可以画一棵递归树,会发现:
-
f(5) 会调用 f(4) 和 f(3)
-
f(4) 又会调用 f(3) 和 f(2)
-
f(3) 被算了很多次
也就是说:同一个问题被重复计算了
这就是动态规划解决的核心问题:
把已经算过的结果存下来,不要再算第二次
二、动态规划到底在做什么
其实就三件事:
-
定义一个数组(或状态)来存答案
-
找到状态之间的关系
-
按顺序把它们算出来
听起来还是有点抽象,我们直接继续刚才的题。
三、例题一:爬楼梯(真正的 DP 入门题)
我们重新思考这个问题:
第一步:换个角度想问题
到第 n 阶,最后一步一定是:
-
从 n-1 走 1 步上来
-
或从 n-2 走 2 步上来
所以:
到第 n 阶的方法数 = 到 n-1 的方法数 + 到 n-2 的方法数
这一步非常关键,这其实就是"状态转移"的来源,但你完全不需要记这个词。
第二步:定义状态
我们定义:
dp[i] = 到第 i 阶的方法数
第三步:写出递推关系
dp[i] = dp[i-1] + dp[i-2]
第四步:确定初始值
这个很多人会卡住,其实很简单:
-
dp[1] = 1(只有一种走法)
-
dp[2] = 2(1+1 或 2)
第五步:代码实现
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n;
cin >> 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];
}
cout << dp[n];
return 0;
}
写到这里,其实你已经掌握了 DP 的核心。
四、很多人卡住的点:到底什么时候该用 DP?
我给你一个很实用的判断方法:
如果你发现:
-
一个问题可以拆成小问题
-
小问题会重复出现
-
并且结果是"最值 / 方案数"
那大概率就是 DP。
五、例题二:最大子数组和(真正理解"状态"的题)
题目:
给你一个数组,找连续子数组的最大和
例如:
-2 1 -3 4 -1 2 1 -5 4
答案是:6(子数组 [4,-1,2,1])
关键问题:dp[i] 到底表示什么?
这是 DP 最难的一步。
我们定义:
dp[i] = 以 i 结尾的最大子数组和
为什么要这样定义?
因为"连续"这个条件,强迫我们必须以 i 结尾,否则不好转移。
状态转移怎么来?
对于 nums[i],有两种选择:
-
单独开一个新子数组
-
接在前面的子数组后面
所以:
dp[i] = max(nums[i], dp[i-1] + nums[i])
完整代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main(){
int n;
cin >> n;
vector<int> nums(n);
for(int i=0;i<n;i++) cin>>nums[i];
vector<int> dp(n);
dp[0] = nums[0];
int ans = dp[0];
for(int i=1;i<n;i++){
dp[i] = max(nums[i], dp[i-1] + nums[i]);
ans = max(ans, dp[i]);
}
cout << ans;
return 0;
}
六、你以为你会了,其实还差一步
很多人到这里会觉得:
"我懂了 DP"
但一做题还是不会。
问题在这里:
你只是在"记题型",没有掌握"拆问题的能力"
七、再给你一个稍微进阶一点的例子
例题三:01 背包(最经典 DP)
题目:
有 n 个物品,每个有重量 w[i] 和价值 v[i],背包容量是 W,问最大价值是多少
思考过程
对于第 i 个物品:
-
不选它 → 价值和前 i-1 一样
-
选它 → 剩余容量减少
定义状态
dp[i][j] = 前 i 个物品,在容量 j 下的最大价值
状态转移
dp[i][j] = dp[i-1][j] (不选)
dp[i][j] = dp[i-1][j-w[i]] + v[i] (选)
取最大值。
代码
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n, W;
cin >> n >> W;
vector<int> w(n+1), v(n+1);
for(int i=1;i<=n;i++) cin>>w[i]>>v[i];
vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
for(int i=1;i<=n;i++){
for(int j=0;j<=W;j++){
dp[i][j] = dp[i-1][j];
if(j >= w[i]){
dp[i][j] = max(dp[i][j], dp[i-1][j-w[i]] + v[i]);
}
}
}
cout << dp[n][W];
return 0;
}
八、总结一下真正有用的东西
动态规划不是套路,而是一个思考习惯:
-
把问题拆小
-
想清楚"最后一步"是什么
-
用一个数组记录结果
-
避免重复计算
如果你只能记住一句话,那就是:
动态规划,本质就是在问:这个问题的答案,能不能由更小的问题拼出来?