动态规划入门:从棋盘路径问题掌握状态定义与转移方程
1. 项目概述从棋盘问题二看算法竞赛中的路径计数最近在带学生准备一些算法竞赛正好翻到了上海计算机学会2024年5月月赛的题目其中丙组的T5“棋盘问题二”引起了我的注意。这类棋盘路径问题可以说是动态规划DP入门的经典试金石它不涉及特别复杂的数据结构但对思维逻辑的严谨性和状态定义的准确性要求极高。很多初学者在接触DP时总觉得状态转移方程“只可意会”而棋盘问题恰恰提供了一个将抽象思维可视化的绝佳场景——你可以实实在在地看到一个“棋盘”想象一个“棋子”在上面移动这比单纯处理一维数组要直观得多。这道题的核心简单来说就是给定一个N x M的棋盘棋子在左上角(1,1)起点要走到右下角(N, M)终点。棋子只能向右或向下移动。这听起来就是最基础的“不同路径”问题。但题目真正的挑战在于棋盘上存在一些“障碍格”棋子不能落在这些格子上。同时题目还可能对路径的“代价”或“属性”有额外要求比如路径上经过的数字之和、是否需要满足特定奇偶性等这需要我们在基础模型上增加状态维度。解决这类问题不仅是为了AC一道题更是为了掌握一种将复杂约束条件转化为清晰状态定义的思维能力这种能力在解决更复杂的优化问题时至关重要。2. 核心思路拆解状态定义与转移方程的构建逻辑面对棋盘问题我们的第一反应往往是搜索DFS/BFS。对于小规模棋盘比如N, M 10搜索是可行的。但题目数据范围往往会设得较大比如N, M 100甚至1000搜索的指数级时间复杂度将无法承受。这时动态规划的优势就体现出来了它可以将时间复杂度优化到O(N*M)甚至更低。2.1 基础模型无障碍棋盘的不同路径我们先从最简单的模型开始一个N行M列的无障碍棋盘求从(1,1)到(N,M)的总路径数。这里的“状态”非常自然设dp[i][j]表示从起点(1,1)走到格子(i,j)的不同路径总数。那么如何走到(i,j)呢根据“只能向右或向下”的规则棋子只可能从它的上方(i-1, j)或者左方(i, j-1)走过来。因此到达(i,j)的路径数就等于到达(i-1,j)的路径数与到达(i,j-1)的路径数之和。这就引出了我们的状态转移方程dp[i][j] dp[i-1][j] dp[i][j-1]当然我们需要边界条件或称初始状态起点dp[1][1] 1因为从起点到起点只有一种方式不动。对于第一行i1的格子它们只能从左方来因为上方没有格子。所以当j1时dp[1][j] dp[1][j-1]。同理对于第一列j1的格子它们只能从上方来。所以当i1时dp[i][1] dp[i-1][1]。这个模型是所有棋盘路径问题的基石。2.2 引入障碍状态转移的“断路”机制现在引入障碍。假设我们有一个二维数组grid[N1][M1]为了方便我们从1开始索引grid[i][j] 1表示该格子是障碍grid[i][j] 0表示可通过。我们的状态定义dp[i][j]依然表示走到(i,j)的路径数但需要增加一个关键判断如果(i,j)本身是障碍那么不可能有任何路径到达这里所以dp[i][j]应该直接为0。相应地状态转移方程也需要修改。只有当(i,j)不是障碍时我们才计算从上方和左方转移过来的路径。同时在计算转移来源时也必须确保来源格子不是障碍。因为如果来源格子是障碍从那里过来的路径数为0。所以更严谨的写法是 如果grid[i][j] 1则dp[i][j] 0。 否则dp[i][j] (grid[i-1][j] 0 ? dp[i-1][j] : 0) (grid[i][j-1] 0 ? dp[i][j-1] : 0)。这里有一个编程细节为了处理边界i1或j1时访问dp[i-1][j]或dp[i][j-1]会导致数组越界我们通常会将dp数组定义为(N2) x (M2)大小并将下标0的行和列初始化为0作为虚拟边界。这样状态转移可以统一写成dp[i][j] (grid[i][j] 1) ? 0 : (dp[i-1][j] dp[i][j-1])因为对于边界格子其虚拟上方或左方的dp值为0符合“没有路径从界外来”的逻辑。2.3 进阶思考路径代价与多维状态“棋盘问题二”之所以是“二”通常意味着它比基础的无障碍路径计数更复杂。常见的进阶方向有带权路径每个格子有一个数值代价或收益要求计算所有路径的代价之和或者求一条总代价最小/最大的路径。这时dp[i][j]的含义就需要变为“到达(i,j)时的最小总代价”转移方程变为取min或max操作并加上当前格子的代价grid[i][j]。路径属性约束例如要求路径上经过的数字之和为偶数或者路径必须经过某个特定格子。这需要在状态中增加一个维度来记录这个属性。比如定义dp[i][j][k]其中k0表示路径和为偶数到达(i,j)的路径数k1表示路径和为奇数。转移时需要根据当前格子的数字奇偶性来更新k的状态。理解如何根据问题约束来增加状态维度是解决复杂DP问题的关键。这需要仔细分析哪些信息是决定未来决策所必需的必须把它们纳入状态定义中。3. 代码实现与细节剖析理论清晰后我们来看代码实现。这里我以“带障碍的路径计数”为基础模型给出一个完整的C实现并穿插讲解关键细节和易错点。#include iostream #include vector using namespace std; int main() { int n, m; cin n m; // 为了方便从1开始索引我们定义大小为 (n2) x (m2) 的网格和dp数组 // 第0行和第0列作为虚拟边界全部初始化为障碍或0值 vectorvectorint grid(n 2, vectorint(m 2, 1)); // 1表示障碍0表示通路 vectorvectorlong long dp(n 2, vectorlong long(m 2, 0)); // 读取棋盘1-based索引 for (int i 1; i n; i) { for (int j 1; j m; j) { cin grid[i][j]; // 假设输入中0表示通路1表示障碍 } } // 初始化起点。注意如果起点就是障碍那么路径数为0。 if (grid[1][1] 0) { dp[1][1] 1; } // 动态规划填表 for (int i 1; i n; i) { for (int j 1; j m; j) { // 跳过起点因为已经初始化了 if (i 1 j 1) continue; // 如果当前格子是障碍dp值保持为0初始化值 if (grid[i][j] 1) { dp[i][j] 0; continue; } // 状态转移只能从上方或左方来 // 因为dp[0][*]和dp[*][0]都是0所以边界情况也适用 dp[i][j] dp[i - 1][j] dp[i][j - 1]; // 注意如果路径数可能非常大题目可能要求取模 // dp[i][j] % MOD; } } // 输出终点(n, m)的路径数 cout dp[n][m] endl; return 0; }3.1 关键实现细节与避坑指南数组索引与边界处理这是最容易出错的地方。坚持使用1-based索引即下标从1开始表示第一行第一列并预留第0行和第0列作为“哨兵”可以极大地简化边界条件的代码。如上所示dp[0][j]和dp[i][0]自然为0使得状态转移方程dp[i][j] dp[i-1][j] dp[i][j-1]对第一行和第一列的格子也成立无需特殊判断。数据类型与溢出路径数可能增长得非常快。对于100x100的棋盘无障碍路径数是一个巨大的组合数。int类型几乎肯定会溢出。务必使用long long64位整数。如果题目明确要求对一个大数取模如1e97则在每次加法后立即取模。障碍格子的处理顺序在双重循环中我们首先判断grid[i][j]是否为障碍。如果是则显式地将dp[i][j]设为0虽然它初始化就是0但显式设置更清晰然后continue跳过转移。这确保了障碍格子的值不会被错误地计算。起点的初始化这是一个逻辑点。dp[1][1]应该初始化为1吗前提是(1,1)不是障碍。如果起点就是障碍那么整个问题无解所有dp值都应为0。代码中必须包含这个判断。输入格式务必看清题目描述障碍物的表示方式可能不同。有的题目用‘#’表示障碍用‘.’表示通路有的用1表示通路0表示障碍。读取和判断时要对应正确。注意上面的代码假设输入中0表示通路1表示障碍。如果题目规定相反需要在读取后或判断时进行取反逻辑。4. 从路径计数到最小代价路径“棋盘问题二”很可能不是简单的计数而是引入了“代价”概念。我们来看看如何修改模型。假设每个格子(i,j)有一个非负代价cost[i][j]要求从起点到终点的所有路径中总代价最小的那条路径的代价是多少。这时dp[i][j]的定义就需要改变它表示从起点(1,1)走到(i,j)的最小总代价。状态转移方程也相应变为到达(i,j)的最小代价等于从上方来的最小代价和从左方来的最小代价中较小的那个再加上踏上(i,j)格子本身的代价。dp[i][j] min(dp[i-1][j], dp[i][j-1]) cost[i][j]边界条件dp[1][1] cost[1][1]。对于第一行i1, j1只能从左方来dp[1][j] dp[1][j-1] cost[1][j]。对于第一列j1, i1只能从上方来dp[i][1] dp[i-1][1] cost[i][1]。如果还有障碍物那么障碍物格子的dp值可以设为无穷大INT_MAX或LLONG_MAX表示不可达并在状态转移时忽略来自障碍物格子的路径。// 最小代价路径核心转移代码片段 const long long INF 1e18; vectorvectorlong long dp(n 2, vectorlong long(m 2, INF)); if (grid[1][1] ! 障碍) dp[1][1] cost[1][1]; for (int i 1; i n; i) { for (int j 1; j m; j) { if (i 1 j 1) continue; if (grid[i][j] 障碍) continue; // dp[i][j] 保持 INF long long from_top (grid[i-1][j] ! 障碍) ? dp[i-1][j] : INF; long long from_left (grid[i][j-1] ! 障碍) ? dp[i][j-1] : INF; if (from_top INF from_left INF) { // 两个方向都不可达当前格子也不可达 dp[i][j] INF; } else { dp[i][j] min(from_top, from_left) cost[i][j]; } } } // 最终答案 if (dp[n][m] INF) { cout 无路径 endl; } else { cout dp[n][m] endl; }5. 常见问题与调试技巧实录在实际编码和调试这类问题时我遇到和学生们犯过的错误五花八门。这里总结几个高频问题5.1 初始化错误问题忘记初始化dp[1][1]或者错误地将其初始化为0在最小代价问题中。排查总是先单独检查起点状态。对于计数问题起点若非障碍则为1对于代价问题起点代价就是cost[1][1]。5.2 数组越界问题在循环中访问了dp[i-1][j]当i1时访问了dp[0][j]如果数组没有多开一行就会越界。解决强烈推荐“多开一圈”的数组定义法。如vectorvectorlong long dp(n 2, vectorlong long(m 2, 0))并从下标1开始使用。虚拟的0行0列自动提供了安全的边界值。5.3 整数溢出问题路径数巨大使用int导致结果出现负数或完全错误。解决在竞赛中只要涉及计数或累加除非题目明确说明范围很小否则无脑使用long long。这是一个成本极低的好习惯。5.4 状态转移逻辑遗漏问题在带障碍的问题中只判断了当前格子(i,j)是否为障碍但忘记了在计算dp[i-1][j] dp[i][j-1]时dp[i-1][j]或dp[i][j-1]本身可能因为对应格子是障碍而为0。如果代码逻辑是if(grid[i][j]!障碍) dp[i][j]dp[i-1][j]dp[i][j-1]这本身没问题因为来源格子的dp值如果为0加法自然体现。但更清晰的写法是显式判断来源格子是否可达尤其是在求最小值等问题中。5.5 输入读取与题意理解偏差问题这是最致命的错误。题目说“1表示障碍”你代码里判断if(grid[i][j]1)但实际输入样例中可能用‘#’表示障碍。解决编码前花一分钟仔细阅读输入输出格式。写代码时将“通路”和“障碍”的判断条件用有意义的常量或布尔变量表示例如const int OBSTACLE 1; if (grid[i][j] OBSTACLE) { ... }这样如果理解错了只需修改一个常量。5.6 调试技巧打印DP表当程序结果不对时最有效的调试方法之一就是打印出整个dp表对于小规模数据。cout DP Table: endl; for (int i 1; i n; i) { for (int j 1; j m; j) { cout dp[i][j] \t; } cout endl; }对照着手算或逻辑推导的几行几列很容易发现哪里开始出错的。例如如果发现第一行的某个值不对那肯定是第一行的初始化或转移逻辑有问题。6. 性能优化与空间复杂度思考我们当前的算法时间复杂度是O(NM)这对于N, M在1000以内的题目通常足够了。空间复杂度也是O(NM)即dp数组的大小。在某些极端情况下如果N, M非常大比如10^4O(N*M)的空间约10^8个long long占用接近800MB可能会超出内存限制。这时我们可以进行空间优化。观察状态转移方程dp[i][j]只依赖于dp[i-1][j]上一行和dp[i][j-1]当前行左边。因此我们并不需要保存整个二维表只需要保存“上一行”和“当前行”即可。vectorlong long prev_row(m 2, 0), curr_row(m 2, 0); // 初始化第一行 curr_row[1] (grid[1][1] 0) ? 1 : 0; for (int j 2; j m; j) { curr_row[j] (grid[1][j] 0) ? curr_row[j-1] : 0; } if (n 1) { // 只有一行的情况 cout curr_row[m] endl; return 0; } for (int i 2; i n; i) { // 交换上一行变成旧的当前行新的当前行待计算 swap(prev_row, curr_row); // 计算新当前行的第一个元素 curr_row[1] (grid[i][1] 0) ? prev_row[1] : 0; // 计算新当前行的其余元素 for (int j 2; j m; j) { if (grid[i][j] 1) { curr_row[j] 0; } else { curr_row[j] prev_row[j] curr_row[j-1]; } } } cout curr_row[m] endl;这样空间复杂度从O(N*M)降到了O(M)。这种优化在笔试或竞赛中遇到大数据时非常有用。不过在初学阶段先写出清晰正确的二维DP版本更为重要优化可以在理解透彻后进行。7. 举一反三相关变种问题掌握了基础模型你可以尝试解决一系列变种问题这些都是对状态定义和转移方程设计能力的很好锻炼最大收益路径每个格子有收益值求最大总收益路径。将状态转移中的min改为max即可。路径方案数带模数路径数巨大要求输出对1e97取模的结果。在每次加法后立即取模dp[i][j] (dp[i-1][j] dp[i][j-1]) % MOD。有“传送门”的棋盘某些格子是传送门到达后会立刻传送到另一个指定格子。这需要在状态转移时特殊处理当(i,j)是传送门时dp[i][j]的值直接加到目标格子的dp值上而dp[i][j]本身可能置0或保持取决于题目规则是“经过”还是“到达并传送”。必须经过某些点的路径计数可以将棋盘按必须经过的点分割成若干段分别计算每段之间的路径数然后相乘。路径回文问题要求从左上到右下的路径构成的序列如经过格子的字符是回文串。这通常需要结合DP和区间DP的思想状态可能定义为dp[x1][y1][x2][y2]表示从起点到(x1,y1)和从终点到(x2,y2)的两条对称路径的匹配情况复杂度较高。解决这些问题的心法是仔细分析问题的新约束条件思考这个条件如何影响“状态”。是需要增加一个维度来记录信息如奇偶性、余数、特定计数还是需要改变状态的含义如从计数变为最值多练习这种建模能力就会逐渐内化。棋盘问题就像动态规划的一个微观世界它规则清晰场景具体。通过反复练习这类问题你能深刻理解“状态”、“状态转移方程”、“最优子结构”和“无后效性”这些DP核心概念。下次再遇到更复杂的DP问题不妨先在脑子里画一个“棋盘”想想“状态”是什么“棋子”怎么走或许就能找到突破口。

