一、线性动态规划的定义
具有线性阶段划分的动态规划算法称为线性动态规划 (简称线性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级到第2级,再走1级到第3级
-
直接走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级、2级或3级台阶:递推公式变为f(n) = f(n-1) + f(n-2) + f(n-3)
-
如果起始位置不是第1级:需要修改初始条件
-
如果M很大(如10^9):需要使用矩阵快速幂方法,将时间复杂度降到O(log n)
这个超级楼梯问题是理解递归和动态规划的经典例题,掌握了这个问题的解法,对于理解更复杂的动态规划问题有很大帮助。