跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

动态规划入门:线性 DP 四道经典题解析

线性动态规划是算法竞赛中的基础题型,状态转移通常依赖前序状态。选取台阶问题、最大子段和、传球游戏、乌龟棋四个典型例题,分别演示一维与多维 DP 的状态定义技巧、转移方程推导逻辑以及边界条件处理方法。重点涵盖取模运算、环形数组处理、状态压缩优化等实战细节,帮助读者建立系统的线性 DP 解题思路。

静心发布于 2026/3/16更新于 2026/10/1584 浏览
动态规划入门:线性 DP 四道经典题解析

线性 DP 概述

线性动态规划是算法竞赛和面试中的基础题型。它的核心在于状态转移只依赖于前一个或前几个状态,通常可以用一维或二维数组来存储中间结果。我们在入门阶段遇到的《下楼梯》和《数字三角形》本质上都是线性 DP 的变体。

下面通过四道经典题目,梳理线性 DP 的状态定义、转移方程推导以及边界处理的实战技巧。

台阶问题

题目描述

给定 n 个台阶,每次可以走 1 到 k 步,求走到第 n 个台阶的方案数。结果对 100003 取模。

思路分析

这道题是典型的一维 DP。我们定义 dp[i] 为走到第 i 个台阶的方案总数。对于第 i 个台阶,可以从 i-1, i-2, ..., i-k 处跳过来。因此,状态转移方程就是累加这些前驱状态的值。

这里有两个细节需要注意:

  1. 取模运算:由于方案数增长很快,每一步加法后都要取模,防止溢出。
  2. 边界检查:当 i < k 时,i-j 可能小于 0,循环中需要判断 i - j >= 0。

关于初始化,很多初学者会尝试将前 k 个位置单独赋值,其实更简洁的做法是将 dp[0] 设为 1。这代表站在起点(第 0 阶)有一种方案(不动),这样从 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;
    return 0;
}

最大子段和

题目描述

给定一个整数序列,找出连续子序列的最大和。

思路分析

定义 f[i] 为以第 i 个元素结尾的所有子数组中,最大的和是多少。这个定义非常关键,它锁定了'结尾'位置,从而避免了重复计算。

对于第 i 个元素,有两种选择:

  1. 接在前面的子段后面:f[i-1] + a[i]
  2. 重新开始一个新的子段:a[i]

如果前面的子段和是负数,加上它只会让当前值变小,所以直接选 a[i] 更好。状态转移方程即为:f[i] = max(a[i], f[i-1] + a[i])。

初始化时,设 f[0] = 0 即可满足逻辑。最终答案不是 f[n],而是整个 f 数组中的最大值。

为了节省空间,我们可以边读入边计算,不需要额外存储原数组 a。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 2e5 + 10;
int n;
int f[N];

int main() {
    cin >> n;
    
    // 初始化 f[0] 为 0
    f[0] = 0;
    
    int ret = -0x3f3f3f3f; // 初始化为极小值
    
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        f[i] = max(x, x + f[i - 1]);
        ret = max(ret, f[i]);
    }
    
    cout << ret << endl;
    return 0;
}

传球游戏

题目描述

n 个同学围成一圈,球从 1 号同学开始传,传 m 次后回到 1 号同学的方案数。

思路分析

这是一个环形结构的问题,用二维 DP 比较直观。定义 dp[i][j] 表示传了 i 次球后,球在 j 号同学手里的方案数。

因为是在圆圈上,每个同学只能从左边的邻居或右边的邻居接球。对于编号 j,来源可能是 j-1 和 j+1。但要注意首尾相接的特殊情况:

  • 1 号同学的左边是 n 号,右边是 2 号。
  • n 号同学的左边是 n-1 号,右边是 1 号。

初始化时,dp[0][1] = 1,表示 0 次传球时球在 1 号手里。其他 dp[0][j] 均为 0。

填表顺序是先枚举传球次数 i,再枚举同学编号 j。注意处理边界时的索引映射。

#include <iostream>
using namespace std;

const int N = 40;
int n, m;
int dp[N][N]; // dp[次数][同学编号]

int main() {
    cin >> n >> m;
    
    // 初始化:0 次传球在 1 号手中
    dp[0][1] = 1;
    
    // 枚举传球次数
    for (int i = 1; i <= m; i++) {
        // 处理 1 号同学(特殊边界)
        dp[i][1] = dp[i - 1][2] + dp[i - 1][n];
        
        // 处理中间同学
        for (int j = 2; j < n; j++) {
            dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
        }
        
        // 处理 n 号同学(特殊边界)
        dp[i][n] = dp[i - 1][1] + dp[i - 1][n - 1];
    }
    
    cout << dp[m][1] << endl;
    return 0;
}

