传纸条P1006双状态DP详解:从状态设计到滚动数组优化
P1006 传纸条我第一次见是在一份动态规划题单里题目包装得很校园你传我、我传你中间每个格子还标着善意值。剥掉故事外壳它其实是个需要把两条路径放在同一个 DP 状态里一起规划的双状态DP问题。很多做过“方格取数”的同学第一次写这题都会很自信结果交上去发现要么答案偏大要么直接 WA原因多半是没想透“两条路径同时走”意味着什么。这篇文章我从头推导一遍状态设计、转移方程、代码实现再把实际调题踩过的坑翻出来希望能让刷这道题的人少走点弯路。1. 这道题在问什么以及最容易掉的坑1.1 题目场景还原题面大概是这样有一个 n 行 m 列的矩阵每个格子有一个数值代表同学之间的善意值。现在要从左上角 (1,1) 的同学出发把一张纸条传到右下角 (n,m) 的同学手里对方看完之后还要再传回一张纸条。传纸条只能向右或向下移动每个格子上的同学最多只能帮忙传递一次所以两条路径不能在中间格子重合。要求两条路径经过的格子上的数值之和最大。这里有个容易误会的点题目里说的是“过去一条、回来一条”但二维网格里的路径是可逆的。你把回程路径从 (n,m) 倒着看到 (1,1)本质上还是一条从起点到终点的路径。所以问题可以等价转换成选两条从 (1,1) 到 (n,m) 的路径中间不能相交让覆盖到的格子分值总和最大。起点和终点是两个路径共用的允许重合这个特判后面会反复用到。这类题的经典解法不是“分别规划”而是“同步规划”。什么叫同步规划就是想象两个人同时从左上角出发每一步一个人只能向右或向下走两个人的走动是同步的步数永远一样多。既然步数一样多那么同一时刻两个人在棋盘上的“行列和”一定相等。正是这个“行列和相等”的约束最后帮我们把 DP 状态从四维压到三维。1.2 为什么不能拆成两次普通 DP初学者最常见的想法是先用一次普通 DP 求出“第一条最优路径”然后把这条路径经过的格子分数清零再跑一次 DP 求第二条路径两次结果相加不就是答案吗这个思路看起来自然但它贪心了。第一次 DP 只保证第一条路单独最优完全不考虑这条路是否会堵死第二条路。数学上这破坏了动态规划的最优子结构每一步的最优决策不应该只依赖当前局部状态还要照顾另一条路径未来的空间。你可以自己构造一个小矩阵试试故意把高权值格放在一条窄通道上第一条路取走它之后第二条路只能被迫绕到全是低分的角落而如果第一条路稍微绕一下让出通道两条路的总分反而更高。这就是“分开两次 DP”会得到错误答案的核心原因。所以 P1006 这类题必须把两条路径放进同一个状态里让它们在时间轴上一起演进。这也是标题里“双状态DP”的真正含义。2. 双状态DP的状态设计逻辑2.1 用一条“对角线”当作时间轴先假设两条路径同时出发、同时走规定每一步只能向右或向下。设第一条路径当前在 (x1, y1)第二条路径当前在 (x2, y2)。因为同步走两个人从 (1,1) 出发经过的步数是一样的所以有x1 y1 x2 y2这个相等的“行列和”就是每条对角线上所有格子的共同编号。如果要遍历格子不需要同时关注 x 和 y只要知道当前在哪条对角线上再知道该路径当前在第几行列就唯一确定了。这个“行列和”就是时间轴我习惯用 s 表示。于是可以定义状态dp[s][x1][x2] 表示两条路径目前都位于行列和为 s 的对角线上第一条路径在第 x1 行列是 s - x1第二条路径在第 x2 行列是 s - x2时能取得的最大分值总和。单个路径的普通 DP状态里只需要一个当前点双状态DP状态里同时装着两个当前点。第一维 s 记录公共时间后两维 x1、x2 记录两个人的行坐标两个人的列坐标通过 s 减去行坐标得到。这就是“双状态”这个说法的来源表面上状态变量有三个维度但本质上同时管理两条路径的位置。2.2 为什么知道了行就能推列有人会问状态里为什么没有 y1 和 y2因为 s 定了行定了列就是 s - x。比如某条路径在 s 5 这一层行 x 2那列必然是 3。这要求在棋盘边界内合法也就是列必须在 1 到 m 之间。这个“由行列和反推列坐标”的操作是整道题最关键的压缩手段。你如果硬要用四维状态 dp[x1][y1][x2][y2] 也不是不行但那样状态数量会膨胀到 n²m²而且转移时还得考虑两条路径步数是否一致会很别扭。用对角线 s 之后的复杂度是 O((nm) * n²)在实际题目给定 n、m 都是几十到一百左右时运行压力很小代码也清爽很多。这里我想额外说一点做题直觉遇到“两个物体在网格上同步移动”的 DP优先找它们的“公共不变量”。在这题里公共不变量就是行列和在另一些题里可能是可移动步数可能是剩余体力。你把公共不变量找出来往往就能像削苹果一样把高维状态一层层削掉。3. 转移方程和“重叠格子”怎么处理3.1 四个前驱状态怎么来的当前状态是 dp[s][x1][x2]两条路径都走到了对角线 s。每条路径在进入当前格子之前只能来自上边或左边。第一条路径的上一步可能是 (x1 - 1, s - x1) 或 (x1, s - x1 - 1)第二条路径同理。两者组合起来上一层的状态一共有四种第一条从上边来第二条从上边来dp[s-1][x1-1][x2-1]第一条从上边来第二条从左边来dp[s-1][x1-1][x2]第一条从左边来第二条从上边来dp[s-1][x1][x2-1]第一条从左边来第二条从左边来dp[s-1][x1][x2]取这四个值的最大值再加上当前两条路径所在格子的数值就是 dp[s][x1][x2]。写成公式就是dp[s][x1][x2] max(上面四个前驱) 当前两个格子的分数这个转移本身不复杂。容易混的是第四个dp[s-1][x1][x2] 表示两条路径都从各自左边的格子走过来。由于层数从 s-1 变成 s行没变列各自加一完全符合向右走的规则。第一次写代码时容易漏掉这个组合导致答案偏小。3.2 两条路径走到同一个格子怎么办接下来是重头戏如果 x1 x2因为两格子在同一层 s 上列也必然相同也就是说两条路径当前走到了同一个格子。这时按题意中间格子是不能重复使用的所以这个状态应该视情况处理。当 s 2 时两个人都还在起点 (1,1)起点只能贡献一次分数当 s nm 时两个人都到达终点 (n,m)终点也只能贡献一次分数当 s 为中间值且 x1 x2 时两条路径重合了严格写法应该直接跳过这个非法状态。但你会发现很多网上题解在 x1 x2 时并不跳过而是只加一次该格子的分数。为什么这样也能过因为题目给出的格子善意值是非负的。如果两条路径在某个中间格重合它们扫过的格子集合和两条不相交路径扫过的集合在“路径可交换”的意义下是可以对齐的也就是说非负权值时允许重叠但只计一次分的最优值和强制不允许重叠的最优值是相等的。这个结论很多新手不知道看到代码里有个if (i j)的诡异分支容易以为写错了。我个人建议学习阶段先用“严格跳过中间重叠”的版本理解题意提交时用“重叠只计一次分数”的版本后者对 n1 或 m1 这种退化边界更友好。两种写法我都放在后面代码部分给出实现。4. 完整实现从三维 DP 到滚动数组4.1 可以直跑的三维对角 DP先给出一个最容易理解的版本用二维数组滚动也能跑但这里先展示带完整维度的写法方便对着状态定义看。下面代码以 n 行 m 列为例所有格子下标从 1 开始s 从 2 枚举到 n m。二维格子数值用变量名aDP 数组第一维直接用s展开#include bits/stdc.h using namespace std; const int N 55; const int INF 1e9; int n, m; int a[N][N]; int dp[N * 2][N][N]; int main() { cin n m; for (int i 1; i n; i) { for (int j 1; j m; j) { cin a[i][j]; } } // 初始化所有状态为不可达 for (int s 0; s n m; s) { for (int i 0; i n; i) { for (int j 0; j n; j) { dp[s][i][j] -INF; } } } // s 2 时两条路径都只能在起点 (1,1) dp[2][1][1] a[1][1]; for (int s 3; s n m; s) { for (int x1 1; x1 n; x1) { for (int x2 1; x2 n; x2) { int y1 s - x1; int y2 s - x2; if (y1 1 || y1 m || y2 1 || y2 m) continue; int best dp[s - 1][x1][x2]; best max(best, dp[s - 1][x1 - 1][x2]); best max(best, dp[s - 1][x1][x2 - 1]); best max(best, dp[s - 1][x1 - 1][x2 - 1]); if (best -INF / 2) continue; if (x1 x2) { // 里程碑写法重叠只算一次 dp[s][x1][x2] best a[x1][y1]; } else { dp[s][x1][x2] best a[x1][y1] a[x2][y2]; } } } } cout dp[n m][n][n] endl; return 0; }这段代码的边界考虑是这样的起点已经在初始化时加过了所以循环从 s3 开始终点在同一格重叠分支只加一次 a[n][m]中间如果重叠因为是非负权值重复格子只计一次即可。最终答案存在 dp[n m][n][n]也就是两条路径都到达第 n 行时列分别是 m终点重合。4.2 滚动数组优化空间再降一个量级三维版本已经够用但竞赛题经常要养成节约空间的好习惯。因为第 s 层只依赖第 s - 1 层DP 的第一维只需要保留两个取值也就是dp[s 1]和dp[(s - 1) 1]。#include bits/stdc.h using namespace std; const int N 55; const int INF 1e9; int n, m; int a[N][N]; int dp[2][N][N]; int main() { cin n m; for (int i 1; i n; i) { for (int j 1; j m; j) { cin a[i][j]; } } for (int i 0; i n; i) { for (int j 0; j n; j) { dp[0][i][j] dp[1][i][j] -INF; } } // s 2 这一层对应 parity 0 dp[0][1][1] a[1][1]; for (int s 3; s n m; s) { int now s 1; int pre (s - 1) 1; // 先清理当前层防止残留上一层的旧值 for (int i 0; i n; i) { for (int j 0; j n; j) { dp[now][i][j] -INF; } } for (int x1 1; x1 n; x1) { for (int x2 1; x2 n; x2) { int y1 s - x1; int y2 s - x2; if (y1 1 || y1 m || y2 1 || y2 m) continue; int best dp[pre][x1][x2]; best max(best, dp[pre][x1 - 1][x2]); best max(best, dp[pre][x1][x2 - 1]); best max(best, dp[pre][x1 - 1][x2 - 1]); if (best -INF / 2) continue; if (x1 x2) { dp[now][x1][x2] best a[x1][y1]; } else { dp[now][x1][x2] best a[x1][y1] a[x2][y2]; } } } } cout dp[(n m) 1][n][n] endl; return 0; }注意滚动数组最容易被坑的点不清空当前层或者把dp[now]里上一层的值直接拿来参与转移。因为你每次只保留两层但同一个位置的上一轮数据并不一定对应合法状态不清空就会污染答案。时间上s 从 2 到 nm每一层两层行循环总复杂度是 O((nm) * n * n)。空间上从三维变成了两个二维数组就是 O(n²)。这个优化在 P1006 这种小数据面前可能“锦上添花”但养成习惯后遇到 nm 更大的类似模型你就能直接套。5. 实战踩坑记录从 WA 到 AC 的几个关键点5.1 坐标合法性判断千万别漏y s - x是很容易算出负数或超过 m 的。很多人写循环时只遍历 x1、x2忘了检查 y1、y2 是否在 1 到 m 之间结果数组越界或者把非法状态当成合法状态转移。这题的格子强约束就是“列必须存在”所以每层循环一开始就把y1 1 || y1 m || y2 1 || y2 m过滤掉宁可靠前判断也别靠 DP 值自然填充。还有一个隐蔽问题当你用x1 - 1或x2 - 1作为索引访问上一层层时可能访问索引 0。如果 dp 数组第 0 行没有初始化为不可达它会读到一个垃圾值甚至可能导致错误更新。我的习惯是把 dp 数组第 0 行也统一初始化为 -INF代码里循环范围从 0 到 n 都清理一遍这样x1 - 1就算等于 0也只会读到不可达。5.2 初始化用 -INF不要用 0最大值 DP 里不可达状态必须和“合法但分数为 0”的状态区分开。如果初始值全部用 0那些从非法位置“虚空转移”出来的状态也会变成合法路径最终结果会凭空多出一大截。正确做法是用一个足够小的负数比如-1e9表示“这条路径不存在”。转移时再判断best -INF / 2就跳过。这里不要用0x3f3f3f3f那个是求最短路时的“正无穷”用在这里方向反了会表现得特别诡异。5.3 起点和终点的分数别重复加起点有两种处理方式一是先初始化dp[2][1][1] a[1][1]循环里不再加起点二是从 s2 开始把所有状态都通过转移计算但这样代码更难控制。终点同理当 x1 x2 时只加一次终点分。如果你发现自己的答案比预期多了一个格子的值先检查是不是初始化时加了起点转移时又因为重叠把起点加了一次。我调这题的时候曾经把输出写成dp[n m][n][n] a[n][m]结果总是多算一遍终点分。当时百思不解后来才反应过来终点重叠时代码里的if (x1 x2)分支已经加过 a[n][m] 了我输出又加一次当然错。5.4 边界情况 n1 或 m1 的特判思维如果棋盘只有一行或者一列从左上到右下只有一条路径两条路径被迫只能走同一条线。严格题意下这看起来没有合法解但 OJ 的数据一般不会让你真的输出一个无意义的东西而是会把这条唯一路径的分值算作答案。这就是为什么很多实现采用“重叠只加一次分数”而不是“中间重叠直接 continue”。直接跳过重叠的话n1 时所有中间状态全都非法答案会变成 -INF直接 WA。所以我在代码里用了重叠只计一次分数的写法而不是严格禁止重叠。如果你非常较真想写严格版本也可以在中间x1 x2时直接跳过但你必须确认题目的 n、m 数据保证至少存在两条不相交路径。就我实际刷题的经验来看比赛里绝大多数这种题权值非负允许重叠只算一次的写法更稳。6. 从传纸条带走的一点思考方式P1006 是我觉得双状态DP特别好的入门题因为它的故事简单状态压缩又很明显。我做这类题有一个习惯拿到题目先画一个网格把起点、终点和几条对角线标出来然后在纸上手动模拟几个状态。这个过程看着慢但能非常直观地理解“为什么 dp[s][x1][x2] 够用”。如果你后面遇到更复杂的双人移动问题比如两个人有不同速度、可以原地等待、或者允许相撞但代价不同不要急着套公式先回到建模的原点找公共时间轴找坐标之间的关系再把约束写进转移公式。传纸条这道题把“同步 不重叠”这个模型演示得很干净吃透它相当于给更复杂的双状态DP打了一个非常扎实的基础。最后再分享一个我自己的教训刷 DP 题不要只盯着 AC多花十分钟想一想“这个状态为什么可以压缩”“这个重叠分支为什么能这么写”。想明白之后下次遇到变体题你会比死记模板的人快很多。

