线性动态规划基础详解:4 道经典例题与优化技巧
线性动态规划核心思想与解题四步走,通过台阶问题、最大子段和、传球游戏、乌龟棋四道例题演示状态定义、转移方程及初始化。涵盖前缀和、滚动数组等优化技巧,帮助读者掌握一维至多维线性 DP 模型,适合算法基础巩固与进阶。

线性动态规划核心思想与解题四步走,通过台阶问题、最大子段和、传球游戏、乌龟棋四道例题演示状态定义、转移方程及初始化。涵盖前缀和、滚动数组等优化技巧,帮助读者掌握一维至多维线性 DP 模型,适合算法基础巩固与进阶。

动态规划(DP)是算法学习中的核心难点,也是竞赛和面试的必考点。线性 DP 作为最基础的分支,状态转移具有明显的线性关系,逻辑清晰,非常适合作为入门切入点。
本文带大家吃透基础线性 DP,从核心思想拆解到 4 道经典例题,再到空间优化技巧。
线性 DP 是动态规划的一种特殊形式,其核心特点是状态之间的转移关系呈线性结构。简单来说,DP 状态可以用一维或二维数组表示,推导顺序是线性的——要么从左到右、要么从上到下。
例如一维线性 DP 中,dp[i] 的取值只依赖 dp[i-1]、dp[i-2] 等前面的状态;二维线性 DP 中,dp[i][j] 也只依赖相邻的线性状态。
解决任何线性 DP 问题,都可以遵循这四个核心步骤:
相比于其他 DP 分支,线性 DP 的优势在于逻辑清晰、代码简洁、实用性强,很多实际问题(如路径计数、最值问题)都可以抽象为线性 DP 模型。
有 N 级台阶,一开始在底部,每次可以向上迈 1~K 级台阶,问到达第 N 级台阶有多少种不同方式?结果对 100003 取模。 输入:两个正整数 N, K(1≤N≤1e5,1≤K≤100) 输出:到达第 N 级台阶的不同方式数(mod 100003)
dp[i] 表示'到达第 i 级台阶的不同方式数'。i-1 到 i-K 级迈过来。dp[i] = (dp[i-1] + dp[i-2] + ... + dp[i-K]) % MODdp[0] = 1。dp[1] 到 dp[N]。#include <iostream>
using namespace std;
const int N = 1e5 + 10, MOD = 1e5 + 3;
int n, k;
int dp[N];
int main() {
cin >> n >> k;
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k && i - j >= 0; j++) {
dp[i] = (dp[i] + dp[i - j]) % MOD;
}
}
cout << dp[n] << endl;
return 0;
}
基础版的时间复杂度是 O(N*K)。当 N=1e5、K=100 时,总运算量是 1e7,可以通过。但如果 K 更大,会超时。
观察状态转移方程,dp[i] 是前 K 个状态的和。我们可以用一个前缀和数组 sum[i] 表示 dp[0]~dp[i] 的和,将求和转化为 O(1)。
#include <iostream>
using namespace std;
const int N = 1e5 + 10, MOD = 1e5 + 3;
int n, k;
int dp[N], sum[N];
int main() {
cin >> n >> k;
dp[0] = 1;
sum[0] = dp[0];
for (int i = 1; i <= n; i++) {
if (i - k > 0) {
dp[i] = (sum[i-1] - sum[i - k - 1] + MOD) % MOD;
} else {
dp[i] = sum[i-1] % MOD;
}
sum[i] = (sum[i-1] + dp[i]) % MOD;
}
cout << dp[n] << endl;
return 0;
}
给出一个长度为 n 的序列 a,选出其中连续且非空的一段,使得这段的和最大。 输入:第一行是序列长度 n;第二行是 n 个整数(-1e4 ≤ a[i] ≤ 1e4,1≤n≤2e5) 输出:最大子段和
dp[i] 表示'以第 i 个元素为结尾的所有连续子段的最大和'。dp[i] = max(a[i], dp[i-1] + a[i])dp[1] = a[1]。dp 数组中的最大值。#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10;
int n;
int a[N], dp[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dp[1] = a[1];
int max_sum = dp[1];
for (int i = 2; i <= n; i++) {
dp[i] = max(a[i], dp[i-1] + a[i]);
max_sum = max(max_sum, dp[i]);
}
cout << max_sum << endl;
return 0;
}
观察发现,dp[i] 只依赖 dp[i-1],因此不需要用数组存储整个 dp 序列,只用一个变量记录前一个状态即可。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10;
int n;
int a[N];
int main() {
cin >> n;
int prev = 0, max_sum = -1e9;
for (int i = 1; i <= n; i++) {
cin >> a[i];
prev = max(a[i], prev + a[i]);
max_sum = max(max_sum, prev);
}
cout << max_sum << endl;
return 0;
}
n 个同学站成一个圆圈,小蛮(1 号同学)手里拿着球,传了 m 次后,球回到小蛮手里的不同传球方法有多少种? 输入:两个整数 n, m(3≤n≤30,1≤m≤30) 输出:符合条件的传球方法数
dp[i][j] 表示'传球 i 次后,球在第 j 号同学手里的方法数'。dp[i][j] = dp[i-1][j-1] + dp[i-1][j+1]dp[i][1] = dp[i-1][n] + dp[i-1][2]dp[i][n] = dp[i-1][n-1] + dp[i-1][1]dp[0][1] = 1。#include <iostream>
using namespace std;
const int N = 50;
int n, m;
int dp[N][N];
int main() {
cin >> n >> m;
dp[0][1] = 1;
for (int i = 1; i <= m; i++) {
dp[i][1] = dp[i-1][n] + dp[i-1][2];
for (int j = 2; j < n; j++) {
dp[i][j] = dp[i-1][j-1] + dp[i-1][j+1];
}
dp[i][n] = dp[i-1][n-1] + dp[i-1][1];
}
cout << dp[m][1] << endl;
return 0;
}
乌龟棋的棋盘有 N 个格子,每个格子有分数。玩家有 M 张爬行卡片,分为 4 种类型(1、2、3、4),每张卡片只能用一次。乌龟从第 1 格出发,用完全部卡片到达第 N 格,求最大得分。 输入:
dp[a][b][c][d] 表示'使用 a 张 1 型、b 张 2 型、c 张 3 型、d 张 4 型卡片时的最大得分',对应位置 pos = 1 + a + 2b + 3c + 4d。dp[a][b][c][d] = max(四种前序状态) + score[pos]dp[0][0][0][0] = score[1]。#include <iostream>
#include <algorithm>
using namespace std;
const int N = 360, M = 50;
int n, m;
int score[N];
int cnt[5];
int dp[M][M][M][M];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> score[i];
}
for (int i = 0; i < m; i++) {
int t;
cin >> t;
cnt[t]++;
}
dp[0][0][0][0] = score[1];
for (int a = 0; a <= cnt[1]; a++) {
for (int b = 0; b <= cnt[2]; b++) {
for (int c = 0; c <= cnt[3]; c++) {
for (int d = 0; d <= cnt[4]; d++) {
int pos = 1 + a + 2*b + 3*c + 4*d;
int& curr = dp[a][b][c][d];
(a > ) curr = (curr, dp[a][b][c][d] + score[pos]);
(b > ) curr = (curr, dp[a][b][c][d] + score[pos]);
(c > ) curr = (curr, dp[a][b][c][d] + score[pos]);
(d > ) curr = (curr, dp[a][b][c][d] + score[pos]);
}
}
}
}
cout << dp[cnt[]][cnt[]][cnt[]][cnt[]] << endl;
;
}
四种卡片的最大数量都是 40,四维循环总次数是 40^4 = 2,560,000,完全可以通过。
线性 DP 是后续学习背包 DP、区间 DP 等复杂 DP 的基础。打好这个基础,后面的学习会事半功倍。祝大家都能攻克 DP 这个难点,在算法路上越走越远!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online