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

线性 DP 经典四题详解:台阶、子段和、传球游戏与乌龟棋

线性动态规划是基础且高频的算法类型,核心在于状态仅依赖前序结果。通过台阶问题、最大子段和、传球游戏及乌龟棋四道经典例题,演示如何定义状态、推导转移方程及处理边界条件。涵盖一维到多维数组的应用,包含取模运算、空间优化技巧及环形结构处理,适合初学者系统掌握线性 DP 解题思路。

性能调优发布于 2026/3/22更新于 2026/5/2010 浏览
线性 DP 经典四题详解:台阶、子段和、传球游戏与乌龟棋

引言

线性动态规划是算法入门中最基础且高频的一类问题。其核心特征在于状态转移仅依赖于前一个或前几个状态,状态间的关系呈线性分布,通常可用一维或二维数组存储。我们在入门阶段接触过的《下楼梯》和《数字三角形》本质上都是线性 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;
    return 0;
}

最大子段和

题目描述

给定一个整数序列,找出一个连续子段,使得该子段的元素和最大。

解题思路

在处理子序列或子数组问题时,定义状态的关键在于结合题意确定'以谁结尾'。

状态定义 f[i] 表示以第 i 个位置为结尾的所有子数组中,最大的和是多少。

状态转移 考虑最后一步的情况:对于第 i 个格子,最大子段和有两种可能:

  1. 它自己单独构成一段,即 f[i] = a[i]。这通常发生在之前的累积和为负数时。
  2. 接在前面的最大子段后面,即 f[i] = f[i - 1] + a[i]。

综合这两种情况,取最大值即可:f[i] = max(a[i], f[i - 1] + a[i])。

初始化与结果 当 i=1 时,根据方程 f[1] = max(a[1], f[0] + a[1]),若将 f[0] 初始化为 0,则逻辑自洽。最终答案不是 f[n],而是整个 f 数组中的最大值。

代码实现

这里提供两个版本,标准版和优化空间版。

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

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

int main() {
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    
    // 初始化
    f[0] = 0;
    
    // 按序填表
    for(int i = 1; i <= n; i++) {
        f[i] = max(a[i], a[i] + f[i - 1]);
    }
    
    // 输出结果:f 数组中的最大值
    int ret = -0x3f3f3f3f;
    for(int i = 1; i <= n; i++) {
        ret = max(ret, f[i]);
    }
    cout << ret << endl;
    return 0;
}

优化版可以在输入的同时更新当前最大值,无需额外存储 a 数组:

// 优化版(无 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;
    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 问题。由于球可能从左邻也可能从右邻传来,我们需要记录当前的传球次数和持球人的编号。

状态定义 f[i][j] 表示球传递了 i 次之后,最终落在了 j 号同学手里的方案数。

状态转移 因为围成圆圈,需要分情况讨论边界:

  1. 1 号同学:只能从 2 号和 n 号同学处接球。dp[i][1] = dp[i-1][2] + dp[i-1][n]
  2. 中间同学 (2 到 n-1):从左右两边接球。dp[i][j] = dp[i-1][j-1] + dp[i-1][j+1]
  3. n 号同学:从 1 号和 n-1 号同学处接球。dp[i][n] = dp[i-1][1] + dp[i-1][n-1]

初始化 第一行表示传球次数为 0。此时球在 1 号同学手中,方案数为 1。所以 dp[0][1] = 1,其余为 0。

填表顺序 从上往下依次填每一行(枚举传球次数),每行内从左往右(枚举同学编号)。

代码实现

#include<iostream>
using namespace std;

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

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];
        
        // 填第 2 到 n-1 个同学
        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、2、3、4 步),每种卡片数量有限。求从起点走到终点能获得的最大分数。

解题思路

本题类似于飞行棋,但决策维度更多。我们需要知道每种卡片的剩余使用数量来确定当前位置。

状态定义 虽然棋盘是一维的,但决定当前位置的是四种卡片的消耗量。设 dp[i][j][k][z] 表示使用了 i 张 1 步卡、j 张 2 步卡、k 张 3 步卡、z 张 4 步卡时的最大分数。

当前位置 x 可以通过公式计算:x = 1 + i + 2j + 3k + 4*z。

状态转移 到达当前状态 (i, j, k, z) 的前一步可能是使用了某一张卡片。例如,如果 i > 0,则上一步可能是 (i-1, j, k, z)。我们需要从所有可能的上一步状态中取最大值,加上当前格子的分数。

转移方程本质是: dp[i][j][k][z] = max( dp[i-1][j][k][z], dp[i][j-1][k][z], dp[i][j][k-1][z], dp[i][j][k][z-1] ) + score[x]

注意处理边界,只有当对应卡片数量大于 0 时才能进行转移。

初始化 题目规定自动获得起点分数,所以 dp[0][0][0][0] = a[1]。

代码实现

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

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

int n, m;
int a[N], cnt[5]; // 存储棋盘分数,存储四张卡片各自数目
int dp[M][M][M][M];

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++) {
                    // x 为当前访问格子下标
                    int x = 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[x]);
                    if(j > 0) t = max(t, dp[i][j - 1][k][z] + a[x]);
                    if(k > 0) t = max(t, dp[i][j][k - 1][z] + a[x]);
                    if(z > 0) t = max(t, dp[i][j][k][z - 1] + a[x]);
                }
            }
        }
    }
    
    cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]] << endl;
    return 0;
}

总结

这四道题涵盖了线性 DP 从一维到多维的典型应用场景。掌握它们的关键在于准确理解状态的含义,并找到正确的'最后一步'来划分情况。在实际编码中,务必注意数组越界、初始化细节以及取模运算的时机。

目录

  1. 引言
  2. 台阶问题
  3. 题目描述
  4. 解题思路
  5. 代码实现
  6. 最大子段和
  7. 题目描述
  8. 解题思路
  9. 代码实现
  10. 传球游戏
  11. 题目描述
  12. 解题思路
  13. 代码实现
  14. 乌龟棋
  15. 题目描述
  16. 解题思路
  17. 代码实现
  18. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Java Web 开发环境搭建:IDEA 与 Tomcat 安装部署指南
  • 多模态大模型 API 调用与本地部署成本对比分析
  • 8 篇必读的大模型论文
  • 大模型开发核心知识体系与进阶学习路径
  • Photoshop AI 插件使用指南:ComfyUI 与 Stable Diffusion 集成
  • 淘宝超市卡 TopAPI 接入实战:Spring Boot + Lombok 实现方案
  • Python 字节码逆向工具 pycdc 使用指南与原理分析
  • 千寻智能融资近20亿,荣耀进军机器人,智平方成百亿独角兽,华为云发布具身智能平台
  • Vue3 + Python 校园排球俱乐部信息管理系统
  • 自然语言处理在医疗领域的应用与实战
  • C++ 与 Linux 基础:进程打开磁盘文件的内核实现与源码解析
  • C++ STL 容器详解:map 与 set 原理及实战
  • MySQL 8.0.43 免安装版配置与安装指南
  • C++ STL 容器详解:map 与 set 核心用法与底层逻辑
  • C++ STL 容器详解:set、map 及其底层原理
  • Yolo11 基于 DroneVehicle 数据集的无人机视角下车辆目标检测
  • FAIR plus 机器人全产业链接会:聚焦全产业链技术与创新
  • 基于 AI 陪练的前端新手入门:从零开始构建第一个网页
  • Spring Boot 实现三模安全登录:微信扫码 + 手机 + 邮箱验证码
  • C++ 线程库与多线程编程实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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