动态规划专题:00:线性动态规划:爬楼梯问题实例

一、线性动态规划的定义

具有线性阶段划分的动态规划算法称为线性动态规划 (简称线性DP )。若状态包含多个维度,则每个维度都是线性划分的阶段,也属于线性DP

1. 核心概念解读

  • 动态规划(DP):是一种解决复杂问题的高效算法思想,其核心是将原问题分解为相对简单的子问题,并通过保存子问题的解(即"状态")来避免重复计算,最终获得原问题的解。

  • "线性"的含义 :这里的"线性"特指问题求解过程在阶段划分上是线性的、顺序的。就像一条线,从起点(初始状态)开始,按照明确的、不可跳跃的顺序(阶段1 -> 阶段2 -> ... -> 阶段n)一步步推进到终点(最终状态)。整个决策过程形成一个清晰的"链条"。

2. 示意图解构

  • 状态 :图中 状态0状态1, ..., 状态n,代表了在解决问题过程中,各个"时间点"或"步骤点"的情况记录。状态0初始状态状态n目标状态

  • 决策 :图中 每一个阶段都有每一个阶段的决策,决策1决策2, ..., 决策n,代表从前一个状态向后一个状态转移时,我们所做出的选择。每个决策都会产生一定的"代价"或"收益",并导致状态发生变化。

  • 阶段 :图中底部的 阶段1阶段2, ..., 阶段n, 正是线性划分的体现。每个阶段对应一次"决策+状态转移"的过程。阶段与阶段之间首尾相接,顺序固定,构成了线性的求解流程。

  • 流程总结 :整个算法从 状态0(初始条件)出发,在 阶段1根据 状态0做出 决策1,从而更新到 状态1;然后在 阶段2, 再基于 状态1做出 决策2, 更新到 状态2, 如此线性推进,直至在 阶段n做出 决策n后,到达最终的 状态n,问题得以解决。

3. 对"多维度状态"的说明

定义的第二句话是理解的难点和关键。它指出,即使状态包含多个维度(例如用坐标 (i, j)表示位置),只要每个维度的变化是按线性顺序进行遍历或递推的,就仍然属于线性DP。

  • 举例 :在经典的"数字三角形"问题中,状态是二维的 (i, j),表示第i行第j列。我们通常的求解顺序是:从第一行开始,一行一行(线性遍历i)地向下计算,在每一行内,可能从左到右或按特定顺序(线性遍历j)计算每个位置的最优值。虽然状态是二维的,但两个维度(i和j)的变化过程本身都是线性、有序的阶段划分,因此它依然是线性DP。

总结

线性动态规划是一种将问题建模为"多阶段决策过程"的算法模型,其核心特征是阶段的线性、顺序依赖性。 ​ 无论状态是单一变量还是多维变量,只要决策推进的"时间线"或"逻辑顺序"是线性的,就可以用线性DP的思路来解决。图片中的示意图正是这一单向、链式决策过程的经典可视化表示。常见的最长上升子序列、背包问题、最短路径问题等,都是线性DP的典型应用。

实例讲解1: 超级楼梯

题目描述(HDU2041):一个楼梯共有M级台阶,刚开始时我们站在第1级台阶上,若每次只可以走上一级或二级台阶,则要走上第M级台阶共有多少种走法?

输入:第1行包含一个整数N,表示测试用例的个数。然后是N行数据,每行都包含一个整数M(1≤M≤40),表示楼梯的级数。

输出:对每个测试实例都输出不同走法的数量。

输入样例 输出样例

2 1

2 2

3

二、题意讲解

1. 问题理解

这是一个经典的动态规划问题,类似于斐波那契数列问题。我们需要计算从第1级 台阶走到第M级台阶的不同走法数量,约束条件是:

  • 每次只能向上走1级2级台阶

  • 起始位置是第1级台阶

  • 目标位置是第M级台阶

2. 关键点分析

  • 当M=1时:已经在第1级,不需要走,有1种走法(不动)

  • 当M=2时:从第1级到第2级,只能走1步(1级),有1种走法

  • 当M=3时:从第1级到第3级,有两种方式:

    1. 先走1级到第2级,再走1级到第3级

    2. 直接走2级到第3级

      因此有2种走法

三、解题思路讲解

1. 数学递推关系

