迷宫最短路径问题(数据结构4.4.3 拓展)

这是对于邓俊辉老师的《数据结构》一书的4.4.3节的迷宫最短路径算法的自我拓展。在上一篇博客中,我提到本人用DFS算法尝试不出最短路径的解决方法,所以在此篇博客中,本人采用了BFS算法,并结合建图的方式求出了迷宫的最短路径解决方法。
对于BFS的迷宫最短路径问题,其整体的数据结构Cell、ESWN、Status与上篇博客的DFS基本相同,不过有两者略有区别:

  1. Status枚举中的BACKTRACKED:Status中的BACKTRACKED在本程序中当做遍历过的cell,而非回溯的cell;
  2. Cell类:在增加一个prev的数据成员,表示当前节点的前缀,用于建图与反推;
  3. advance函数:增加了想x与y坐标的计算,因为本例中要输出相关坐标了。

而迷宫相关的函数randlaby与displaylaby均没有改变(对于这两个函数,是借鉴了邓公的DASCPP中的函数,可以通过修改其中的相关数值,达到提升迷宫难度、修改图形显示等功能)。
对于BFS的实现,主要基于三个新建的函数:

  1. bfs函数:这个函数的功能替换了DFS的labyrinth函数,使用的是常规的队列代替栈结构,一层一层的遍历入队出队,直到遇见终点或队列为空才推出;
  2. printLabyCell函数:用于输出当前cell的成员变量信息,主要使用在最小路径的路径输出中;
  3. shortest_path函数:用于得到最小路径的函数,其接受在BFS中遍历最后的那个cell,然后根据图结构,沿着cell的prev不断向前反推直到起点,在反推的过程中对每个节点输出路径、修改路径节点状态并计数。

具体的代码如下,相关的函数中均有详细的注释:

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

//这是最小路径的实现函数,通过BFS方式步步遍历的到;如果使用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;				  //运行BFS时建立前缀,用于建图形成反推
};

#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 inline void printLabyCell(Cell* elem)
{
	printf("%d -> (%d, %d) -> %d\n",
		((Cell*)elem)->incoming,
		((Cell*)elem)->x,
		((Cell*)elem)->y,
		((Cell*)elem)->outgoing);
}

void inline shortest_path(Cell *c)  //此函数用于根据传递到终点goalCell的数据,来进行路径反推
{
	cout << "shortest path is: " << endl;
	c->status = ROUTE;   //所有反推的路径全部将状态改为ROUTE,便于显示路径
	printLabyCell(c);
	while (c->incoming)  //一直反推到最短路径的初始点,即起点,起点的incoming是=0的
	{
		length++;        //length开始循环计数
		auto in = c->incoming;
		c = c->prev;
		switch (in)		 //根据上一个cell的incoming,来反推出当前cell的outgoing,相对取反即可
		{
		case EAST:  c->outgoing = WEST;   break;
		case SOUTH: c->outgoing = NORTH;   break;
		case WEST: c->outgoing = EAST;   break;
		case NORTH: c->outgoing = SOUTH;   break;
		default: exit(-1);
		}
		c->status = ROUTE;
		printLabyCell(c);
	}
	cout << "shortest path's long is " << length + 1 << endl;  //由于终点是在循环外面做的,所以此处需要加1
}

static bool bfs(Cell Laby[LABY_MAX][LABY_MAX], Cell *s, Cell *t)
{
	if ((AVAILABLE != s->status) || (AVAILABLE != t->status)) return false;  //首先,起点和终点必须是能访问的
	deque<Cell*> bfs_path;   //采用BFS算法,所以这里改为使用队列结构
	s->incoming = UNKNOWN; s->status = ROUTE; bfs_path.push_back(s);  //将起点的进入点设为无,然后状态设为在路径上,最后入队列
	do
	{
		Cell *c = bfs_path.front();
		bfs_path.pop_front();
		if (c == t)
		{
			t = c;    //当达到终点时,将此时的c传递给goalCell,因为其中储存了其所有的prev前缀
			return true;
		}
		while (NO_WAY != (c->outgoing = nextESWN(c->outgoing)))   //此处改为遍历当前cell的所有方向一次
		{
			if (AVAILABLE == neighbor(c)->status)  //只要cell的一个方向可以,就将其入队
			{
				Cell* temp = advance(c);
				temp->outgoing = UNKNOWN; temp->status = ROUTE;
				temp->prev = c;					   //每个从当前cell出去的cell,都将原cell设为前缀,由此实现当前图(树)结构的实现
				bfs_path.push_back(temp);
				ncheck++;
			}
		}
		c->status = BACKTRACKED;  //而被bfs过的cell,借用BACKTRACKED状态,表示其已经被扫描过但没有到达终点
	} while (!bfs_path.empty());
	return false;
}

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 (bfs(laby, startCell, goalCell))  //判断当前迷宫能否从起点走到终点,如果行的话,输出如下
	{
		cout << "true" << endl;
		cout << "start: " << "(" << startCell->x << "," << startCell->y << ")"            //输出当前迷宫的起点和终点
			<< "  " << "end: " << "(" << goalCell->x << "," << goalCell->y << ")" << endl;
		shortest_path(goalCell); //输出当前迷宫从起点到终点的最短路径与长度
	}
	else
		cout << "false" << endl;
	displayLaby();			 //形象的图像展示最短路径与搜索过程
	cout << "check times: "<<ncheck << endl;  //搜索次数
	return 0;
}

相关的运行结果如下:
运行结果
如图所示,运行结果包含了

  1. 迷宫的N*N格数;
  2. 能否找到起点到终点的路径;
  3. 起点与终点的坐标;
  4. 最短路径的行进过程;
  5. 最短路径的长度;
  6. 迷宫的路径查找图像示意;
  7. 整体查找总数。

其中图中的线段表示最短路径,起点为+符号,终点为$符号,其余的O符号表示每层遍历中已经遍历过的节点,而边缘的+表示与最短路径同级的其他的方向。

所以总结来说,广度优先搜索方法与其拓展的迪克斯特拉算法,还有相关的贪婪算法等策略,能够有效地运用于最短路径,最小开支,最佳选择等情况。而其整体的思路,也就是建立相关的节点类,其中包含了节点的各种成员数据,尤其是前后缀这种与建图(树)息息相关的数据,然后借助队列/栈等数据结构,对相关问题进行对应的算法求解,而其中可能也会涉及到向量、列表、串等数据结构的存储功能。由此可见,图(树)这两个数据结构真是博大精深,衍生出的各种各样的算法还需要我去慢慢学习啊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

方寸间沧海桑田

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

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

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

打赏作者

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

抵扣说明:

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

余额充值