1. 问题分析与算法设计思路
过河卒问题是一个经典的动态规划题目,它模拟了中国象棋中卒子的移动规则,并加入了马的控制点限制条件。这个问题的核心在于计算从起点到终点的所有可能路径数,同时避开被对方马控制的格子。
1.1 问题建模
我们可以将棋盘看作一个二维坐标系,其中:
- 起点A固定在(0,0)
- 终点B在(n,m)
- 对方马的位置在(ax,ay)
卒子的移动规则是只能向右或向下移动,而马的控制点包括马本身的位置以及马一步能跳到的8个位置(中国象棋中马的走法是"日"字形)。
1.2 动态规划思路
解决这个问题的关键在于建立状态转移方程。我们定义f[i][j]表示从起点(0,0)到达点(i,j)的路径数量。根据卒子的移动规则,可以得到以下状态转移方程:
f[i][j] = f[i-1][j] + f[i][j-1]
当然,这个方程的前提是(i,j)不是马的控制点,且i和j都不为0(边界情况需要特殊处理)。
2. 算法实现细节
2.1 数据结构设计
我们需要两个二维数组:
- vis数组:标记马的控制点,1表示控制点,0表示非控制点
- f数组:存储到达每个点的路径数
cpp复制vector<vector<long long>> f(m+1, vector<long long>(n+1)); // 路径数数组
vector<vector<long long>> vis(m+1, vector<long long>(n+1)); // 控制点标记数组
使用long long类型是为了防止大数溢出,因为当n和m较大时(接近20),路径数可能非常大。
2.2 马控制点的计算
马可以移动到8个方向的位置,我们需要检查这些位置是否在棋盘范围内:
cpp复制vector<int> dx = { -2,-2,-1,-1,1,1,2,2 }; // 马移动的x方向
vector<int> dy = { -1,1,2,-2,2,-2,1,-1}; // 马移动的y方向
vis[ay][ax] = 1; // 马本身的位置
for (int i = 0; i < 8;i++) {
long long cx = ax + dx[i];
long long cy = ay + dy[i];
if (cx >= 0 && cx <= n && cy >=0 && cy <= m) {
vis[cy][cx] = 1;
}
}
2.3 边界初始化
边界情况(第一行和第一列)需要特殊处理,因为它们只能从一个方向到达:
cpp复制// 初始化第一行
for (int i = 1; i <= n; i++) {
f[0][i] = f[0][i - 1];
if (vis[0][i] == 1) {
f[0][i] = 0;
}
}
// 初始化第一列
for (int i = 1; i <= m; i++) {
f[i][0] = f[i-1][0];
if (vis[i][0] == 1) {
f[i][0] = 0;
}
}
2.4 动态规划填表
填充剩余格子的路径数:
cpp复制for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
if (vis[i][j] == 1) {
f[i][j] = 0;
}
}
}
3. 算法优化与注意事项
3.1 空间优化
虽然上述实现使用了O(nm)的空间,但实际上可以优化到O(n)或O(m)的空间复杂度,因为计算每个格子时只需要左边和上边的格子信息。
3.2 边界条件处理
需要特别注意以下几种特殊情况:
- 起点或终点本身就是马的控制点
- 马的控制点导致路径被完全阻断
- n或m为0的情况
3.3 大数处理
当n和m较大时(接近20),路径数可能非常大(最大可达C(40,20)=137846528820),因此必须使用足够大的数据类型(如long long)。
4. 完整代码实现
cpp复制#include<bits/stdc++.h>
using namespace std;
int main() {
int n, m, ax, ay;
cin >> n >> m >> ax >> ay;
// 初始化方向数组
vector<int> dx = { -2,-2,-1,-1,1,1,2,2 };
vector<int> dy = { -1,1,2,-2,2,-2,1,-1};
// 初始化动态规划数组
vector<vector<long long>> f(m+1, vector<long long>(n+1, 0));
vector<vector<bool>> vis(m+1, vector<bool>(n+1, false));
// 标记马的控制点
vis[ay][ax] = true;
for (int i = 0; i < 8; i++) {
int cx = ax + dx[i];
int cy = ay + dy[i];
if (cx >= 0 && cx <= n && cy >= 0 && cy <= m) {
vis[cy][cx] = true;
}
}
// 初始化起点
f[0][0] = vis[0][0] ? 0 : 1;
// 初始化第一行
for (int j = 1; j <= n; j++) {
if (!vis[0][j]) {
f[0][j] = f[0][j-1];
}
}
// 初始化第一列
for (int i = 1; i <= m; i++) {
if (!vis[i][0]) {
f[i][0] = f[i-1][0];
}
}
// 动态规划填表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (!vis[i][j]) {
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
}
cout << f[m][n] << endl;
return 0;
}
5. 常见问题与调试技巧
5.1 数组越界问题
在计算马的控制点时,必须检查坐标是否在有效范围内:
cpp复制if (cx >= 0 && cx <= n && cy >= 0 && cy <= m)
5.2 初始化顺序问题
必须先标记所有马的控制点,然后再进行动态规划表的初始化,否则可能导致错误。
5.3 数据类型选择
路径数增长非常快,必须使用足够大的数据类型(如long long),否则会导致溢出错误。
5.4 测试用例设计
建议设计以下测试用例进行验证:
- 马的控制点阻断所有路径的情况
- 起点或终点是马的控制点的情况
- 小规模棋盘(如2x2)的简单情况
- 最大规模(20x20)的情况
6. 算法扩展与变种
6.1 多马控制点问题
如果有多个马的控制点,只需要将所有马的控制点都标记出来即可,算法框架不变。
6.2 不同移动规则
如果卒子的移动规则改变(如可以斜着走),只需要调整状态转移方程即可。
6.3 路径输出问题
如果需要输出所有具体路径而不仅仅是计数,可以使用回溯法来实现,但要注意时间复杂度会大大增加。
在实际编程竞赛中,过河卒问题是一个很好的动态规划入门题目,它涵盖了动态规划的基本思想:将大问题分解为子问题,建立状态转移方程,然后通过填表法自底向上解决问题。理解这个问题的解法对于掌握更复杂的动态规划问题非常有帮助。