目录

常用算法深度优先搜索Depth-First-Search-

目录

常用算法:深度优先搜索(Depth First Search )

最近事少,代码也写的少,当然工作这么些年写过的代码也能绕xx四五圈啊四五圈,现在则思考除了纯粹写代码之外的事情,比如总结一些常用的通用的算法,学习一下遗忘很久的数学,思考下今后的发展,比如是在哪个桥洞下讨饭什么的。

实际上呢,很多数学和算法,我们的开发生涯中都是使用过的,但是记忆力这个东西是不可靠的,我小时候看过一本叫XYZ记忆法和记忆宫殿的书,里面讲的人的“永久记忆”也就那么二十年,而且记忆的内容需要七次及以上的反复学习才可以达到所谓的“永久记忆”,大部分情况下就是我们用到某个算法,但是并没有持续对该算法进行七次及以上的学习,可能就会导致我们突然想用某个算法的时候,就“卡壳”了。

比如这次要讲的深度优先搜索(后面简称DFS),做游戏或者其他开发,比如数据挖掘什么的,基本上都会接触和实现过,但是某些年后突然准备再用这个算法,还真可能需要再看一遍,所以现在我就准备业余时间再次学习理解实现一遍。

DFS在游戏中有一个应用场景就是寻路,也就是玩家给角色指定一个目标地点,角色就移动过去,这个移动的过程就涉及以下技术细节:

①.美术制作好场景地形,根据地形网格的顶点和三角面进行标定,确定哪些三角面是“平坦畅通”的,哪些三角面是“障碍物”,策划根据实际情况进行表格记录,将这些信息储存起来给程序使用。当然了这一个步骤程序完全可以自己处理,我们开发可以对网格三角面顶点检测,将顶点的“海拔Y轴”比较大或者比较小(也就是比较“高”或者比较“低”)的三角面标记为“凹凸物不可通过”,将Y轴差异性比较小的三角面标记为“平坦可通过的”。这个步骤一般引擎都帮我们做好了,叫做Navigation agent,一般引擎都自带这种功能。

②.当然也有Q游,场景比较卡通简洁,而且比较规整,可能一个场景完全就可以进行横纵二维数组标定区域可通过性。

③.当场景的信息标定完毕,使用DFS算法进行目标移动路径处理,找到目标路程。

④.角色进行行走动画和位移插值旋转插值等处理,表现出良好的寻路效果。

这里我们只来观察学习③中的DFS路径计算技术,假设我们拥有下面一张Q版游戏地图,如下图:

https://i-blog.csdnimg.cn/blog_migrate/8b110568c8ae251a8ce088e5d6e96726.png

这是我用ps随便做的,不要介意。

假如我们的“羊羹君”想吃到右下角的小鱼干,但是地图上到处都是水坑和路障,请问它要怎么走到目的地呢,这里顺便说一下“羊羹君”是一只年迈的老猫,它的智力已经和计算机差不多了(这里也别奇怪,计算机的“智力”是很低的,只是运算速度很快),只能“走一步看一步”, 羊羹君的行走方式就是先往右走,被挡住了就往下走,再被挡住了就往左走,还是被挡出了就往上走 ,因为“羊羹君”年纪太大了,现在行走方式就跟一个上了发条的机器人一样,只能这样撞一下换个方向(・。・),直到找到小鱼干,或者永远找不到小鱼干。

这时我们就要帮助“羊羹君”找到它的小鱼干!我们首先要将地图的信息进行二维数组标定储存,然后“模拟”“羊羹君”年迈的行走方式,进行路线推算,因为我们是在计算机中进行的,所以可以很快的得到结果,直接帮助”羊羹君“确定行走线路,具体程序实现如下:

// CatDFS.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<Windows.h>
#include<map>

using namespace std;

//6行6列地图
const int Row = 6;
const int Col = 6;

//将Q版地图用二维数组进行标定,0代表畅通无阻,1代表路障,-1代表深水坑
int Qmap[Row][Col] = { 0,0,0,0,1,0,
				 0,0,-1,-1,0,0,
				 0,1,0,0,0,0,
				 0,0,0,-1,0,0,
				 0,0,0,1,0,0,
				 0,0,0,0,0,0 };

//这是一个和map相同容积的标记数组,作用是标记羊羹君已经走过的坐标,避免重复行走
//因为假设我们不使用一个额外的数组储存羊羹君的行走历史记录,那么可能会存在重复行走的死循环
int tag[Row][Col] = { 0 };


//记录羊羹君走过的坐标,使用索引:坐标 map记录
class Coord
{
public:
	Coord() {};
	Coord(int i, int j) :x(i), y(j) {};
	~Coord() {};
	int x;
	int y;

};
map<int, Coord> pathMap;
int pathIndex = 0;	

//记录线路步数
int totalStep = 0;

