1. 问题背景与题目解析
"过河卒"是NOIP(全国青少年信息学奥林匹克联赛)2002年普及组的一道经典动态规划题目。题目描述了一个象棋棋盘上的卒子从起点到终点的路径规划问题,其中包含了一些限制条件。
象棋中卒子的移动规则是:每次只能向前(向上)移动一格,或者向右移动一格。题目在此基础上增加了"马"的阻挡点——即象棋中马能走到的"日"字形位置,这些位置卒子不能经过。我们需要计算卒子从棋盘左下角(0,0)移动到右上角(n,m)的所有可能路径数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题建模与算法选择
2.1 棋盘表示与状态定义
我们可以用一个二维数组dp[i][j]来表示走到(i,j)位置的路径总数。初始状态dp[0][0]=1,因为起点只有一种方式(就是不动)。
对于普通位置(i,j),其路径数等于从左边来的路径数加上从下面来的路径数:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
2.2 障碍点处理
马的控制点(障碍点)需要特殊处理。对于每个障碍点,我们将其dp值设为0,表示不能经过该点。具体来说,如果(i,j)是障碍点,则dp[i][j]=0。
马的控制点共有8个位置,相对于马的位置(x,y):
(x+1,y+2)
(x+1,y-2)
(x-1,y+2)
(x-1,y-2)
(x+2,y+1)
(x+2,y-1)
(x-2,y+1)
(x-2,y-1)
2.3 边界条件处理
对于第一行(i=0)和第一列(j=0)需要特殊处理:
- 第一行只能从左向右移动
- 第一列只能从下向上移动
- 如果路径上有障碍点,则后面的点都不可达
3. 动态规划实现细节
3.1 初始化阶段
首先初始化dp数组全为0,然后设置起点dp[0][0]=1。接着标记所有障碍点的dp值为0。
3.2 递推计算
按照行优先或列优先的顺序遍历整个棋盘,对每个非障碍点(i,j):
- 如果不是第一行(i>0),则加上dp[i-1][j]
- 如果不是第一列(j>0),则加上dp[i][j-1]
3.3 代码实现示例(C++)
cpp复制#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m, x, y;
cin >> n >> m >> x >> y;
vector<vector<long long>> dp(n+1, vector<long long>(m+1, 0));
dp[0][0] = 1;
// 标记马的控制点
vector<vector<bool>> block(n+1, vector<bool>(m+1, false));
int dx[] = {1,1,-1,-1,2,2,-2,-2};
int dy[] = {2,-2,2,-2,1,-1,1,-1};
block[x][y] = true;
for(int i=0; i<8; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if(nx>=0 && nx<=n && ny>=0 && ny<=m) {
block[nx][ny] = true;
}
}
// 动态规划计算
for(int i=0; i<=n; i++) {
for(int j=0; j<=m; j++) {
if(block[i][j]) {
dp[i][j] = 0;
continue;
}
if(i==0 && j==0) continue;
if(i>0) dp[i][j] += dp[i-1][j];
if(j>0) dp[i][j] += dp[i][j-1];
}
}
cout << dp[n][m] << endl;
return 0;
}
4. 算法优化与注意事项
4.1 空间优化
由于dp[i][j]只依赖于左边和下面的值,可以将二维数组优化为一维数组,节省空间:
cpp复制vector<long long> dp(m+1, 0);
dp[0] = 1;
for(int i=0; i<=n; i++) {
for(int j=0; j<=m; j++) {
if(block[i][j]) {
dp[j] = 0;
continue;
}
if(i==0 && j==0) continue;
if(j>0) dp[j] += dp[j-1];
}
}
4.2 大数处理
当n和m较大时(如20以上),路径数会非常大,可能超过int范围。因此需要使用long long类型存储结果。
4.3 边界检查
需要特别注意马的控制点是否在棋盘范围内,避免数组越界访问。
5. 常见错误与调试技巧
5.1 初始化错误
常见错误包括:
- 忘记初始化起点dp[0][0]=1
- 忘记将障碍点的dp值设为0
- 数组大小设置不正确(应该是n+1和m+1)
5.2 递推顺序错误
必须确保在计算dp[i][j]时,dp[i-1][j]和dp[i][j-1]已经被计算过。因此应该按照i从0到n,j从0到m的顺序遍历。
5.3 测试用例设计
建议设计以下测试用例:
- 小棋盘无马(如2x2)
- 马在起点或终点
- 马的控制点挡住所有路径
- 大棋盘测试(如10x10)
6. 算法扩展与变种
6.1 多条障碍路径
可以扩展为有多个马的情况,只需将所有马的控制点都标记为障碍即可。
6.2 不同移动规则
可以改变卒子的移动规则,比如允许斜向移动,此时状态转移方程需要相应调整。
6.3 求具体路径
如果需要输出所有具体路径而不仅仅是计数,则需要使用回溯法而非动态规划。
7. 实际应用与思维训练
这道题目虽然场景简单,但很好地训练了以下几个方面的能力:
- 将实际问题抽象为数学模型的能力
- 动态规划的状态设计和转移方程构建
- 边界条件的处理能力
- 代码实现的细节把控
在实际编程竞赛中,类似的网格路径问题非常常见,掌握这类问题的解法对提高竞赛成绩很有帮助。