相关新闻

大气波导高度如何影响雷达探测与微波通信:从修正折射率到射线追踪

大气波导高度如何影响雷达探测与微波通信:从修正折射率到射线追踪

简介:一份面向通信工程与气象交叉领域研究者的MATLAB源码资源,围绕大气波导高度受气温、气压、湿度等气候参数影响的建模与分析展开,涉及大气折射率、不均匀性参数与折射角等核心物理量。压缩包内共4个m文件,整体仅2KB&#xff0c…

2026/10/18 20:58:39 阅读更多 →
MSVC2017+OSG3.63+osgEarth2.10+Qt5.14.7 Windows构建指南

MSVC2017+OSG3.63+osgEarth2.10+Qt5.14.7 Windows构建指南

简介:本资源是一套面向三维地理信息可视化开发者的完整编译环境集成包,适用于使用MSVC2017进行QtOSGosgEarth跨平台C开发的中高级工程师与科研人员,解决OpenSceneGraph 3.6.3与osgEarth 2.10在Qt5.14环境下依赖复杂、编译困难、链接易错等典型…

2026/10/19 1:08:58 阅读更多 →
Win32字符集全解:从Unicode到TCHAR的编程避坑指南

Win32字符集全解:从Unicode到TCHAR的编程避坑指南

很多刚接触Win32 SDK编程的人,打开Visual Studio新建项目时,多半会注意到“常规”选项卡里有一个字符集下拉框,默认是“使用Unicode字符集”,边上还放着一个“使用多字节字符集”。说实话,初次看到这两个选项&#xff…

