从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?

在做 DP 题时,最常遇到的困境是:"看题解秒懂,自己写就懵,尤其是那个核心的转移方程,到底是怎么想出来的?"

今天,从 LeetCode 上一道最经典的入门题------70. 爬楼梯 提炼出一套普适的动态规划解题框架。

一、 引言:从爬楼梯说起

题目描述:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

这题看似是排列组合,实际上是一道标准的动态规划题。

二、 动态规划的核心:状态与转移

动态规划的本质是通过定义状态和状态之间的递推关系,将大问题拆解为小问题,并利用历史结果避免重复计算。

要写出正确的代码,必须回答两个问题:

  1. 状态定义(State) :dp[i] 代表什么?

  2. 状态转移方程(Transition) :dp[i] 和 dp[i-1]、dp[i-2] 等有什么关系?

1. 状态定义

对于爬楼梯,最自然的定义是:

dp[i] 表示:到达第 i 阶楼梯的方法总数。

2. 推导状态转移方程(核心难点)

如何从已知状态推导出 dp[i]?

关键思考法:最后一步法(逆向思维)

想象你已经站在了第 i 阶楼梯上。你是怎么上来的?题目规定你只能爬 1 阶或 2 阶。

  • 情况 A:你是从第 i-1 阶爬 1 阶上来的。

  • 情况 B:你是从第 i-2 阶爬 2 阶上来的。

因为这两条路径是互斥的(要么最后一步跨1阶,要么跨2阶),所以到达第 i 阶的方法总数 = 到达第 i-1 阶的方法数 + 到达第 i-2 阶的方法数。

由此得出转移方程:

复制代码
dp[i]=dp[i−1]+dp[i−2]

注意:这里用的是加法,而不是 max,因为题目求的是"总数",不是"最大步数"。

3. 边界条件与初始化

  • dp[1] = 1:到第1阶只有1种方法(爬1阶)。

  • dp[2] = 2:到第2阶有2种方法(1+1 或 2)。

  • dp[0]:理论上不需要用到,但有时为了方便计算可以设为1。

4. 正确的代码实现(C++)(其实就是斐波那契数列)

cpp 复制代码
class Solution {
public:
    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];
    }
};

进阶优化:由于 dp[i] 只依赖前两个状态,可以把空间复杂度从 O(N) 优化到 O(1),用两个变量滚动更新即可。

三、 普适化:如何识别不同类型的动态转移方程?

爬楼梯是"线性DP"的入门,但在实际算法题中,DP 题型千变万化。下面是 DP 常见的转移方程类型的五大模式。

模式一:线性递推(爬楼梯型)

  • 特征:状态按照线性顺序(如数组下标、楼梯阶数)依次递推。

  • 方程形式 :dp[i] = dp[i-1] + dp[i-2] 或带有权重的 dp[i] = min(dp[i-1], dp[i-2]) + cost[i]。

模式二:区间 DP(合并型)

  • 特征 :状态通常定义在区间 [i, j] 上,大区间的解由小区间合并而来。

  • 方程形式 :dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost),其中 k 是分割点。

  • 思考技巧 :枚举分割点。把大问题切分成两个子问题,再加合起来的代价。

模式三:背包 DP(选择型)

  • 特征:给定一个容量限制(背包),在若干物品中选择,求最大价值或方案数。

  • 方程形式:

    • 0-1背包(每个物品只能选一次):dp[j] = max(dp[j], dp[j-w[i]] + v[i])(注意逆序遍历 j)。

    • 完全背包(物品无限):dp[j] = max(dp[j], dp[j-w[i]] + v[i])(正序遍历 j)。

  • 思考技巧 :"选"与"不选" 。当前容量 j,对于物品 i,要么不选(保持 dp[j]),要么选(腾出空间 w[i] 放入)。

模式四:双序列 DP(匹配型)

  • 特征:涉及两个字符串或两个数组,通常需要二维 DP 表。

  • 方程形式 :dp[i][j] 表示 A 的前 i 个和 B 的前 j 个的某种关系。

    • 若 A[i] == B[j],则 dp[i][j] = dp[i-1][j-1] + 1(或直接继承)。

    • 若不等,则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。

  • 思考技巧 :画二维表格。考虑当前字符匹配时,左上角怎么转移;不匹配时,左边或上边怎么转移。

模式五:树形 DP

  • 特征:数据结构是树,状态在递归回溯时从子节点传递给父节点。

  • 方程形式 :通常结合 DFS,dp[node][0/1] 表示节点选或不选的状态。

  • 思考技巧 :后序遍历。先算清楚左右子树的状态,再决定当前节点的状态。

四、 总结:动态规划解题四步走

当你面对一道新题,怀疑它是 DP 时,请按以下步骤操作:

  1. 定义状态 :明确 dp 数组的含义(一维还是二维?下标代表什么?)。

  2. 找转移方程 :思考**"最后一步"** 是怎么来的,或者**"当前状态"** 可以由哪些**"前驱状态"**推导出来。这是最关键的一步,通常需要分类讨论(如选/不选、匹配/不匹配)。

  3. 确定初始值 :dp[0]、dp[1] 等边界是什么?注意数组越界问题(如爬楼梯中 n=1 的情况)。

  4. 确定遍历顺序 :是从前往后,还是从后往前?是外层循环物品还是内层循环容量?确保计算 dp[i] 时,依赖的状态已经计算完毕。

相关推荐
2601_960356383 小时前
2027校招采购岗解析:数学基础如何迁移到需求预测、成本与供应商分析
人工智能·算法
h_a_o777oah3 小时前
【树论】树链剖分:将树形结构转化为线性结构以处理树上路径问题(洛谷 P3384 附C++代码模板)
数据结构·算法·线段树·acm·树链剖分·树论·重链剖分
HEJOO93 小时前
指针的算术运算详解
java·数据结构·算法
Coder-magician3 小时前
《代码随想录》刷题打卡day47:并查集-part01
数据结构·算法·图论
垆边人似月.3 小时前
VLAN 资源池(100~200 分档)
数据结构·算法
潼心1412o4 小时前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法
星光开发者5 小时前
基于决策树随机森林算法的小说推荐系统的设计与实现-计算机毕设【课程设计】78485
vue.js·算法·微信小程序·django·php·课程设计·express
-森屿安年-5 小时前
按身高排序
c++·算法
程序员清风5 小时前
算法与数据结构入门:复杂度、数组与链表
数据结构·算法·链表