基于广度优先搜索及运用队列和栈解决迷宫问题(c语言)
一、迷宫问题:
假设迷宫由m行n列构成,有一个入口和一个出口,入口坐标为(1,1),出口坐标为(m,n),试设计并验证以下算法:找出一条从入口通往出口的路径,或报告一个“无法通过”的信息。
(1) 用C语言实现顺序存储结构上队列的基本操作,然后利用该队列的基本操作找出迷宫的一条最短路径。
(2) 设计一个二维数组MAZE[m+2][n+2]表示迷宫,数组元素为0表示该位置可以通过,数组元素为1表示该位置不可以通行。MAZE[1][1]、MAZE[m][n]分别为迷宫的入口和出口。
(3) 输入迷宫的大小m行和n列,动态生成二维数组;由随机数产生0或1,建立迷宫,注意m*n的迷宫需要进行扩展,扩展部分的元素设置为1,相当于在迷宫周围布上一圈不准通过的墙。
(4) 要求输出模拟迷宫的二维数组;若存在最短路经,则由出口回溯到入口(出队列并利用栈实现),再打印从入口到出口的这条路径,例如(1,1),……,(i,j),……,(m,n);若没有路径,则打印“No path!”。
(5)迷宫的任一位置(i,j)上均有八个可以移动的方向,用二维数组Direction存放八个方向上的位置偏移量。
Direction[8][2]={{0,1},{1,1},{0,-1},{-1,-1},{1,1},{0,-1},{-1,-1},{0,1}};
(6)为避免出现原地踏步的情况为了标志已经通过的位置,采用一个标志数组MARK[m+2][n+2],初值均为0,在寻找路径的过程中,若通过了位置(i,j),则将MARK[i][j]置为1。
(7) 为了记录查找过程中到达位置(i,j)及首次到达(i,j)的前一位置(i_pre,j_pre),需要记住前一位置(i_pre,j_pre)在队列中的序号pre,即队列中数据元素应该是一个三元组(i,j,pre)。
(8) 搜索过程简单描述如下:将入口MAZE[1][1]作为第一个出发点,依次在八个方向上搜索可通行的位置,将可通行位置(i,j,pre)入队,形成第一层新的出发点,然后依次出队,即对第一层中各个位置分别搜索它所在八个方向上的可通行位置,形成第二层新的出发点,…,如此进行下去,直至达到出口MAZE[m][n]或者迷宫所有位置都搜索完毕为止。
简述问题:让我们运用到顺序队列和顺序栈基于广度优先搜索找出一个自动生成迷宫的路径,每次搜索是向周围八个方向搜索。
二、广度优先搜索在本次迷宫问题上的应用:
通俗的来说,这种思想在本次问题上的体现是,引入队列,由起点元素出发,找其周围八个地图结点,如果该地图结点时可通的,也就是值为’0‘,那么纳入队列。搜索完起点的八个方向,也就是完成了第一层搜索,此时队列中的元素是起点以及起点的儿子(起点周围的可通点),之后对起点的每个儿子开始搜索,也就是进行第二层搜索,如此又找到起点儿子的各自儿子,又将其纳入队列又开始新一层的搜索,(每次入队时都要入栈,为了便于打印出路径)。搜索会有两种结束情况,1.队列为空,证明在某一层搜索时其所有点都没有儿子(某层的点周围都没有通路),2.在某层对某个点周围进行搜索时找到了终点(最短路径出现了)。
如上的这种搜索,找到的路径一定是最短路径!(读者可自行思考)
三、核心代码解释
1.路径的搜索
void FindPath(MyQueue *queue,MyStack *stack,int sx,int sy,int MAZE[][N],int MARK[][N],int m,int n){//捕捉路径
node Gather;//先引入一个结构体变量和三个整形的
int tx;
int ty;
PushQueue(queue,sx,sy,-1);//将起点入队和入栈,由于其没有父亲,可将pre定义为-1,其实可以任意定义一个小于1的数字
PushStack(stack,sx,sy,-1);
int find = 0;//代表是否搜索到终点
while (!QueueEmpty(queue) && !find){//循环的结束条件是队列为空或者已经捕获到终点
Gather = GetQueueHead(queue);//获取队头元素
OutQueue(queue);//队头结点出队
int now = queue->front;//队头元素的下标
for(int k = 0;k < 8;k++){//遍历八次即搜索八个方向
tx = Gather.x + Direction[k][0];//加上特定二维数组完成对八个方向的搜索
ty = Gather.y + Direction[k][1];
if(tx < 1 || tx > n || ty < 1 || ty > m){//判断搜索到的是否是边界
continue;
}
if(MAZE[tx][ty] == 0 && MARK[tx][ty] == 0){//如果其在迷宫上为0且没有在标记数组中出现(即没有被走过)那么纳入队列和栈
MARK[tx][ty] = 1;
PushQueue(queue,tx,ty,now);
PushStack(stack,tx,ty,now);
}
if(tx == m && ty == n){//如果捕获到终点则结束
find = 1;
break;
}
}
}
if(find == 0){//如果上面的操作并没有找到终点则打印如下并退出
printf("NO PATH!");
exit(-1);
}
}
2.迷宫的打印
void PrintPath(MyStack *myStack,int MAZE[][N]){//打印地图操作
printf("\n");
printf("路径(从右到左):");//由于是栈的操作所以打印出的是终点到起点
int target = myStack->elem[myStack->top-1].pre;//如果有通路,那么栈顶元素一定是终点,pre为该点父亲的下标也等于其父亲的top-1
MAZE[GetStackTop(myStack).x][GetStackTop(myStack).y] = 2;//把最短路径赋值为2方便观察
printf("(");
printf("%d,%d",myStack->elem[myStack->top-1].x,myStack->elem[myStack->top-1].y);//先打印出终点
printf(") ");
while (!StackEmpty(myStack)){//循环结束即出栈完成
if(myStack->top-1 == target){//如果出栈元素的top-1也就是其下标等于上一次被打印元素的pre,则证明是上一个被打印元素的父亲
printf("<- (");
printf("%d,%d",myStack->elem[myStack->top-1].x,myStack->elem[myStack->top-1].y);
printf(") ");
target = myStack->elem[myStack->top-1].pre;//target改变,因为已经又找到一个路径元素
MAZE[GetStackTop(myStack).x][GetStackTop(myStack).y] = 2;
PopStack(myStack);//出栈
}
else{//如果不是其父亲直接出栈
PopStack(myStack);
}
}
printf("\n\n");
}
四、结果展示:
在这里插入图片描述
五、完整代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
#define M 100
#define N 100
int Direction[8][2]={{-1,0},{-1,1},{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1}};//代表八个方向的位移偏量
typedef struct {//结点元素结构体
int x;//代表某个点坐标x
int y;//代表某个点坐标y
int pre;//代表某点的前驱结点在队列中的下标
}node;
typedef struct {//顺序栈的定义
node *elem;
int size;
int top;
}MyStack;
typedef struct {//顺序队列的定义
node *node;
int rear;
int front;
}MyQueue;
void color(int x);//改变打印出颜色的函数
void InPutMaze(int maze[][N],int m,int n);//自动创建迷宫的函数
void OutPutMaze( int MAZE[][N],int m,int n);//输出迷宫的函数
MyStack *StackCreak(MyStack *stack);//栈的创建
MyQueue *QueueCreak(MyQueue *queue);//队列的创建
void PushStack(MyStack *stack,int x,int y,int pre);//入栈操作
void PopStack(MyStack *stack);//出栈操作
int StackEmpty(MyStack *stack);//判断栈是否为空
node GetStackTop(MyStack *stack);//获取栈顶元素
void PushQueue(MyQueue *queue,int x,int y,int pre);//入队操作
void OutQueue(MyQueue *queue);//出队操作
node GetQueueHead(MyQueue *queue);//获取队头元素
int QueueEmpty(MyQueue *queue);//判断队列是否为空
void PrintPath(MyStack *myStack,int MAZE[][N]);//打印出路径
void FindPath(MyQueue *queue,MyStack *stack,int sx,int sy,int MAZE[][N],int MARK[][N],int m,int n);//找到路径
int main() {
int m;
int n;
printf("请依次输入迷宫的m和n\n");
printf("迷宫的大小为m+2 * n+2\n");
scanf("%d %d",&m,&n);
int MAZE[M][N];
int MARK[M][N];
for(int i = 0;i < m+2;i++){
for(int j = 0; j < n+2;j++){
MARK[i][j] = 0;
}
}
InPutMaze(MAZE,m,n);//自动生成迷宫
OutPutMaze(MAZE,m,n);//先打印出迷宫的样子
MyStack *stack = StackCreak(stack);//创建栈
MyQueue *queue = QueueCreak(queue);//创建队列
PushStack(stack,2,2,2);//由于遍历的需要,栈会有个多余元素,其本身不会影响结果,不加的话才会
FindPath(queue,stack,1,1,MAZE,MARK,m,n);//捕捉路径,传入的参数依次为,队列,栈,起点坐标,迷宫,标记数组,迷宫行列(迷宫终点与其相关)
PrintPath(stack,MAZE);//打印出路径
OutPutMaze(MAZE,m,n);//输出操作后迷宫
return 0;
}
void OutPutMaze( int MAZE [][N],int m,int n) {
for(int i = 0; i < m + 2; i++){//以二重循环遍历二维数组中的每个元素打印
for(int j = 0;j < n+2;j++){
if(MAZE[i][j] == 0){//对于0即可通结点用‘3’颜色,下面原理相同
color(3);
printf("%d ",MAZE[i][j]);
}
else if(MAZE[i][j] == 1)
{
color(4);
printf("%d ",MAZE[i][j]);
}
else if(MAZE[i][j] == 2){
color(6);
printf("%d ",MAZE[i][j]);
}
}
printf("\n");
}
}
void InPutMaze(int maze[][N],int m,int n){//随机生成一个迷宫
srand((unsigned)time(NULL));
for(int i = 0;i < m+2;i++){
for(int j = 0; j < n+2;j++){
if(i == 0 || j == 0 || i == m+1 || j == n+1)//判断某个坐标点是否为边界点,是的话赋值为1
maze[i][j] = 1;
else
maze[i][j] = rand()%2;//对随机数除2取余即可获得随机的0或1
}
}
maze[1][1] = 0;//起点和终点赋值为0
maze[m][n] = 0;
}
MyStack *StackCreak(MyStack *stack){//栈的初始化操作
stack = malloc(sizeof (MyStack));
stack->elem = malloc(sizeof (node) * 10000);//申请足够大的空间避免溢出
stack->top = 0;
stack->size = 1;
return stack;//返回该指针
}
void PushStack(MyStack *stack,int x,int y,int pre){//入栈操作,栈中每个元素都是一个结构体结点
stack->size++;
stack->elem[stack->top].x = x;
stack->elem[stack->top].y = y;
stack->elem[stack->top].pre = pre;
stack->top++;
}
void PopStack(MyStack *stack){//出栈操作
stack->top--;
}
node GetStackTop(MyStack *stack){//获取栈顶元素
node node1;//返回的是一个结构体结点
node1.x = stack->elem[stack->top-1].x;
node1.y = stack->elem[stack->top-1].y;
node1.pre = stack->elem[stack->top-1].pre;
return node1;
}
int StackEmpty(MyStack *stack){//判断栈是否为空
if(stack->top == 0)
return 1;
return 0;
}
MyQueue *QueueCreak(MyQueue *queue){//队列的初始化
queue = malloc(sizeof (MyQueue));
queue->node = malloc(sizeof (node) * 10000);//也需要申请足够大的空间
queue->front = 0;
queue->rear = 0;
return queue;
}
void PushQueue(MyQueue *queue,int x,int y,int pre){//入队操作
queue->node[queue->rear].x = x;//让rear往后移动即可
queue->node[queue->rear].y = y;
queue->node[queue->rear].pre = pre;
queue->rear++;
}
void OutQueue(MyQueue *queue){//出队操作,让front向后偏移
queue->front++;
}
node GetQueueHead(MyQueue *queue){//获取队头元素
node node1;//返回对头的坐标及父亲的下标
node1.x = queue->node[queue->front].x;
node1.y = queue->node[queue->front].y;
node1.pre = queue->node[queue->front].pre;
return node1;
}
int QueueEmpty(MyQueue *queue){//判断队列是否为空
if(queue->rear == queue->front)
return 1;
return 0;
}
void FindPath(MyQueue *queue,MyStack *stack,int sx,int sy,int MAZE[][N],int MARK[][N],int m,int n){//捕捉路径
node Gather;//先引入一个结构体变量和三个整形的
int tx;
int ty;
PushQueue(queue,sx,sy,-1);//将起点入队和入栈,由于其没有父亲,可将pre定义为-1,其实可以任意定义一个小于1的数字
PushStack(stack,sx,sy,-1);
int find = 0;//代表是否搜索到终点
while (!QueueEmpty(queue) && !find){//循环的结束条件是队列为空或者已经捕获到终点
Gather = GetQueueHead(queue);//获取队头元素
OutQueue(queue);//队头结点出队
int now = queue->front;//队头元素的下标
for(int k = 0;k < 8;k++){//遍历八次即搜索八个方向
tx = Gather.x + Direction[k][0];//加上特定二维数组完成对八个方向的搜索
ty = Gather.y + Direction[k][1];
if(tx < 1 || tx > m || ty < 1 || ty > n){//判断搜索到的是否是边界
continue;
}
if(MAZE[tx][ty] == 0 && MARK[tx][ty] == 0){//如果其在迷宫上为0且没有在标记数组中出现(即没有被走过)那么纳入队列和栈
MARK[tx][ty] = 1;
PushQueue(queue,tx,ty,now);
PushStack(stack,tx,ty,now);
}
if(tx == m && ty == n){//如果捕获到终点则结束
find = 1;
break;
}
}
}
if(find == 0){//如果上面的操作并没有找到终点则打印如下并退出
printf("NO PATH!");
exit(-1);
}
}
void PrintPath(MyStack *myStack,int MAZE[][N]){//打印地图操作
printf("\n");
printf("路径(从右到左):");//由于是栈的操作所以打印出的是终点到起点
int target = myStack->elem[myStack->top-1].pre;//如果有通路,那么栈顶元素一定是终点,pre为该点父亲的下标也等于其父亲的top-1
MAZE[GetStackTop(myStack).x][GetStackTop(myStack).y] = 2;//把最短路径赋值为2方便观察
printf("(");
printf("%d,%d",myStack->elem[myStack->top-1].x,myStack->elem[myStack->top-1].y);//先打印出终点
printf(") ");
while (!StackEmpty(myStack)){//循环结束即出栈完成
if(myStack->top-1 == target){//如果出栈元素的top-1也就是其下标等于上一次被打印元素的pre,则证明是上一个被打印元素的父亲
printf("<- (");
printf("%d,%d",myStack->elem[myStack->top-1].x,myStack->elem[myStack->top-1].y);
printf(") ");
target = myStack->elem[myStack->top-1].pre;//target改变,因为已经又找到一个路径元素
MAZE[GetStackTop(myStack).x][GetStackTop(myStack).y] = 2;
PopStack(myStack);//出栈
}
else{//如果不是其父亲直接出栈
PopStack(myStack);
}
}
printf("\n\n");
}
void color(int x) {//一个可以自由选打印颜色的方法
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), x);
}
六、问题反思:
我认为本问题更适合用链式结构的线性表,如果采用顺序结构,可以使用动态栈和队列,感兴趣的同学可以试试。