当前位置: 首页 > news >正文

用C语言解决‘马拦过河卒’:一个动态规划的经典入门案例(附完整代码)

用C语言解决‘马拦过河卒’:动态规划思想的实战演练

棋盘上一个小卒要从A点走到B点,只能向右或向下移动。但棋盘上还有一匹敌方的马,它会阻挡小卒前进的路线。这个看似简单的棋盘问题,实际上蕴含着动态规划的核心思想。对于刚接触算法的新手来说,这是一个绝佳的入门案例——它不需要复杂的数学推导,却能清晰展示动态规划解决问题的完整过程。

1. 问题理解与建模

"马拦过河卒"问题描述了一个经典的路径计数场景:在棋盘坐标系中,卒子从原点(0,0)出发,每次只能向右或向下移动一格,最终到达目标点(n,m)。但棋盘上存在一个敌方马及其控制点(马走"日"字能到达的8个位置),这些点构成卒子无法通过的障碍。

关键问题要素:

  • 棋盘坐标系:原点在左上角,x轴向右,y轴向下
  • 卒子移动规则:仅能→或↓
  • 障碍点:马的位置及其8个控制点
  • 求解目标:计算从(0,0)到(n,m)的所有可行路径数

这个问题的动态规划解法需要建立两个核心概念:

  1. 状态表示:用二维数组f[i][j]表示从起点到点(i,j)的路径数
  2. 状态转移f[i][j] = f[i-1][j] + f[i][j-1](来自上方或左方的路径和)

2. 动态规划框架搭建

2.1 数据结构初始化

首先需要初始化两个关键数组:

int f[20][20]; // 存储路径数 int g[20][20]; // 标记障碍点(1表示不可达)

初始化过程包括:

  1. 将所有f[i][j]初始化为0
  2. 将所有g[i][j]初始化为0
  3. 标记马的位置及其控制点为1(障碍)
// 初始化数组 for(int i=0; i<=n; i++){ for(int j=0; j<=m; j++){ f[i][j] = 0; g[i][j] = 0; } } // 标记马的控制点 g[x][y] = 1; // 马的位置 g[x-1][y+2] = 1; // 马的8个控制点 g[x-1][y-2] = 1; g[x-2][y+1] = 1; g[x-2][y-1] = 1; g[x+1][y+2] = 1; g[x+1][y-2] = 1; g[x+2][y+1] = 1; g[x+2][y-1] = 1;

2.2 边界条件处理

动态规划问题的边界处理至关重要。对于棋盘的第一行和第一列:

  • 第一行:只能从左边过来(如果左边可达)
  • 第一列:只能从上方过来(如果上方可达)
// 处理第一行 for(int j=0; j<=m; j++){ if(g[0][j] == 0){ f[0][j] = 1; // 可达则路径数为1 } else { // 遇到障碍,后面所有点都不可达 for(; j<=m; j++) f[0][j] = 0; break; } } // 处理第一列 for(int i=0; i<=n; i++){ if(g[i][0] == 0){ f[i][0] = 1; // 可达则路径数为1 } else { // 遇到障碍,后面所有点都不可达 for(; i<=n; i++) f[i][0] = 0; break; } }

注意:边界处理时要特别注意障碍点的连锁效应——一旦某点不可达,其后的所有点也都不可达。

3. 核心动态规划实现

3.1 状态转移方程

动态规划的核心在于状态转移方程的设计。对于棋盘内部点(i,j)(i>0且j>0),路径数计算遵循:

如果(i,j)不是障碍点: f[i][j] = f[i-1][j] + f[i][j-1] 否则: f[i][j] = 0

这一方程直观反映了卒子的移动规则:到达(i,j)的路径只能来自上方(i-1,j)或左方(i,j-1)。

3.2 双重循环实现

用双重循环填充整个f数组:

for(int i=1; i<=n; i++){ for(int j=1; j<=m; j++){ if(g[i][j] == 0){ f[i][j] = f[i-1][j] + f[i][j-1]; } else { f[i][j] = 0; } } }

