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

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

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

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

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


一、先别急着学 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. 避免重复计算

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

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

相关推荐
炘爚2 小时前
深入解析printf缓冲区与fork进程复制机制
linux·运维·算法
迈巴赫车主3 小时前
蓝桥杯19724食堂
java·数据结构·算法·职场和发展·蓝桥杯
6Hzlia3 小时前
【Hot 100 刷题计划】 LeetCode 78. 子集 | C++ 回溯算法题解
c++·算法·leetcode
Kethy__3 小时前
计算机中级-数据库系统工程师-数据结构-查找算法
数据结构·算法·软考·查找算法·计算机中级
所以遗憾是什么呢?3 小时前
【题解】Codeforces Round 1081 (Div. 2)
数据结构·c++·算法·acm·icpc·ccpc·xcpc
白藏y4 小时前
【C++】muduo接口补充
开发语言·c++·muduo
xiaoye-duck4 小时前
《算法题讲解指南:递归,搜索与回溯算法--综合练习》--14.找出所有子集的异或总和再求和,15.全排列Ⅱ,16.电话号码的字母组合,17.括号生成
c++·算法·深度优先·回溯
OOJO4 小时前
c++---vector介绍
c语言·开发语言·数据结构·c++·算法·vim·visual studio
茉莉玫瑰花茶4 小时前
数据结构 - 并查集
数据结构