2026/10/19 11:25:09 阅读更多 →

最新新闻

AI率过高怎么办?九款降AI率工具与避坑指南

AI率过高怎么办?九款降AI率工具与避坑指南

上个月帮一位在外地项目上常驻的在职朋友看论文。他发来一张截图,检测系统提示“疑似AI生成内容占比接近四成”,导师在批注里连续写了好几遍——这几段请你按自己的话重写。他挺委屈:内容确实是自己对着调研材料一点一点整理出来的&#xff0…

2026/10/19 23:26:19 阅读更多 →
开源代码评审工作流:本地化、可审计、可定制的LLM实践方案

开源代码评审工作流:本地化、可审计、可定制的LLM实践方案

1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码评审工作流“open-code-review”这个名字乍一听像某个新发布的 CLI 工具,或者某家大厂刚开源的 AI 代码审查平台。但实际翻遍 GitHub、Hugging Face 和主流技术社区,你会…

2026/10/19 23:26:19 阅读更多 →
基于YOLO11的无人机检测识别系统设计与实现

基于YOLO11的无人机检测识别系统设计与实现

一提到毕设做深度学习目标检测,十个里有八个都在刷COCO、VOC数据集,真正能落到现实问题上的反而不多。但“空域安全无人机检测识别”这个方向不一样,它直接对准了一个真实且刚需的场景——机场周边、重要场馆、活动安保都需要防范低空慢速小目…