乌龟棋

题目描述

棋盘是一维的,有若干张卡片(1~4 步)。从起点出发,用完所有卡片到达终点,求路径上的最大分数。

思路分析

这道题看起来像背包问题,但状态维度较高。我们需要知道每种卡片的剩余数量来决定下一步怎么走。不过,既然卡片总数固定,我们可以反过来思考:用了多少张 1 步卡、2 步卡... 就能确定当前所在的位置。

定义 dp[i][j][k][z] 表示使用了 i 张 1 步卡、j 张 2 步卡、k 张 3 步卡、z 张 4 步卡时的最大得分。当前位置可以通过公式计算:pos = 1 + i1 + j2 + k3 + z4。

状态转移时,考虑最后一步是从哪里来的。比如当前状态是 (i,j,k,z),最后一步如果是用了 1 步卡,那么前一个状态就是 (i-1,j,k,z)。我们需要遍历四种可能的来源,取最大值。

注意边界条件:只有当对应卡片数量大于 0 时,才能进行相应的状态转移。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 360;
const int M = 50;

int n, m;
int a[N];       // 棋盘分数
int cnt[5];     // 卡片数量统计
int dp[M][M][M][M]; // 四维 DP 数组

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    
    while (m--) {
        int x;
        cin >> x;
        cnt[x]++;
    }
    
    // 初始化:起点分数
    dp[0][0][0][0] = a[1];
    
    // 四层循环枚举卡片使用数量
    for (int i = 0; i <= cnt[1]; i++) {
        for (int j = 0; j <= cnt[2]; j++) {
            for (int k = 0; k <= cnt[3]; k++) {
                for (int z = 0; z <= cnt[4]; z++) {
                    // 计算当前位置
                    int pos = 1 + i + 2 * j + 3 * k + 4 * z;
                    int &t = dp[i][j][k][z];
                    
                    // 尝试从不同方向转移
                    if (i > 0) t = max(t, dp[i - 1][j][k][z] + a[pos]);
                    if (j > 0) t = max(t, dp[i][j - 1][k][z] + a[pos]);
                    if (k > 0) t = max(t, dp[i][j][k - 1][z] + a[pos]);
                    if (z > 0) t = max(t, dp[i][j][k][z - 1] + a[pos]);
                }
            }
        }
    }
    
    cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]] << endl;
    return 0;
}

以上四道题涵盖了线性 DP 中一维、二维及多维状态压缩的典型场景。掌握状态定义的切入点,往往比死记硬背模板更重要。

目录

  1. 线性 DP 概述
  2. 台阶问题
  3. 最大子段和
  4. 传球游戏
  5. 乌龟棋

更多推荐文章

查看全部
  • 基于星辰 RPA 实现小红书自动发文机器人
  • 基于 BRPC+Protobuf+Etcd 的分布式文件存储 RPC 设计
  • 低代码平台构建服装 ERP 系统实战:从设计到自动化流程
  • VS Code 远程连接服务器后 GitHub Copilot 无法使用问题的解决方案
  • 大模型实战:深入解析 LLaMA 核心算子 RMSNorm 开发
  • DevEco Studio 云工程部署与通用模板实战
  • NPU、RKNN、TPU、GPU、FPGA AI 加速芯片选型指南
  • Gemini 全能 QQ 机器人部署手册
  • Stable Diffusion 16 款常用插件测评与推荐
  • 不是机器人,是数字员工:OpenClaw 核心逻辑全景解析
  • C++ 图论实战:深入理解三种经典最短路径算法
  • Apache IoTDB 数据保留时间管理:TTL 设置与操作实践
  • 用 OpenClaw 做科研文献、数据与排版自动化
  • OpenClaw 的安装、启动和联网配置
  • Android Studio 安装与 SDK、JDK、Gradle 配置详解
  • 医疗 AI 场景下算法编程深度解析(一)
  • mdev 与 udev:嵌入式及桌面 Linux 设备管理对比
  • OpenSpec 完全使用指南:约束驱动 AI 编码
  • Milvus 实战:Attu 可视化安装与 Python 整合指南
  • CentOS 部署 Teemii 搭建私人漫画库

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online