5分钟用一道典例教你学会DFS与DP

写在前面

这个文章适合初学者,我会以比较简单易懂的方法来讲解,同样我也是一个入门学者,也借这篇文章分享一些我的见解,如有疏漏和错误还请指出和谅解!让我们开始,这道题是在做洛谷的时候遇到的,题目名称为: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,在这道题中是一种更简单高效的算法,我们知道,题目说,我们的士兵只能走右侧和下侧,我们可以写出动态规划转移方程

                                        f(x,y)=f(x-1,y)+f(x,y-1)

也就是说,我们如果想去一个点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的指数阶时间复杂度,效率明显高了不少。

感谢阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值