迷宫问题(上)之堆栈详解(C语言描述)
迷宫问题,顾名思义就是求出从入口到出口的路径。正常情况下,我们的想法都是采用“穷举求解”的方法,即从迷宫入口出发,顺着某一方向向前试探,若能走通则继续向前走;否则就沿着原路返回,换一个方向再继续进行试探,直到所有可能的路径均被试探完。为了保证我们可以沿着原路返回(回溯),我们可以选用栈这个先入后出的数据结构来保存从入口到当前位置的路径。
首先我们建立了这样一个8*8的迷宫,为了在一会儿计算方位的时候更加便利,我们给迷宫外面加上了一个“外墙”,如下图所示,白色代表通路,黑色代表墙,题目要求我们的路径必须是简单路径即在所求得的路径上不能重复出现同一通道块。
为了表示迷宫,我们建立了一个二维数组mg,用0来表示通路,用1来表示墙,在迷宫中的每一个方块,其上下左右均有一个方块与之相邻,如下图所示,我们假设当前方块的位置为(i, j),规定其上方的方块的方位号为0,按照顺时针的递增顺序依次给其余三个方位的方位号进行编号。在试探的过程中,我们按照方位号从0到3进行查找下一个可走的方块的位置。
我们采用栈这个数据结构来进行方块的存储,自然要对栈进行相应的定义,其定义如下
用C语言的思路来求解迷宫问题(从(1,1)到(M,N)的路径)算法:先将入口位置的方块进栈(初始方块的位置设置为-1),在栈不空的时候进循环:读栈顶方块(不退栈),如果该方块是出口,则输出栈中的方块位置即为路径;否则就在相邻的方块中寻找下一个可走的方块,若不存在可走的方块,则退栈(即向后退一步);若存在这样的方块,则将其位置存在栈中,并将这个可走的相邻方块入栈(并将新方块的初始位置设置为1)。
为了保证试探的可走的相邻方块不是已经走过的方块,我们可以在一个方块入栈后将其对应的迷宫数组(mg数组)元素改为-1(变为不可走的方块),在退栈时(当没有可以走的方块的时候)再将其恢复为0。例如方块(i, j)已经入栈,在试探(i +1 ,j)的时候,又会试探到(i, j),这样可能会引起死循环,采用上述方法之后就会避免这种情况发生。
对应的具体实现如下:
/*
1表示对应通道是墙,0表示对应通道为通路
*/
#include<stdio.h>
#define maxsize 10001 //栈的最大容量
#define M 8 //8行
#define N 8 //8列
struct
{
int i; //当前方块的行号
int j; //当前方块的列号
int di; //下一个可走的方位的方位号
}st[maxsize]; //定义栈
int top = -1; //初始化栈顶指针
int mg[M + 2][N + 2] = //为了计算方便,在最外边加了"外墙",即增加了两行两列
{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 1, 0, 0, 1},
{1, 0, 1, 1, 1, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 1, 0, 0, 0, 0, 1},
{1, 0, 1, 0, 0, 0, 1, 0, 0, 1},
{1, 0, 1, 1, 1, 0, 1, 1, 0, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
//路径为,入口(x1, y1),出口(x2, y2)
void mgpath(int x1, int y1, int x2, int y2)
{
int i, j, di, find, k;
top++; //初始化方块进栈
st[top].i = x1; //行号
st[top].j = y1; //列号
st[top].di = -1; //下一块的方位
mg[x1][y1] = -1; //因为题目要求不能走已经走过的路线,因此将第一个变为不可再走的方块
while(top > -1) //栈不空时,循环继续
{
i = st[top].i;
j = st[top].j;
di = st[top].di;
if(i == x2 && j == y2) //说明已经找到了出口,直接输出路径
{
puts("迷宫路径如下:");
for(k = 0;k <= top;k++)
{
printf("\t(%d, %d)", st[k].i, st[k].j);
if((k +1) % 5 == 0) //输出的时候每5个方块后换行
puts("");
}
puts("");
return;
}
find = 0;
while(find == 0 && di < 4) //寻找下一个可走的方块
{
di++;
switch(di)
{
case 0:i = st[top].i - 1;j = st[top].j;break; //向上走
case 1:i = st[top].i;j = st[top].j + 1;break; //向右走
case 2:i = st[top].i + 1;j = st[top].j;break; //向下走
case 3:i = st[top].i;j = st[top].j - 1;break; //向左走
}
if(mg[i][j] == 0) //如果找到了通路,就不用再进行上面的循环了
find = 1;
}
if(find == 1) //找到了下一个可走的方块
{
st[top].di = di; //修改之前栈顶元素中di的值
top++; //下一个可以走的方块进栈,栈顶指针上移
st[top].i = i;
st[top].j = j;
st[top].di = -1; //注意这里要修改di的值
mg[i][j] = -1; //避免重复走该方块
}
else //没有路径就退栈(回溯)
{
mg[st[top].i][st[top].j] = 0; //让该位置变为其他路径可走的方块
top--; //栈顶指针下移
}
}
puts("没有可走路径。");
}
int main()
{
int x1, y1, x2, y2;
puts("请输入起点与终点坐标(中间以空格分开且不大于8)");
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
mgpath(x1, y1, x2, y2);
return 0;
}
如果本题稍作改动,变为输出迷宫所有路径并求出最短路径及其长度。对比上面的解法,在成功的找到一条可以走下去的路径之后就会退出;那我们可以将路径保存到另一个栈path[]中,并定义变量minlen来保存最短路径长度。当 while循环结束的时候输出path[]中最短的路径。具体实现如下:
#include<stdio.h>
#define maxsize 10001 //栈的最大容量
#define M 8 //8行
#define N 8 //8列
struct
{
int i; //当前方块的行号
int j; //当前方块的列号
int di; //下一个可走的方位的方位号
}st[maxsize], path[maxsize]; //定义栈
int top = -1; //初始化栈顶指针
int count = 1; //初始化计数器
int minlen = maxsize; //最短路径长度
int mg[M + 2][N + 2] = //为了计算方便,在最外边加了"外墙",即增加了两行两列
{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 1, 0, 0, 1},
{1, 0, 1, 1, 1, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 1, 0, 0, 0, 0, 1},
{1, 0, 1, 0, 0, 0, 1, 0, 0, 1},
{1, 0, 1, 1, 1, 0, 1, 1, 0, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
//路径为,入口(x1, y1),出口(x2, y2)
void mgpath(int x1, int y1, int x2, int y2)
{
int i, j, di, find, k;
top++; //初始化方块进栈
st[top].i = x1; //行号
st[top].j = y1; //列号
st[top].di = -1; //下一块的方位
mg[x1][y1] = -1; //因为题目要求不能走已经走过的路线,因此将第一个变为不可再走的方块
while(top > -1) //栈不空时,循环继续
{
i = st[top].i;
j = st[top].j;
di = st[top].di;
if(i == x2 && j == y2) //说明已经找到了出口,直接输出路径
{
printf("迷宫路径如下: ");
printf("\t第%d条:\n",count++);
for(k = 0;k <= top;k++)
{
printf("\t(%d, %d)", st[k].i, st[k].j);
if((k +1) % 5 == 0) //输出的时候每5个方块后换行
puts("\t");
}
puts("");
if(top + 1 < minlen)
{
for(k = 0;k <= top;k++)
path[k] = st[k]; //当前栈中元素依次压入临时栈中
minlen = top + 1; //栈顶指针对应
}
mg[st[top].i][st[top].j] = 0; //让该位置方块变为其他路径可走的方块
top--;
i = st[top].i;
j = st[top].j;
di = st[top].di;
}
find = 0;
while(find == 0 && di < 4) //寻找下一个可走的方块
{
di++;
switch(di)
{
case 0:i = st[top].i - 1;j = st[top].j;break; //向上走
case 1:i = st[top].i;j = st[top].j + 1;break; //向右走
case 2:i = st[top].i + 1;j = st[top].j;break; //向下走
case 3:i = st[top].i;j = st[top].j - 1;break; //向左走
}
if(mg[i][j] == 0) //如果找到了通路,就不用再进行上面的循环了
find = 1;
}
if(find == 1) //找到了下一个可走的方块
{
st[top].di = di; //修改之前栈顶元素中di的值
top++; //下一个可以走的方块进栈,栈顶指针上移
st[top].i = i;
st[top].j = j;
st[top].di = -1; //注意这里要修改di的值
mg[i][j] = -1; //避免重复走该方块
}
else //没有路径就退栈(回溯)
{
mg[st[top].i][st[top].j] = 0; //让该位置变为其他路径可走的方块
top--; //栈顶指针下移
}
}
if(minlen == 10001 && st[top].i == 0 == st[top].j == 0) //如果输出的元素均为0,则说明没有路径可走
puts("没有路径可走");
else
{
printf("最短路径的长度为: %d\n", minlen);
printf("最短路径为:\n");
for(k = 0;k < minlen;k++)
{
printf("\t(%d, %d)", path[k].i, path[k].j);
if((k + 1) % 5 == 0)
puts("\t");
}
}
}
int main()
{
int x1, y1, x2, y2;
puts("请输入起点与终点坐标(中间以空格分开且不大于8)");
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
mgpath(x1, y1, x2, y2);
return 0;
}