设f(n)表示从第1级走到第n级的走法数,则有以下递推关系:

  • f(1) = 1

  • f(2) = 1

  • f(n) = f(n-1) + f(n-2),当n ≥ 3时

解释:要到达第n级台阶,可以从第n-1级走1步上来,或者从第n-2级走2步上来。

2. 递归方法

直接使用递推公式进行递归计算,但这种方法存在大量重复计算,时间复杂度为O(2^n),效率较低。

3. 动态规划方法

使用数组存储中间结果,避免重复计算,时间复杂度为O(n),效率高。

四、C++完整代码实现

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// ==================== 方法一:递归实现(带记忆化) ====================
// 递归函数:计算从第1级走到第n级的走法数
int recursiveSolution(int n, vector<int>& memo) {
    // 基本情况
    if (n == 1) return 1;  // 已经在第1级,只有1种走法
    if (n == 2) return 1;  // 从第1级到第2级,只能走1级
    
    // 如果已经计算过,直接返回结果
    if (memo[n] != -1) return memo[n];
    
    // 递归计算:f(n) = f(n-1) + f(n-2)
    memo[n] = recursiveSolution(n-1, memo) + recursiveSolution(n-2, memo);
    return memo[n];
}

// ==================== 方法二:动态规划实现 ====================
// 动态规划函数:计算从第1级走到第n级的走法数
int dpSolution(int n) {
    // 如果n=1或n=2,直接返回
    if (n == 1) return 1;
    if (n == 2) return 1;
    
    // 创建dp数组,dp[i]表示从第1级走到第i级的走法数
    vector<int> dp(n + 1, 0);
    
    // 初始化基本情况
    dp[1] = 1;  // 从第1级到第1级,有1种走法(不动)
    dp[2] = 1;  // 从第1级到第2级,只能走1级,有1种走法
    
    // 递推计算
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    
    return dp[n];
}

// ==================== 方法三:空间优化的动态规划 ====================
// 由于只需要前两个状态,可以优化空间复杂度到O(1)
int dpOptimizedSolution(int n) {
    if (n == 1) return 1;
    if (n == 2) return 1;
    
    int prev2 = 1;  // f(n-2)
    int prev1 = 1;  // f(n-1)
    int current;     // f(n)
    
    for (int i = 3; i <= n; i++) {
        current = prev1 + prev2;
        prev2 = prev1;
        prev1 = current;
    }
    
    return current;
}

int main() {
    int N;  // 测试用例个数
    cin >> N;
    
    // 存储所有测试用例
    vector<int> testCases(N);
    
    // 读取所有测试用例
    for (int i = 0; i < N; i++) {
        cin >> testCases[i];
    }
    
    cout << "========== 递归方法(带记忆化)结果 ==========" << endl;
    for (int i = 0; i < N; i++) {
        int M = testCases[i];
        // 创建记忆化数组,初始值为-1表示未计算
        vector<int> memo(M + 1, -1);
        cout << "M = " << M << ": " << recursiveSolution(M, memo) << endl;
    }
    
    cout << "\n========== 动态规划方法结果 ==========" << endl;
    for (int i = 0; i < N; i++) {
        int M = testCases[i];
        cout << "M = " << M << ": " << dpSolution(M) << endl;
    }
    
    cout << "\n========== 空间优化的动态规划结果 ==========" << endl;
    for (int i = 0; i < N; i++) {
        int M = testCases[i];
        cout << "M = " << M << ": " << dpOptimizedSolution(M) << endl;
    }
    
    // 运行题目示例
    cout << "\n========== 题目示例运行结果 ==========" << endl;
    cout << "输入:" << endl;
    cout << "2" << endl;
    cout << "2" << endl;
    cout << "3" << endl;
    
    cout << "\n输出:" << endl;
    vector<int> memo1(3, -1);
    cout << dpSolution(2) << endl;  // 输出1
    cout << dpSolution(3) << endl;  // 输出2
    
    return 0;
}

五、代码详细讲解

1. 递归方法(带记忆化)

复制代码
复制代码
复制代码
int recursiveSolution(int n, vector<int>& memo)
  • 参数

    • n:目标台阶级数

    • memo:记忆化数组,存储已计算的结果,避免重复计算

  • 实现原理

    • 基础情况:n=1或n=2时直接返回结果

    • 递归情况:f(n) = f(n-1) + f(n-2)

    • 记忆化:将计算结果存入memo数组,下次直接使用

  • 时间复杂度:O(n),因为有记忆化,每个f(n)只计算一次

  • 空间复杂度:O(n),用于存储memo数组和递归调用栈