相关新闻

本地部署AI代码助手:从开源模型到IDE集成的完整实践指南

本地部署AI代码助手:从开源模型到IDE集成的完整实践指南

这次我们来看一个名为“Codex”的项目。从标题“我的拼多多版Codex可能要融到2000万美金了...”来看,这很可能是一个定位为“平价”或“高性价比”的AI代码生成工具,旨在提供类似GitHub Copilot或OpenAI Codex的功能,但成本更低、更易获取。对…

2026/7/28 10:18:59 阅读更多 →
JavaScript进阶避坑指南:this、闭包与异步编程实战

JavaScript进阶避坑指南:this、闭包与异步编程实战

1. JavaScript进阶避坑指南:这些坑我替你踩过了从事前端开发十年,我见过太多开发者从入门到放弃的故事。JavaScript这门语言看似简单,实则暗藏玄机。今天要分享的这些"坑",都是我和团队成员用真实项目事故换来的经验。无…

2026/7/28 10:18:59 阅读更多 →
TPIC7710EVM评估模块深度解析:从硬件设计到软件驱动的汽车电子开发实战

TPIC7710EVM评估模块深度解析:从硬件设计到软件驱动的汽车电子开发实战

1. 项目概述与EVM的核心价值 在汽车电子,尤其是车身控制和安全系统领域,开发周期和前期验证的可靠性是决定项目成败的关键。当你拿到一颗功能复杂的专用集成电路(ASIC),比如用于电子驻车制动(EPB&#xff0…

2026/7/28 10:17:58 阅读更多 →

最新新闻

WebSocket认证实践:Token传递与安全实现

WebSocket认证实践:Token传递与安全实现

1. Websocket与Token认证的深度解析Websocket作为一种全双工通信协议,在现代Web应用中扮演着重要角色。不同于传统的HTTP请求,Websocket建立的是持久化连接,这就带来了一个关键问题:如何在连接建立时传递认证凭证(如To…

2026/7/28 10:27:02 阅读更多 →
Zephyr RTOS设备树实战:STM32F103C8T6 GPIO控制LED详解

Zephyr RTOS设备树实战:STM32F103C8T6 GPIO控制LED详解

这次我们来看一个基于 Zephyr RTOS 的 STM32F103C8T6 GPIO 控制 LED 的实战案例。对于很多从传统单片机开发(如 STM32 HAL/标准库)转向 Zephyr 的开发者来说,最大的困惑可能就是“设备树(Device Tree)”。以前配个 LED 灯,无非就是改改宏定义、初始化一下 GPIO 端口,现在…

2026/7/28 10:27:02 阅读更多 →
AI时代超级个体的创造力革命与实战指南

AI时代超级个体的创造力革命与实战指南

1. 星河超级个体SHOW TIME:AI时代的创造力革命 在算法主导的数字洪流中,一个有趣的现象正在发生——越来越多的"超级个体"正在突破传统组织边界,借助AI工具构建独特的创意生态。这场名为"星河超级个体SHOW TIME"的浪潮&a…

2026/7/28 10:27:02 阅读更多 →
戴尔G15散热控制终极指南:开源免费替代AWCC的完整解决方案

戴尔G15散热控制终极指南:开源免费替代AWCC的完整解决方案

戴尔G15散热控制终极指南:开源免费替代AWCC的完整解决方案 【免费下载链接】tcc-g15 Thermal Control Center for Dell G15 - open source alternative to AWCC 项目地址: https://gitcode.com/gh_mirrors/tc/tcc-g15 还在为戴尔G15笔记本散热问题烦恼吗&…

2026/7/28 10:27:02 阅读更多 →
QQ音乐格式解密:Mac用户必备的音频格式转换神器QMCDecode完整指南

QQ音乐格式解密:Mac用户必备的音频格式转换神器QMCDecode完整指南

QQ音乐格式解密:Mac用户必备的音频格式转换神器QMCDecode完整指南 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目录&#…

2026/7/28 10:27:02 阅读更多 →
基于树莓派与红外传感器的低成本智能猫砂盆通风系统DIY

基于树莓派与红外传感器的低成本智能猫砂盆通风系统DIY

1. 项目缘起:一个“有味道”的痛点与低成本解法 养猫的朋友们,尤其是像我这样住在小户型公寓里的,大概都经历过一个共同的烦恼:猫主子如厕后的“余味绕梁”。传统的猫砂盆,无论封闭式还是开放式,都很难在第…

2026/7/28 10:26:02 阅读更多 →

日新闻

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生 【免费下载链接】OmenSuperHub Control Omen laptop performance, fan speeds, and keyboard lighting, and unlock power limits. 项目地址: https://gitcode.com/gh_mirrors/om/OmenSuperHub 你是否也曾为官方Om…

2026/7/28 0:00:43 阅读更多 →
RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

做 RAG 的人应该都踩过这个致命的坑:把几百页的财报、法规、技术手册扔给向量库,问一个具体问题,搜出来的全是沾边但没用的内容 —— 关键信息要么被硬切块拆碎了,要么藏在几十条结果的最下面。语义相似≠真正相关,这个…

2026/7/28 0:00:43 阅读更多 →
抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

2026年做短视频运营,从抖音上扒文案早就不是偷偷抄笔记的事了。我刚开始做内容的时候,每天刷半小时抖音,手动把爆款视频的口播敲进备忘录,一条2分钟的视频得花十来分钟,碰到语速快的还要反复回听。后来试了一圈工具&am…

2026/7/28 0:00:43 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/27 4:33:59 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/28 8:29:16 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/28 5:03:42 阅读更多 →

月新闻