//羊羹君的移动方式,也就是“碰壁式移动”,先右走,再下走,再左走,在上走
//相应的坐标计算规则则是如下
int nextMove[4][2] = { {0,1},		//坐标右移
					{1,0},		//坐标下移
					{0,-1},		//坐标左移
					{-1,0} };	//坐标上移

//深度优先搜索算法
//因为羊羹君每走一步,都会环顾四周,观察下一步可以走哪里,所以可以递归进行
//参数首先确认当前走到了二维数组的哪个位置,也就是xy
//参数同时确认当前已经走了多少步了,方便我们找到最短路线
void Dfs(int x, int y, int step)
{
	//当我们到达[5,5]坐标时,代表我们已经找到小鱼干了
	//这是我们就打印出来羊羹君走了多少步
	//同时将羊羹君每次行走的坐标示意图也打印出来
	if (x == (Row - 1) && y == (Col - 1))
	{
		//记录最短路线步数
		if (totalStep < step)
		{
			totalStep = step;
		}
		printf("conguratulation 羊羹君 find the fish totalStep = %d\n", totalStep);
		for (int i = 0; i < Row; i++)
		{
			for (int j = 0; j < Col; j++)
			{
				printf("%d ", tag[i][j]);
			}
			printf("\n");
		}
		for (map<int, Coord>::iterator it = pathMap.begin(); it != pathMap.end(); it++)
		{
			printf("index = %d x=%d y=%d\n", it->first, it->second.x, it->second.y);
		}
		return;
	}
	int tx, ty;
	for (int i = 0; i < 3; i++)
	{
		//tx,ty表示下一步的二维坐标值
		tx = x + nextMove[i][0];
		ty = y + nextMove[i][1];
		//如果下一步坐标越界了,就直接跳过
		if (tx < 0 || tx >= Row || ty < 0 || ty >= Col)
		{
			continue;
		}
		//如果下一步坐标在地图map中标记为0通畅,同时tag标记数组表示这个坐标没有走过
		//那么我们就表示这一步行走正确,同时进行递归DFS
		if (Qmap[tx][ty] == 0 && tag[tx][ty] == 0)
		{
			tag[tx][ty] = 1;			//先标记tag数组,表示这个坐标已经行走过了,避免重复
			pathMap[pathIndex] = Coord(tx, ty);  //记录坐标map
			++pathIndex;
			Dfs(tx, ty, step + 1);		//然后进行DFS递归
			//如果DFS递归失败(没找到小鱼干,且这条路走堵死了)
			//那么回滚tag数组标记这个坐标没有行走过,并且清理路线中的坐标
			//同时继续for循环进行下一个方向的探索
			tag[tx][ty] = 0;	
			--pathIndex;
			pathMap.erase(pathIndex);
		}
	}
}


int main()
{
	for (int i = 0; i < Row; i++)
	{
		for (int j = 0; j < Col; j++)
		{
			printf("%d ", Qmap[i][j]);
		}
		printf("\n");
	}
	tag[0][0] = 1;
	pathMap[pathIndex] = Coord(0, 0);
	++pathIndex;
	Dfs(0, 0, 0);
    return 0;
}

代码如上图,进行了很详细的注释,核心函数Dfs是一个递归处理的函数,因为“羊羹君”每走一步都是进行“碰壁式”移动,所以可以将每一步的Dfs进行递归处理,同时判断好成功条件。我们调试运行上面的代码,如下图:

https://i-blog.csdnimg.cn/blog_migrate/e75d0585d3db6f37d882fbd346819c31.png

第一次我们行走了12步,路径字典中一共13个坐标记录。

当然我们的Dfs函数是1分4,4分16探索方式的,因为Dfs后面的for(i++;i<3)循环,我们找到第一条路return后,函数会自动寻找其他路线,如果我们不break循环的话,那么继续运行,就会出现后面的路径,如下图:

https://i-blog.csdnimg.cn/blog_migrate/ef4fe2ba90b4bfed1ae0bc408add97ca.png https://i-blog.csdnimg.cn/blog_migrate/52fc315ba10a89caa8ed7cbb55f281fc.png

当然DFS深度优先搜索的特点就是第一次寻找的线路是最优线路,也就是最短的路径,因为DFS就是“碰壁式”寻路,就跟盲人摸着墙壁走路一样,如果这个“盲人”这摸那摸,走的路线就最远了。

当然这里要注意,这个线路只是“年迈的羊羹君”行走的最短线路,并不是实际上的最优化线路 ,大家可以观察一下,“年迈的羊羹君”在(0,0) (0,1) (1,0) (1,1)这四个位置都走了一遍,而不是直接跨过到达(2,0),这就是因为羊羹君nextMove“碰壁式”移动导致的,这就是DFS函数的最大缺点。

那么有没有其他算法能解决这个“最优线路”的问题呢?当然有,只是相比DFS深度优先搜索更加耗费时间而已,DFS的优点就是速度快,一次探索寻路完毕,而我们想得到”最优线路“,则需要地图全局探索(当然就会带来更大的时间消耗),后面马上就开始学习讲解。

so,我们接下来继续。