题目:[NOIP 2002 普及组] 过河卒
题号:P1002
难度:普及一
该文章记录关于解决方案的整个思路,想要解决答案,请直接滑到底端。
愚人节快乐
题目分析
这道题,大致来看还是挺令人迷惑的,
每次只能向下或者向右走一格的距离,且路径中存在若干个阻挡点,
最后统计出能到达目标点的路径数。
引入大致思路
这是我第首先想到的思路。
左边的绿色点是起点相对马的可能的位置,
右边和下边的绿色点是终点相对马的可能位置。
看似马口所占领的八个点,实际上由于卒只能向右或向下走,
则图中的红点也是默认无法抵达的点,可以去除,这样的话。
原本的通路就变成了:
马控区域的上路,
马控区域的上半条路,
马控区域的下半条路,
马控区域的下路,
然后对于外面的四条蓝色边框区域分别求路径,
且黄色点可选择是否进入,进入的话,就分别对应一个蓝点道路,
中间是没有选择的,
除此之外再额外考虑两个橙色点(左下橙色点,只能由左边进入,右上橙色点只能由上边进入)
说的轻巧,直接求各部分路径,但是路径怎么求?
其实只要仔细看不难发现,我把外面的蓝色区域分成的都是矩形,且中间不包含阻挡点,
关于矩形坐标对角点相隔的路径,我们是有方法的。
矩形路径
在该4*4的矩形表格中从左上顶点到右下顶点,应该有多少中可能的路径 ,
首先,肯定要向右走4步,向下也走4步,那么就是8步。
且每次不是向下走就是向右走,则如果挑出向右走或者向左走的步数,
就反向也得到另一种走法的步数,但是我们已经说了需要向右和向下各4步,
但是不知道具体哪一步向下或向右,
则对于这个问题,使用数学排列组合知识,C (x+y, x) ,
即在m+n步找出具体的m步或n步,当然由数学知识可知,无论向下还是向右,
知道其一便可得其二,C (x+y, x) = C (x+y, y);
如果写成运算形式就是(x + y)! / (x! * y!)。
初始基本思路
那这样看的话,矩形区域的路径就解决好了,
而还需要面临的问题就是剩下的极少数的矩形外的区域,
和重合矩形的重叠问题,以及走到黄点是否拐弯的问题。
其中通过黄点进入区域的问题最好解决,因为如果通过该路径的话,那么
路径数 = 起点走到黄点的路径 * 蓝点走到终点的路径。
关于第二个问题,面临的问题就是剩下的极少数的矩形外的区域,
也就是图中两个橙色点,不难发先,两个橙色点均只有一个通过方法,
(左下角的橙色点左近下出,右上角的橙色点上进下出)
这样的话也就是和前一个问题一样了,
路径数 = 起点到进入橙色的路径 * 从橙色点出来走到终点的路径。
从两个橙色点出来之后走向终点的范围都是矩形区域,这样便实现了全矩形区域的计算。
此时面对的就仅剩下一个问题,就是处理减去矩形重合计算的部分。
但是这本身就是一个动态规划的问题,该方法对于该题过于麻烦,先不用动态规划,
在此方法的基础上优化一下。
初始基本思路的重新构建的优化
我刚开始提到过的就是分成各个矩形,然后分别求矩形的路径,现在我们退一步。
将起点到终点的全部区域当成一个矩形,假设没有阻挡点,然后计算路径。
很显然n*m的矩形区域的路径数 = C (n+m, n) = (n + m)! / (n! * m!);
引入阻挡点
当存在一个阻挡点时,我们需要做的时减去经过该阻挡点的全部路径数,就得到了有效路径数。
计算方法正如前面所提到的 该点前路径 *该点后路径。
也就是 C (n1+m1, n1) * C (n2+m2, n2) 其中n1 + n2 = 总长n m1 + m2 = 总宽m。
这样的话,最终目标有效路径就是C (n+m, n) - C (n1+m1, n1) * C (n2+m2, n2);
(n + m)! / (n! * m!) - [(n1+ m1)! / (n1! * m1!) ] * [ (n2 + m2)! / (n2! * m2!) ]
便得到了最终的计算公式,但是我们面临的点并非是单个的阻挡点,
假设有两个阻挡点 ,便需要计算同时经过两条阻挡点的路径,便需要分成三个概念矩形计算,还有仅仅经过某个阻挡点的次数,还要再被减去,这样一直套娃的形式,虽然不难,但是计算量和时间复杂度随着阻挡点的增加而急剧增加,显然不符合接解题的要求,所以该思路适用于阻挡点较少的情况,我们不得步换别的方法。
从此行开始,存在某些代码步骤来源于AI,本章下半部分我结合AI代码和思路仅作解题总结。
动态规划
动态规划通常是将问题分解为子问题,通过保存子问题的解来避免重复计算。
在这个问题里,每个点的路径数依赖于其上方和左方的点,所以适合用动态规划。
需要从状态转移方程入手,说明每个点的路径数是如何由前一步推导出来的。
同时,要强调阻挡点的处理方式,即如果某个点被阻挡,那么它的路径数为 0,并且不会对后续的点产生贡献。
起点如果被阻挡,直接返回 0。否则,起点的路径数是 1。按行或列的顺序遍历每个点,确保在计算当前点时,上方和左方的点已经被处理过
动态规划的核心是状态转移。对于网格中的每个点 (i,j)
,其路径数等于从上方 (i-1,j)
和左方 (i,j-1)
到达此处的路径数之和。
如果某点被阻挡,则其路径数为 0,且不会对后续点产生贡献。
简要来说:每个点都只有两种形式到达,(从上面位置进入,从左边位置进入)
则 到达某点的路径数 = 从上面进入的路径数 + 从左边进入的路径数;
而前一个点仍然满足这个规律,这时便涉及到状态转移
dp[i][j] = dp[i-1][j] + dp[i][j-1]
(当 (i,j)
未被阻挡时)
简单说明一下大致步骤
1. 初始化 DP 数组
创建一个 m x n
的二维数组 dp
,初始值为 0。检查起点 (0,0)
是否被阻挡:若阻挡,直接返回 0(无法出发)。否则,dp[0][0] = 1
。
储存马的位置
const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
2. 遍历网格
使用双重循环遍历每个点 (i,j)
,从左上到右下。
顺序说明:
先处理上方和左方的点,确保计算 dp[i][j]
时,dp[i-1][j]
和 dp[i][j-1]
已计算完成。
3. 处理阻挡点
- 若当前点
(i,j)
被阻挡:将dp[i][j]
设为 0,表示无法到达此处。跳过后续计算,因为该点不会贡献路径。
4. 状态转移
- 若当前点未被阻挡:上方贡献:若
i > 0
,则dp[i][j] += dp[i-1][j]
。左方贡献:若j > 0
,则dp[i][j] += dp[i][j-1]
。
5. 结果输出:最终结果为 dp[m-1][n-1]
,即右下角终点的路径数。
源代码
#include <stdio.h>
#define MAX 25
int main() {
int n, m, cx, cy;
scanf("%d %d %d %d", &n, &m, &cx, &cy);
int block[MAX][MAX] = {0};
// 标记马的位置及其控制点
block[cx][cy] = 1;
int dx[] = {1, 1, -1, -1, 2, 2, -2, -2};
int dy[] = {2, -2, 2, -2, 1, -1, 1, -1};
for (int i = 0; i < 8; i++) {
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx >= 0 && nx <= n && ny >= 0 && ny <= m) {
block[nx][ny] = 1;
}
}
long long dp[MAX][MAX] = {0};
if (!block[0][0]) {
dp[0][0] = 1;
}
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (block[i][j]) continue;
if (i > 0) dp[i][j] += dp[i - 1][j];
if (j > 0) dp[i][j] += dp[i][j - 1];
}
}
printf("%lld\n", dp[n][m]);
return 0;
}
这个方法好理解,但是上手并不好写,当然这道题本身就是属于动态规划的典型题,肯定要用动态规划来解题。关于动态规划,有点类似于数学中的斐波那契数列,看来也是可以用递归来解的。将复杂问题(如路径计数)拆解为可递归解决的子问题
写完文章之后提升还是很多的,现在再回头看看本道题刚开始的思路,虽然不是本题合适的解决办法,但是还是有一定的思路基础的,写进笔记就当作昙花一现的果实吧。
也是快四点了,晚安。
当我以为这道题彻底结束的时候,
我又欣赏到了洛谷某位大神的优化方法,
由于版权问题,在本篇文章不做提及,感兴趣的朋友可以通过下述链接查看:题解 P1002 【过河卒】 - 洛谷专栏https://www.luogu.com.cn/article/bgami1gy