【数据结构与算法】动态规划

很多人刚学动态规划的时候都会有一种感觉:

看答案好像能懂,让自己写就完全不会。

我一开始也是这样。后来慢慢发现,问题不在于代码,而在于------你根本不知道"该怎么想"

这篇文章不打算讲一堆抽象定义,而是带你从最原始的思考出发,一步一步走到动态规划。


一、先别急着学 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) 被算了很多次

也就是说:同一个问题被重复计算了

这就是动态规划解决的核心问题:

把已经算过的结果存下来,不要再算第二次


二、动态规划到底在做什么

其实就三件事:

  1. 定义一个数组(或状态)来存答案

  2. 找到状态之间的关系

  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],有两种选择:

  1. 单独开一个新子数组

  2. 接在前面的子数组后面

所以:

复制代码
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;
}

八、总结一下真正有用的东西

动态规划不是套路,而是一个思考习惯:

  1. 把问题拆小

  2. 想清楚"最后一步"是什么

  3. 用一个数组记录结果

  4. 避免重复计算

如果你只能记住一句话,那就是:

动态规划,本质就是在问:这个问题的答案,能不能由更小的问题拼出来?

相关推荐
Rabitebla44 分钟前
【C++】string 类:原理、踩坑与对象语义
linux·c语言·数据结构·c++·算法·github·学习方法
邪修king1 小时前
UE5 零基础入门第四弹:UMG UI 系统入门,从静态界面到逻辑联动
c++·ui·ue5
小雅痞2 小时前
[Java][Leetcode middle] 167. 两数之和 II - 输入有序数组
java·算法·leetcode
CN-Dust2 小时前
【C++】输入cin例题专题
java·c++·算法
数模竞赛Paid answer3 小时前
2025年MathorCup数学建模A题汽车风阻预测解题文档与程序
算法·数学建模·mathorcup
xin_nai3 小时前
LeetCode热题100(Java)(6)矩阵
java·leetcode·矩阵
Old Uncle Tom8 小时前
OpenClaw 记忆系统 -- 记忆预加载
java·数据结构·算法·agent
会编程的土豆9 小时前
洛谷题单入门1 顺序结构
数据结构·算法·golang
生信碱移9 小时前
PACells:这个方法可以鉴定疾病/预后相关的重要细胞亚群,作者提供的代码流程可以学习起来了,甚至兼容转录组与 ATAC 两种数据类型!
人工智能·学习·算法·机器学习·数据挖掘·数据分析·r语言
智者知已应修善业9 小时前
【51单片机中的打飞机设计】2023-8-25
c++·经验分享·笔记·算法·51单片机