这个看似简单的循环,实际上完成了动态规划最关键的步骤——自底向上构建解空间。

3.3 结果输出

最终结果存储在f[n][m]中:

printf("%d\n", f[n][m]);

4. 完整代码实现与优化

4.1 完整代码整合

将上述各部分整合,得到完整解决方案:

#include <stdio.h> int main() { int n, m, x, y; scanf("%d %d %d %d", &n, &m, &x, &y); int f[20][20] = {0}; // 路径数数组 int g[20][20] = {0}; // 障碍标记数组 // 标记马的控制点 g[x][y] = 1; int dx[] = {-1, -1, -2, -2, 1, 1, 2, 2}; int dy[] = {2, -2, 1, -1, 2, -2, 1, -1}; for(int k=0; k<8; k++){ int nx = x + dx[k]; int ny = y + dy[k]; if(nx>=0 && nx<=n && ny>=0 && ny<=m){ g[nx][ny] = 1; } } // 处理第一行 for(int j=0; j<=m; j++){ if(g[0][j] == 0){ f[0][j] = 1; } else { for(; j<=m; j++) f[0][j] = 0; break; } } // 处理第一列 for(int i=0; i<=n; i++){ if(g[i][0] == 0){ f[i][0] = 1; } else { for(; i<=n; i++) f[i][0] = 0; break; } } // 动态规划填充 for(int i=1; i<=n; i++){ for(int j=1; j<=m; j++){ if(g[i][j] == 0){ f[i][j] = f[i-1][j] + f[i][j-1]; } } } printf("%d\n", f[n][m]); return 0; }

4.2 代码优化技巧

  1. 马控制点的标记优化: 使用方向数组简化代码:

    int dx[] = {-1, -1, -2, -2, 1, 1, 2, 2}; int dy[] = {2, -2, 1, -1, 2, -2, 1, -1}; for(int k=0; k<8; k++){ int nx = x + dx[k]; int ny = y + dy[k]; if(nx>=0 && nx<=n && ny>=0 && ny<=m){ g[nx][ny] = 1; } }
  2. 边界条件检查优化: 可以提前检查起点或终点是否为障碍点:

    if(g[0][0] == 1 || g[n][m] == 1){ printf("0\n"); return 0; }
  3. 数组大小调整: 根据题目约束(n,m≤15),数组大小可以适当减小:

    int f[16][16] = {0}; int g[16][16] = {0};

4.3 常见错误与调试

初学者在实现时容易遇到以下问题:

  1. 数组越界

    • 访问g[x-2][y-1]时可能越界
    • 解决方案:添加边界检查或使用更大的数组
  2. 初始化不完全

    • 忘记初始化fg数组
    • 解决方案:使用= {0}初始化
  3. 障碍点连锁反应处理不当

    • 第一行/列遇到障碍后未正确中断
    • 解决方案:使用break或设置标志变量
  4. 状态转移条件遗漏

    • 忘记检查当前点是否为障碍
    • 解决方案:确保每次更新f[i][j]前检查g[i][j]

5. 动态规划思想的扩展应用

"马拦过河卒"问题展示的动态规划模式可以推广到许多类似场景:

  1. 网格路径问题

    • 不同移动规则(如增加对角线移动)
    • 带权路径(求最小/最大路径和)
  2. 障碍物处理

    • 多种障碍类型
    • 动态变化的障碍物
  3. 高维扩展

    • 三维网格路径计数
    • 带时间维度的移动限制

