迷宫:根据迷宫中通路的数量和路的特点,可将迷宫分为三类
- 简单迷宫
- 多通路迷宫(通路间不带环)
- 多通路迷宫(通路间带环)
不管是哪种迷宫,通常我们要解决的问题是找出通路,通常还得找到最短的那条通路,这里我们先说如何找到一条通路,等这个问题解决之后再说说如何找到最短的那条通路
找迷宫通路
找迷宫通路需要使用回溯法,找迷宫通路是对回溯法的一个很好的应用,实现回溯的过程用到了数据结构—栈
回溯法:对一个包括有很多个结点,每个结点有若干个搜索分支的问题,把原问题分解为若干个子问题求解的算法;当搜索到某个结点发现无法再继续搜索下去时,就让搜索过程回溯(回退)到该节点的前一个结点,继续搜索该节点外的其他尚未搜索的分支;如果发现该结点无法再搜索下去,就让搜索过程回溯到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一直进行到搜索到问题的解或者搜索完 了全部可搜索分支没有解存在为止。
问题分析:
代码实现:
void InitMaze(Maze* m, int map[ROW][COL]) //迷宫的初始化
{
for (int i = 0;i< ROW ;++i)
{
for (int j = 0; j < COL; ++j)
{
m->map[i][j] = map[i][j];
}
}
}
void PrintMaze(Maze* m) //打印迷宫
{
for (int i = 0; i < ROW; ++i)
{
for (int j = 0; j < COL; ++j)
{
printf("%d ",m->map[i][j]);
}
printf("\n");
}
printf("\n");
}
int IsValidEnter(Maze* m, Position cur) //判断是否是有效的入口
{
assert(m);
if ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)
&& (m->map[cur._x][cur._y] == 1))
return 1;
else
return 0;
}
int IsNextPass(Maze* m,Position cur, Position next) //判断当前节点的下一个节点能不能走通
{
assert(m);
//这里有必要判断一下他给的节点在不在迷宫里面
if (((next._x >= 0 && next._x < ROW) || (next._y >= 0 && next._y < COL))
&&(m->map[next._x][next._y] == 1))
return 1;
else
return 0;
}
int IsExitPosition(Maze* m, Position cur,Position enter) //判断当前节点是不是迷宫的出口
{
assert(m);
//这里首先得保证该节点不是入口点,其次只要它处在迷宫的边界即可
if ((cur._x != enter._x || cur._y != enter._y)
&& ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)))
{
return 1;
}
else
return 0;
}
int PassMaze(Maze* m,Position enter,Stack* s) //找迷宫通路
{
assert(m && IsValidEnter(m,enter) == 1); //对给的迷宫的入口进行合法性判断
Position cur = enter;
Position next;
StackPush(s, cur); //首先将迷宫的入口压入栈中
m->map[cur._x][cur._y] = 2; //将入口值改为2
//PrintMaze(m);
while (StackEmpty(s) != 0) {
cur = StackTop(s);
if (IsExitPosition(m,cur,enter) == 1) //调用函数检查当前节点是不是出口
return 1;
//上:看当前节点的上一个节点能不能走通
next = cur;
next._x = cur._x - 1;
if (IsNextPass(m,cur,next) == 1) //调用函数判断到next节点能走通时,将其压入栈中
{
StackPush(s,next);
m->map[next._x][next._y] = m->map[cur._x][cur._y]+1; //将next节点的值等于cur节点的值加1
//PrintMaze(m);
continue;
}
//左:看当前节点的向左的一个节点能不能走通
next = cur;
next._y = cur._y - 1;
if (IsNextPass(m, cur,next) == 1)
{
StackPush(s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//右:看当前节点的向右的一个节点能不能走通
next = cur;
next._y = cur._y + 1;
if (IsNextPass(m, cur,next) == 1)
{
StackPush(s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//下:看当前节点的下一个节点能不能走通
next = cur;
next._x = cur._x + 1;
if (IsNextPass(m, cur,next) == 1)
{
StackPush(s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//走到这里说明当前节点的四个方向都走不通,进行回溯,看前一个节点未被遍历的方向是否还能走通
StackPop(s);
}
return 0;
}
void test()
{
int map[ROW][COL] = { //用二维数组描绘迷宫:1 代表通路,0 代表墙
0,0,0,0,0,0,
0,0,1,0,0,0,
0,0,1,0,0,0,
0,0,1,1,1,1,
0,0,1,0,0,0,
0,0,1,0,0,0
};
Maze m;
Position enter; //迷宫入口
enter._x = 5;
enter._y = 2;
InitMaze(&m, map);
PrintMaze(&m);
Stack s; //定义一个栈,可以认为它是完成回溯的桥梁
StackInit(&s); //栈的初始
int ret = PassMaze(&m,enter,&s);
PrintMaze(&m);
}
步步打印出的迷宫,直观感受,深入理解这里通过栈实现的回溯法
找迷宫的最短路
问题分析:
找迷宫最短路的核心是对递归函数和栈的同步应用,你要找到最短的那条通路需要遍历或者说要走遍每一条迷宫中的通路,并且要记录每次走过的路,所有通路都被遍历完后选出最短的那条通路即可,这里的递归函数帮我们遍历迷宫中的所有通路,栈用于记录每条通路,下面具体说一下递归函数和栈是如何做到遍历和记录的
第一个阶段: 从入口到第一条通路走完的整个过程
函数栈帧图及栈中的节点情况:
这个过程中迷宫的变化如下:
第二阶段:
函数栈帧及栈中的节点情况:
这个阶段迷宫节点值是不发生改变的
第三阶段:
函数栈帧及栈中节点情况:
这个过程中迷宫的变化如下:
第四阶段:
函数栈帧及栈中节点情况:
这个过程中迷宫图中节点值是不会发生变化的
代码实现:
void InitMaze(Maze* m, int map[ROW][COL]) //迷宫的初始化
{
assert(m);
for (int i = 0; i < ROW; ++i)
for (int j = 0; j < COL; ++j)
{
m->map[i][j] = map[i][j];
}
}
void PrintMaze(Maze* m) //打印迷宫
{
assert(m);
for (int i = 0; i < ROW; ++i)
{
for (int j = 0; j < COL; ++j)
{
printf("%d ", m->map[i][j]);
}
printf("\n");
}
printf("\n");
}
int IsValidEnter(Maze* m, Position cur) //判断是否是有效的入口
{
assert(m);
if ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)
&& (m->map[cur._x][cur._y] == 1))
return 1;
else
return 0;
}
int IsNextPass(Maze* m, Position cur, Position next) //判断当前节点的下一个节点能不能走通
{
assert(m);
//这里有必要判断一下他给的节点在不在迷宫里面
if (
((next._x >= 0 && next._x < ROW) && (next._y >= 0 && next._y < COL))
&& ((m->map[next._x][next._y] == 1) || m->map[cur._x][cur._y] < m->map[next._x][next._y])
)
return 1;
else
return 0;
}
int IsExitPosition(Position cur, Position enter) //判断当前节点是不是迷宫的出口
{
//这里首先得保证该节点不是入口点,其次只要它处在迷宫的边界即可
if ((cur._x != enter._x || cur._y != enter._y)
&& ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)))
{
return 1;
}
else
return 0;
}
void SaveShortPath(Stack* shortpath, Stack* newpath) //保存当前最短的通路
{
assert(shortpath && newpath);
for (size_t i = 0;i < StackSize(newpath);++i)
StackPush(shortpath,newpath->_array[i]);
}
void _GetMazeShortPath(Maze* m,Position enter,Position cur,Stack* shortpath,Stack* newpath)
{
assert(m && shortpath && newpath);
StackPush(newpath, cur);
PrintMaze(m);
if (IsExitPosition(cur,enter) == 1) //判断当前节点是否是出口
{
if (StackEmpty(shortpath) == 0 || StackSize(shortpath) > StackSize(newpath))
SaveShortPath(shortpath,newpath);
StackPop(newpath);
return;
}
Position next; //标记当前节点的下一个节点
//上
next = cur;
next._x -= 1; //当前节点上面的那个节点
if (IsNextPass(m,cur,next) == 1) //判断next节点能否走通
{
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
_GetMazeShortPath(m, enter,next,shortpath,newpath);
}
//左
next = cur;
next._y -= 1; //当前节点左边的那个节点
if (IsNextPass(m, cur, next) == 1) //判断next节点能否走通
{
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
_GetMazeShortPath(m, enter, next, shortpath, newpath);
}
//右
next = cur;
next._y += 1; //当前节点上面右边的那个节点
if (IsNextPass(m, cur, next) == 1) //判断next节点能否走通
{
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
_GetMazeShortPath(m, enter, next, shortpath, newpath);
}
//下
next = cur;
next._x += 1; //当前节点下面的那个节点
if (IsNextPass(m, cur, next) == 1) //判断next节点能否走通
{
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
_GetMazeShortPath(m, enter, next, shortpath, newpath);
}
//走到这里说明当前节点的四个方向都走不通,接着搜索当前节点的上一个节点中未被搜索的方向
StackPop(newpath);
}
void PassMaze(Maze* m,Position enter, Stack* shortpath)
{
assert(m && shortpath && IsValidEnter(m, enter) == 1); //给的入口是否合适?
m->map[enter._x][enter._y] = 2; //将入口节点值改为2
Position cur = enter; //表示当前节点
Stack newstack; //定义一个栈,用于记录当前正在走的通路
StackInit(&newstack); //初始化栈
_GetMazeShortPath(m,enter,cur,shortpath,&newstack);
}
void test()
{
int map[ROW][COL] = { //用二维数组描绘迷宫:1 代表通路,0 代表墙
0,0,0,0,0,
0,1,1,1,0,
0,1,0,1,0,
0,1,1,1,1,
0,1,0,0,0
};
Maze m;
Position enter; //迷宫入口
enter._x = 4;
enter._y = 1;
InitMaze(&m, map);
PrintMaze(&m);
Stack shortpath; //定义一个栈用来存放最短的那条通路
StackInit(&shortpath); //初始化栈
PassMaze(&m,enter,&shortpath);
PrintMaze(&m);
}
总结: 个人觉得通过迷宫求解对递归的理解是这里很重要的一部分,数据结构中递归的分量很重,在很多地方都会用到递归,比如树就是根据递归定义的,通过递归能实现树中的很多操作,再到后面的快速排序以及归并排序都用到了递归
学长问过我一个问题,栈的最重要的应用是什么?我当时答的是找迷宫通路,栈的最重要的应用是函数栈帧,解迷宫问题是体现的特别明显(先建立的栈帧后销毁,比如迷宫入口节点A调用递归函数建立的栈帧是第一个被建立的,同时也是最后一个被销毁的)。