迷宫寻径问题(数据结构4.4.3)

这里的迷宫寻径问题是针对邓公《数据结构》第四节的再学习的总结,刚开始学数据结构看到这里的时候确实有点看不太明白,现在整体学完后再回归学习一下。
这里的迷宫寻径过程仍然是基于试探回溯法的。其数据结构与函数的建立分两部分:

  1. 数据结构:先创建两个枚举,Status存单元cell的状态,ESWN存单元cell的方向。然后建立了Cell类,其创建的单元为cell,内部数据为xy坐标,当前状态,进入方向和出去方向。这里值得一提的是,在后面的运算过程中并没有对除起点外的其他cell的xy坐标进行改变,根据讨论区的助教意思,这里没有太多的必要来进行xy的计算,因为最终的路径可以通过stack的size或者倒推incoming得出来。
  2. 函数:函数分为三个,方向变换函数nextESWN(东南西北顺序改变,更新当前cell的outgoing),邻格查询函数neighbor(根据当前cell的outgoing进入下个cell,查询这个cell的状态),邻格转入函数advance(实质性的转入,根据当前cell的outgoing进入下个cell,并更新这个cell的incoming)

主要的迷宫寻径算法仍分为试探与回溯两部分,当然在一切开始之前需要先检查当前迷宫的起点和终点是否都是可行的,不然的话直接退出。这里的迷宫我使用了邓公的dsacpp中的迷宫构造函数,并把随机生成改为固定生成,便于调试。之后再将迷宫的起点导入,其状态设为ROUTE表示已经进入路径,输入改为UNKNOWN(因为确实是未知的),然后压入用于存储路径的stack path中。

  1. 试探:试探与回溯都在一个大循环中进行,首先判断当前的栈顶元素是否到达终点,如果到达则直接返回true即可。如果没有,就开始试探工作,即修改当前栈顶cell的outgoing(由于初始化为UNKNOWN,所以nextESWN的过程可以最多按东南西北顺序遍历完成),一旦发现下个节点使AVAILABLE的或当前节点已经是NO_WAY状态则可直接跳出了(这个发现的过程通过neighbor函数来实现)。倘若发现下个节点处于AVAILABLE状态,则将当前节点通过advance函数实质性的转过去,进行下一步循环。
  2. 回溯:回溯的过程发生在发现当前cell的四个outgoing方向的节点全部都无法进行,自己的状态变为NO_WAY时,此时则需要回溯当前cell,即将当前cell的状态改为BACKTRACKED,然后将栈顶元素删除,将新的栈顶(即原栈顶的下一位)元素最为下一步循环的节点。(这里指的一提的是,由于nextESWN函数的方向改变是首先执行的,所以不会出现回溯后的节点在一个方向上死循环)

具体的代码如下:

#include <iostream>
#include <stack>
#include <time.h>
using namespace std;

//现在所能得到的就是只能判断当前的迷宫是否能够找到出路,而最小路径由于概率不够随机的原因本人目前在DFS下暂时还没有实现

/*迷宫寻径主流的三大算法:广度/深度优先搜素算法,以及A*算法*/
/*相对而言,深度优先搜索是最适合迷宫出去路径寻径的,通过一步一步的试探和回溯,能很快找到一条出去的路*/
typedef enum { AVAILABLE, ROUTE, BACKTRACKED, WALL } Status;
typedef enum { UNKNOWN, EAST, SOUTH, WEST, NORTH, NO_WAY } ESWN;
inline ESWN nextESWN(ESWN eswn) { return ESWN(eswn + 1); }

static struct Cell
{
	int x, y; Status status;  //xy的坐标与类型
	ESWN incoming, outgoing;  //进入的方向与出去的方向
	Cell *prev;
};

#define LABY_MAX 40
static Cell laby[LABY_MAX][LABY_MAX];
static int ncheck, nback, length;

static inline Cell *neighbor(Cell *cell) //移动的探测,即得到当前cell的邻居,根据outgoing确定方向
{
	switch (cell->outgoing)
	{
	case EAST:return cell + LABY_MAX;
	case SOUTH:return cell + 1;
	case WEST:return cell - LABY_MAX;
	case NORTH:return cell - 1;
	default:exit(-1); //如果不是这四个方向,即UNKNOWN和NO_WAY,则直接退出这个switch循环
	}
}

