写在前面
这个文章适合初学者,我会以比较简单易懂的方法来讲解,同样我也是一个入门学者,也借这篇文章分享一些我的见解,如有疏漏和错误还请指出和谅解!让我们开始,这道题是在做洛谷的时候遇到的,题目名称为:P1002 [NOIP2002 普及组] 过河卒。原题如下:
一.使用DFS(深度优先搜索)
在这道题中,我们实际上可以遍历每一种可能的路线,直到走完所有可能的情况,然后计算有多少种情况最后到达了B点。为此,我们可以编写一个DFS函数,并递归调用。
void DFS(int x, int y) //x,y表示当前坐标
{
if (x == m && y == n) //如果当前坐标是目标点
{
result++; //路线增加一条
return; //回到上一次调用结束的地方
}
if (x + 1 <= m)DFS(x + 1, y); //如果不超x边界,那就右走
if (y + 1 <= n)DFS(x, y + 1); //如果不超y边界,那就下走
}
当然,这只是一个简单的示例函数,还没有考虑控制点的情况,而如果你并不了解递归,我们可以先把他简单理解成:函数调用其自身的操作。
我们假设从(0,0)开始,到(2,2)结束,不考虑控制点的情况,函数的执行实际上是这个样子的:
DFS(0,0) -> x+1
-> DFS(1,0) -> x+1
-> DFS(2,0) -> y+1
-> DFS(2,1) -> y+1
-> DFS(2,2) -> 回到(2,1),但是(2,1)已经执行完y+1,函数结束,所以再回到(2,0),再回到(1,0).
DFS(1,0)已经执行了x+1,所以执行y+1 -> DFS(1,1) -> 以此类推,就可以不重复的走完每一种可能的路线.
然后我们根据题目的要求,将马的控制点用一个数组表示出来。
bool vis[25][25];
vis[x][y] = 1;
vis[x - 2][y - 1] = 1;
vis[x - 2][y + 1] = 1;
vis[x + 2][y - 1] = 1;
vis[x + 2][y + 1] = 1;
vis[x - 1][y + 2] = 1;
vis[x - 1][y - 2] = 1;
vis[x + 1][y + 2] = 1;
vis[x + 1][y - 2] = 1;
不过观察我们的这个数组,其可能有一些问题,因为如果x=1,或者y=1的情况下,数组会出现越界访问的情况,所以我们可以把整个坐标系进行平移,即xymn同时自增,这样相对位置就没有改变。
综上所述,我将给出全部代码:
#include<iostream>
using namespace std;
int result = 0;
int x, y, m, n;
bool vis[25][25]; //二维数组表示控制点
void DFS(int x, int y) //x,y表示当前坐标
{
if (x == m && y == n)
{
result++;
return;
}
if (x + 1 <= m && vis[x+1][y] != 1)DFS(x + 1, y); //判断是否超过目标点x值和是否是控制点
if (y + 1 <= n && vis[x][y+1] != 1)DFS(x, y + 1); //判断是否超过目标点y值和是否是控制点
}
int main()
{
cin >> m >> n >> x >> y;
//将坐标系整体平移
m++;
n++;
x++;
y++;
//标记控制点
vis[x][y] = 1;
vis[x - 2][y - 1] = 1;
vis[x - 2][y + 1] = 1;
vis[x + 2][y - 1] = 1;
vis[x + 2][y + 1] = 1;
vis[x - 1][y + 2] = 1;
vis[x - 1][y - 2] = 1;
vis[x + 1][y + 2] = 1;
vis[x + 1][y - 2] = 1;
DFS(1, 1); //调用DFS函数,从1,1开始,因为刚刚平移过,函数会遍历所有到m,n的路径
cout << result; //输出结果
}
但是这样有一个很严重的问题,我们知道(假设你知道,如果你不知道,那么你现在也知道了):如果在一个二维坐标系上,从(0,0)出发去(x,y),那么总共有Choose m/m+n种最短的路径,而对于一个m*n大小的棋盘,其可能的路径也有2的m+n次方种。
假设不算控制点,我们的时间复杂度也可以近似是:O(2^(m+n)),当马的坐标取最大值(20,20)的时候,我们的时间复杂度近似是2^40,可见,即使马的坐标没有足够大,我们的程序也会花掉相当多的时间,当然,在这道题中,使用DFS算法只能得60分,超时在所难免,由此,我们需要更加高效的算法。
二.使用DP(动态规划)
动态规划DP,在这道题中是一种更简单高效的算法,我们知道,题目说,我们的士兵只能走右侧和下侧,我们可以写出动态规划转移方程:
也就是说,我们如果想去一个点x,y那么,可能的路径个数就是去左侧的点,和上面的点的路径个数的和,因为士兵只能走右侧和下侧。
为此,根据我们的转移方程,可以直接锁定最关键的代码:
ways_to[i][j] = ways_to[i - 1][j] + ways_to[i][j - 1];
我们直接编写一个循环,将i和j分别递增到m和n,这样就可以直接得到ways_to[i][j]的值。
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if ((i != 1 || j != 1) && !vis[i][j])
ways_to[i][j] = ways_to[i - 1][j] + ways_to[i][j - 1];
注意,这里的判断条件,i和j不能同时为1(即在原点),因为如果给原点的路径赋值,那就是0+0=0,而实际上,去原点本身的路径是必有1条的,而且这1条路径,是我们能继续往下计算的关键。
其他部分则与DFS别无二至,请看全部代码:
#include <iostream>
using namespace std;
bool vis[25][25];
long long ways_to[25][25];
int main()
{
int m, n, x, y;
cin >> m >> n >> x >> y;
m++;
n++;
x++;
y++;
vis[x][y] = 1;
vis[x - 2][y - 1] = 1;
vis[x - 2][y + 1] = 1;
vis[x + 2][y - 1] = 1;
vis[x + 2][y + 1] = 1;
vis[x - 1][y + 2] = 1;
vis[x - 1][y - 2] = 1;
vis[x + 1][y + 2] = 1;
vis[x + 1][y - 2] = 1;
ways_to[1][1] = 1;//去原点的路径
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if ((i != 1 || j != 1) && !vis[i][j])
ways_to[i][j] = ways_to[i - 1][j] + ways_to[i][j - 1];
cout << ways_to[m][n];
return 0;
}
再次运行代码,就可以直接AC了,分析时间复杂度,则很容易算出是O(m*n),即使m和n都是最大值20,计算也只有400次,相比我们刚刚DFS的指数阶时间复杂度,效率明显高了不少。