2026/10/19 23:23:08 阅读更多 →
BP-PID残差补偿控制:Simulink中可解释的神经网络增强型PID实现

BP-PID残差补偿控制:Simulink中可解释的神经网络增强型PID实现

简介:本资源是一套基于Simulink实现BP神经网络自整定PID控制器的完整工程实践材料,面向自动化、控制工程及人工智能交叉领域的高校学生、科研人员与工业控制工程师,解决传统PID参数依赖经验整定、适应性差、动态响应不足等实际问题。压缩包共…

2026/10/19 23:18:58 阅读更多 →
STM32 SD卡数据落盘实战:fatfs移植与CSV文件生成

STM32 SD卡数据落盘实战:fatfs移植与CSV文件生成

简介:这份资源面向嵌入式开发学习者与STM32工程师,聚焦在STM32F429平台上集成FatFS文件系统、驱动SD卡写入数据并生成.csv文件,同时结合以太网驱动搭建TCP服务器接收网络数据。内容覆盖FatFS的移植与参数配置、SDIO或SPI方式下的SD卡初始化与…

2026/10/19 23:16:55 阅读更多 →
AI视频生成工作流:从产品拆解到批量成片的完整指南

AI视频生成工作流:从产品拆解到批量成片的完整指南

去年年底接了一个电商代运营团队的活儿,帮他们把带货短视频从“每周人工憋3条”改成“一天批量出20条成片”。项目做完以后,团队里负责剪辑的同事跟我说了一句话,让我印象特别深:以前最怕的不是剪片子,而是想不出脚本、…

2026/10/19 23:05:36 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →