引言
线性动态规划是算法入门中最基础且高频的一类问题。其核心特征在于状态转移仅依赖于前一个或前几个状态,状态间的关系呈线性分布,通常可用一维或二维数组存储。我们在入门阶段接触过的《下楼梯》和《数字三角形》本质上都是线性 DP 的变体。
本文将通过四道经典例题——台阶问题、最大子段和、传球游戏以及乌龟棋,系统梳理线性 DP 的状态定义、转移方程推导及边界处理技巧。
台阶问题
题目描述
给定 n 个台阶,每次可以走 1 到 k 步,求走到第 n 个台阶的方案数。结果需对 100003 取模。
解题思路
这道题是经典的下楼梯问题的加强版。我们按照动态规划的标准步骤来分析:
状态定义 dp[i] 表示走到第 i 个台阶的所有方案数。
状态转移 要到达第 i 个台阶,可以从 i-1, i-2, ..., i-k 这些位置跳过来。因此,第 i 个台阶的方案数等于这 k 个前驱状态之和。由于数据量较大,计算过程中需要时刻注意取模,防止溢出。
需要注意的是,当 i < k 时,i-j 可能小于 0,访问数组时需保证下标非负。
转移方程为:dp[i] = (dp[i] + dp[i - j]) % MOD,其中 j 从 1 遍历到 k。
初始化与填表 一种常见的误区是手动初始化前 k 个状态,但这比较繁琐。更优雅的方法是将 dp[0] 置为 1,代表起点本身有一种方案(不动),然后从 dp[1] 开始递推。填表顺序自然是从左向右。
代码实现
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, MOD = 1e5 + 3;
int n, k;
LL dp[N];
int main() {
cin >> n >> k;
// 初始化:起点视为一种方案
dp[0] = 1;
// 循环填表
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= k; j++) {
// 保证不越界访问
if(i - j < 0) break;
dp[i] = (dp[i] + dp[i - j]) % MOD;
}
}
cout << dp[n] << endl;
;
}