static inline Cell* advance(Cell* cell)  //实质性的移动,根据cell的incoming移动当前cell到对应的cell
{
	Cell *next;
	switch (cell->outgoing)
	{
	case EAST:next = cell + LABY_MAX; next->incoming = WEST; next->x = cell->x + 1; break;  //这里的操作意思是,现节点的进入为西,即相当于原节点的出是东
	case SOUTH:next = cell + 1;		  next->incoming = NORTH; next->y = cell->y + 1; break;
	case WEST:next = cell - LABY_MAX; next->incoming = EAST; next->x = cell->x - 1; break;
	case NORTH:next = cell - 1;		  next->incoming = SOUTH; next->y = cell->y - 1; break;
	default: exit(-1);
	}
	return next;
}

static bool labyrinth(Cell Laby[LABY_MAX][LABY_MAX], Cell *s, Cell *t)
{
	if ((AVAILABLE != s->status) || (AVAILABLE != t->status)) return false;  //首先,起点和终点必须是能访问的
	stack<Cell*> path;  //栈中存放的都是指向cell单元的指针,这样对于栈的操作过程都是指针操作,能有效提升效率
	s->incoming = UNKNOWN; s->status = ROUTE; path.push(s);  //将起点的进入点设为无,然后状态设为在路径上,最后入栈
	do
	{
		Cell *c = path.top();  //c是指向栈顶元素的指针,用于处理当前栈顶的节点数据
		if (c == t)
		{
			length = path.size();
			return true;  //迷宫的最终条件,找到终点
		}
		while (NO_WAY > (c->outgoing = nextESWN(c->outgoing)))  //将c的出方向改为nextESWN枚举中的下个元素(未知,东南西北,无路)
			if (AVAILABLE == neighbor(c)->status) break;		//遍历c的各个邻居(东南西北方向),一旦有可行的就跳出,不然就循环
																//注意上面的循环终止条件,要么是邻居可走就跳出,要么就是走到了NO_WAY,也就是无路可走,所以跳出
																//同时注意,这里是while循环,回溯之后的cell过此段代码时,会先nextESWN到下一个方向,不会出现一个方向无限循环的情况
																//这里有个很有意思的想法,既然在检查方向,其肯定会检查到其incoming的方向,但是前面可以看到,只要走过的路都会标成ROUTE,所以不会干涉
		if (NO_WAY <= c->outgoing)  //说穿了,就是无路可走了,如同字面意思
		{
			c->status = BACKTRACKED;  //将当前的节点c,即对应的栈顶元素标记为BACKTRACKED,即已经走过但是试探全部失败回溯的点,类似于忒休斯的标志
			path.pop();  //栈顶元素出栈,但是cell c本质上还是存在的,没有删除。从实质上实现回溯
			nback++;
		}
		else
		{
			path.push(c = advance(c));   //将c根据前面试探可行的方向移动之后,将移动后的c入栈(此时的C已经是一个新的cell指针了,没有指向之前的栈顶元素了
			c->outgoing = UNKNOWN;  //新的c的出方向必然为未知
			c->status = ROUTE; //新的栈顶元素的标志改为ROUTE,表示进入路径试探了
			ncheck++;
		}
	} while (!path.empty());  //直到存储路径的path为空
	length = path.size();
	return false;  //如果循环内没有实现true的返回,代表起点到终点没有路,那么最终只能返回false了
}

/******************************************************************************************
*   输出某一迷宫格的信息
******************************************************************************************/
static void printLabyCell(Cell* elem)
{
	printf("%d -> (%d, %d) -> %d\n",
		((Cell*)elem)->incoming,
		((Cell*)elem)->x,
		((Cell*)elem)->y,
		((Cell*)elem)->outgoing);
}