通用动态规划解题框架:

  1. 定义状态表示(如f[i][j]
  2. 确定状态转移方程
  3. 处理边界条件
  4. 确定计算顺序(通常自底向上)
  5. 提取最终解

在实际项目中,这种思想可以应用于:

  • 机器人路径规划
  • 游戏AI中的移动决策
  • 网络路由算法
  • 金融中的路径依赖期权定价

理解了这个基础案例后,可以尝试解决更复杂的动态规划问题,如:

  • 背包问题
  • 最长公共子序列
  • 最短路径问题
  • 字符串编辑距离

棋盘上这个小卒的旅程,实际上是一次绝佳的计算思维训练——将复杂问题分解为可管理的子问题,逐步构建完整解决方案。这种思维方式的价值远超出这个具体问题本身。

http://www.jsqmd.com/news/948805/

相关文章:

  • 10分钟打造专属AI声音:RVC语音克隆完全指南
  • 2026 襄阳防水修缮|汉江汛期水位涨跌 + 鄂西喀斯特山体渗水 + 岗地地基沉降 + 老城预制板老楼冻融漏水|襄江修缮全域免费仪器测漏 - 苏易修缮
  • 让AI真正会干活:任务流建模四支柱实战指南
  • 基于Arduino的智能台灯DIY:环境感知与音乐律动灯光实现
  • 2026 年 6 月丹阳市防水维修甄选指南:卫生间免砸砖、屋顶阳台外墙地下室漏水检修避坑全攻略 - 吉修匠
  • 3步完成语雀文档批量导出:免费开源工具终极指南
  • .NET Win32设置只读未对齐,导致NTFS文件系统识别异常
  • 微信视频号直播弹幕实时监控解决方案:wxlivespy 助你全面掌握直播间互动数据
  • 2026年6月张家口黄金回收新手入门:从零搞懂怎么卖金才不吃亏 - 润富黄金回收
  • 别再乱试了!聊聊ETH私钥碰撞的真实原理与安全边界(附多链工具避坑指南)
  • 杭州市天加中央空调维修师傅电话|各区金牌师傅,靠谱选欧米到家 - 欧米到家
  • 从Hub到100G:一文搞懂以太网自协商的演进史与Clause 73的独特使命
  • 基于Arduino Pro Micro打造自定义快捷键键盘:从硬件到软件的完整指南
  • Python之rhinomcp包语法、参数和实际应用案例
  • 2026年论文党必备:盘点2026年行业天花板级的的AI论文平台
  • 2026年工业防护包装厂家选购指南:航空箱、铝箱、卡扣箱、出口木箱、航空托盘厂家选择指南,产能、工艺、品控三维度客观解析 - 海棠依旧大
  • 从考试失利到实战通关:手把手教你用Python实现遗传算法中的轮盘赌选择
  • 2026 年 6 月如皋市防水维修甄选指南:卫生间免砸砖、屋顶阳台外墙地下室漏水检修避坑全攻略 - 吉修匠
  • 别再死记硬背了!深入理解X-Forwarded-For和Referer:从CTF题到真实网络代理场景
  • 豆包2.0:从AI工具到生活操作系统的跃迁
  • 2026年6月天津全城卖金指南金价974元一克该出手了 - 润富黄金回收
  • 2026 武汉防水修缮|两江汛期顶托地下水 + 百湖环湖渗潮 + 梅雨高湿返霉 + 老城预制板老化渗漏|江城修缮全域免费仪器测漏 - 苏易修缮
  • 如何快速解决Dell G15散热问题:开源温度控制中心TCC-G15完全指南
  • 2026最新诚信优选 茂名粤西片区黄金铂金白银彩金回收合规商家TOP6排行榜+联系方式整理推荐 - 余生黄金回收
  • 为什么.net4.5+NModbus3.0.74连不上,换成3.0.83+.net4.8 连成功了
  • 5分钟终极指南:用KMS_VL_ALL_AIO快速搞定Windows和Office永久激活
  • 2026最新诚信优选 日照岚山区黄金回收白银回收铂金回收彩金回收靠谱门店TOP6排行榜+联系方式推荐 - 余生黄金回收
  • 2026年6月津达线缆联系方式厂家推荐,辽宁津达线缆/天津津达线缆/津达电线电缆,津达线缆联系方式公司联系方式是多少 - 品牌推荐师
  • 为什么这个鸿蒙 Flutter 项目把 AI、平台能力、业务逻辑分层放在 ‘core/’
  • 时空地理行业可信数据空间建设