2. 动态规划方法

复制代码
复制代码
复制代码
int dpSolution(int n)
  • 实现原理

    • 创建dp数组,dp[i]表示从第1级走到第i级的走法数

    • 初始化:dp[1]=1, dp[2]=1

    • 状态转移:dp[i] = dp[i-1] + dp[i-2]

    • 最终返回dp[n]

  • 时间复杂度:O(n),需要遍历1到n

  • 空间复杂度:O(n),用于存储dp数组

3. 空间优化的动态规划

复制代码
复制代码
复制代码
int dpOptimizedSolution(int n)
  • 实现原理

    • 由于递推公式只依赖前两个状态,不需要保存整个dp数组

    • 使用三个变量:prev2(f(n-2)), prev1(f(n-1)), current(f(n))

    • 每次迭代更新这三个变量

  • 时间复杂度:O(n)

  • 空间复杂度:O(1),只使用固定数量的变量

4. 主函数

复制代码
复制代码
复制代码
int main()
  • 读取测试用例个数N

  • 读取N个测试用例(台阶级数M)

  • 分别用三种方法计算并输出结果

  • 最后运行并输出题目示例

六、运行示例

对于输入样例:

复制代码
复制代码
复制代码
2
2
3

程序输出:

复制代码
复制代码
复制代码
========== 递归方法(带记忆化)结果 ==========
M = 2: 1
M = 3: 2

========== 动态规划方法结果 ==========
M = 2: 1
M = 3: 2

========== 空间优化的动态规划结果 ==========
M = 2: 1
M = 3: 2

========== 题目示例运行结果 ==========
输入:
2
2
3

输出:
1
2

七、复杂度对比

方法 时间复杂度 空间复杂度 优点 缺点
朴素递归 O(2^n) O(n) 实现简单 大量重复计算,效率极低
记忆化递归 O(n) O(n) 避免重复计算 需要额外存储空间
动态规划 O(n) O(n) 逻辑清晰,无递归开销 需要O(n)数组空间
优化动态规划 O(n) O(1) 空间最优 逻辑稍复杂

八、扩展思考

  1. 如果每次可以走1级、2级或3级台阶:递推公式变为f(n) = f(n-1) + f(n-2) + f(n-3)

  2. 如果起始位置不是第1级:需要修改初始条件

  3. 如果M很大(如10^9):需要使用矩阵快速幂方法,将时间复杂度降到O(log n)

这个超级楼梯问题是理解递归和动态规划的经典例题,掌握了这个问题的解法,对于理解更复杂的动态规划问题有很大帮助。

相关推荐
草莓熊Lotso27 分钟前
MySQL 从入门到实战:视图特性 + 用户权限管理全解
linux·运维·服务器·数据库·c++·mysql
Q741_14729 分钟前
每日一题 力扣 1848. 到目标元素的最小距离 模拟 C++题解
c++·算法·leetcode·模拟
VkN2X2X4b1 小时前
算法性能的渐近与非渐近行为对比的技术9
算法
好家伙VCC2 小时前
**神经编码新视角:用Python实现生物启发的神经信号压缩与解码算法**在人工智能飞速发展的今天
java·人工智能·python·算法
W23035765739 小时前
经典算法:最长上升子序列(LIS)深度解析 C++ 实现
开发语言·c++·算法
.Ashy.9 小时前
2026.4.11 蓝桥杯软件类C/C++ G组山东省赛 小记
c语言·c++·蓝桥杯
minji...10 小时前
Linux 线程同步与互斥(三) 生产者消费者模型,基于阻塞队列的生产者消费者模型的代码实现
linux·运维·服务器·开发语言·网络·c++·算法
语戚10 小时前
力扣 968. 监控二叉树 —— 贪心 & 树形 DP 双解法递归 + 非递归全解(Java 实现)
java·算法·leetcode·贪心算法·动态规划·力扣·
skywalker_1110 小时前
力扣hot100-7(接雨水),8(无重复字符的最长子串)
算法·leetcode·职场和发展