static int labySize;  //此处借用dascpp中邓公的随机迷宫生成程序
static Cell* startCell;
static Cell* goalCell;
static void randLaby()
{
	labySize = LABY_MAX / 2 + rand() % (LABY_MAX / 2); //生成一个随机size的迷宫
	/*DSA*/printf("Using a laby of size %d ...\n", labySize);
	for (int i = 0; i < labySize; i++)
		for (int j = 0; j < labySize; j++)
		{
			laby[i][j].x = i;
			laby[i][j].y = j;
			laby[i][j].incoming =
				laby[i][j].outgoing = UNKNOWN;
			laby[i][j].status = WALL; //边界格点必须是墙
		}
	for (int i = 1; i < labySize - 1; i++)
		for (int j = 1; j < labySize - 1; j++)
			if (rand() % 4) laby[i][j].status = AVAILABLE; //75%的格点为空可用
	startCell = &laby[rand() % (labySize - 2) + 1][rand() % (labySize - 2) + 1];
	goalCell = &laby[rand() % (labySize - 2) + 1][rand() % (labySize - 2) + 1];
	startCell->status = goalCell->status = AVAILABLE; //起始格点必须可用
}

//这里同样借用的是邓公的迷宫显示代码
/******************************************************************************************
* 显示迷宫
******************************************************************************************/
static void displayLaby() { //┘└┐┌│─
	static char*   pattern[5][5] =
	{
		"┼", "┼", "┼", "┼", "┼",
		"┼", "  ", "┌", "─", "└",
		"┼", "┌", "  ", "┐", "│",
		"┼", "─", "┐", "  ", "┘",
		"┼", "└", "│", "┘", "  "
	};
	//system("cls");
	printf("  ");
	for (int j = 0; j < labySize; j++)
		(j < 10) ? printf("%2X", j) : printf(" %c", 'A' - 10 + j);
	printf("\n");
	for (int j = 0; j < labySize; j++)
	{
		(j < 10) ? printf("%2X", j) : printf(" %c", 'A' - 10 + j);
		for (int i = 0; i < labySize; i++)
			if (goalCell == &laby[i][j])
				printf("﹩");
			else
				switch (laby[i][j].status)
				{
				case WALL:  printf("█");   break;
				case BACKTRACKED: printf("○");   break;
				case AVAILABLE: printf("  ");   break;
				default: printf("%s ", pattern[laby[i][j].outgoing][laby[i][j].incoming]);  break;  
				//老师这里的代码%s后面没有空格,需要加上,不然迷宫会乱掉
				}
		printf("\n");
	}
}

int main()
{
	srand(int(time(0)));  //根据系统时间确定随机种子,保证每次执行都不同
	randLaby();
	if (labyrinth(laby, startCell, goalCell))
		cout << "true" << endl;
	else
		cout << "false" << endl;
	displayLaby();
	cout << "start: " << "(" << startCell->x << "," << startCell->y << ")"
		<< "  " << "end: " << "(" << goalCell->x << "," << goalCell->y << ")" << endl;
	cout << "check times: " << ncheck << " back times: " << nback << endl;
	cout << "length of path is " << length << endl;
	return 0;
}

最终的结果展示我借用了邓公的迷宫显示函数,这样更形象一些,具体如下:
程序运行结果
根据结果可以看到,对于一个16*16的随机迷宫,可以找到起点到终点的路径(true),其具体的路径如图所示,其中试探次数共68次,回溯次数20次(图中的O即为被回溯的节点),最终的路径长度为49。
从结果可以看到,邓公的这个算法可以实现找到一条从起点到终点的路径,但是似乎找到的路径并非是最短的路径,而关于这个最短的路径,我根据邓公的提示“一种简便而行之有效的策略是,每次都是按随机次序试探相邻格点。为此,需要改写nextESWN()函数(教材102页代码4.10)以及相关的数据结构。”尝试了一下,发现很难做到将nextESWN函数的每个节点的每次方向选值做到完全随机,最多只能做到每次运行时找到的路径都有所不同,但是路径的长度也有长有短。
所以对于邓公的这个算法如何改进,成为找到最短路径的做法,我目前已经没有了太好的思路,目前也已经在学堂在线的讨论区问了这个问题,不知道最终结果如何。

//1月23日下午更新
目前根据老师的数据结构在BFS下实现了最短路径的查找,通过结果的表现来看,本人现在认为对于最小路径的查找确实还是BFS算法更易于实现一些,其只需要一步一步的遍历,队列入队出队,到第一次出现OK的情况停止即可。当然有些情况下可能会有多种最优解,但是如果存在多种最优解,也可以在完成共级别的队列出入后实现再统计也可以。不仅如此,与广度优先搜索算法相关的迪克斯特拉算法也是对于带权网络的优秀解决算法。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方寸间沧海桑田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值