迷宫问题的求解
在学习了数据结构的栈和队列的相关知识以后,我接触到了栈的一些应用,其中迷宫问题就是一种栈的应用。
下面给上一个简单的迷宫:
在这个迷宫中我们认为0是可以通过的路径,而1就相当于墙,是不可以通过的。
栈的实现方法:
基本的实现方法其实是蛮简单的,我们知道栈的特点就是先进后出,后进先出,所以,当我们从入口开始,将可以通过的路径的坐标压栈,压入栈中,但当路径没有办法通过的时候,我们就开始往后面退,这个时候就是路径坐标出栈的过程,每往后退一次,就再一次在这个点上寻找其余方向是否有可以通过的路径,没有就继续往后面退,有就往新的可以通的方向走,直到走出这个迷宫,这个方法称之为“回溯法”。
在简单介绍了大概地实现原理后,我们可以来想一下具体的实现过程了。
由于我们在这里是通过栈存储点的坐标,所以我们需要一个能够表明坐标的结构体,结构体中包括这个坐标的行和列;接着就是我们在这里给的迷宫是一个二维数组,但是二维数组我们有时候并不清楚他是需要有多大,而且二维数组的动态开辟并不方便实现,动态开辟需要去使用数组指针,显然这样是比较麻烦的,在以前C语言学习的时候我们知道二维数组在内存中的存储方式也还是按照一维数组的方式,还是一段连续的空间,所以我们还是去使用一维数组,在细节方面进行一下强转就好了。
下面我们以代码进行分析:
const size_t N = 10;
void GetMaze(int *maze,size_t N)
{
FILE* fp = fopen("MazeMap_1.txt", "r");
assert(fp);
for (size_t i = 0; i < N; ++i)
{
for (size_t j = 0; j < N;)
{
int value = fgetc(fp);
if (value == '1' || value == '0')
{
maze[i * N + j] = value - '0';
++j;
}
else if (value == EOF)
{
throw invalid_argument("fp error");
}
}
}
fclose(fp);
}
void PrintMaze(int* maze, size_t N)
{
for (size_t i = 0; i < N; ++i)
{
for (size_t j = 0; j < N; ++j)
{
cout << maze[i * N + j] << " ";
}
cout << endl;
}
cout << endl;
}
struct Pos
{
size_t row;//行
size_t col;//列
};
//栈的方式(入栈和出栈,将路径先压入栈中,然后需要回溯的时候将压入栈的路径再出栈)
bool GetMazePath(int* maze, size_t N, Pos entry, stack<Pos>& path)
{
maze[entry.row*N + entry.col] = 2;
Pos Cur, next;
Cur = entry;
path.push(entry);
while (!path.empty())
{
Cur = path.top();
if (Cur.row == N - 1)
{
return true;
}
next = Cur;
//上
next.row -= 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
//下
next.row += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
//左
next.col -= 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
//右
next.col += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
path.pop();
}
return false;
}在这个代码中GetMaze()就是获取迷宫的函数,其中用文件进行读取,读到错误会抛出异常。PrintMaze()就是打印函数了。
最主要的是下面的GetMazePath()函数了,这是为了获取路径的。在这个函数中,我们可以进行一下分析:
首先是传参部分,参数首先得把这个迷宫,也就是这个数组得传进去,然后,要想进入这个迷宫,那么你得需要一个入口吧,当然,最后你还需要一个栈,也就是存储的路径。
然后我们在这里需要看的就是具体的实现方法了,你从迷宫入口进入,每走过一个点,你需要将这个点做一下标记,总不能啥都不做吧,要不然岂不是相当于没走过咯。当然,你还得知道自己到底有没有走出这个迷宫对吧,那么你就需要一个判断出迷宫的条件,这个也是比较简单,只要出了这个迷宫的范围,那不就出去了么。做好了这一部分,那么我们需要考虑的就是如何具体的去走这个迷宫了。之前我说过,走过的迷宫的路径是进行压栈的,那么只要栈不为空,就表示我还在迷宫里面走,如果栈空了,就说明你已经回退出去了,那么就说明迷宫没有出口。
假设你现在在迷宫里面走,只要遇到不是1的墙,是0的路,那么你就走,当你遇到岔路口时,你可以随意先挑一个方向去走,如果走不通你就回退,退到你可以往没走过的地方走时向新的方向走。
下面我给代码添加注释,希望可以方便你去理解。
bool GetMazePath(int* maze, size_t N, Pos entry, stack<Pos>& path)
{
maze[entry.row*N + entry.col] = 2;//先把入口置成2,说明我已经站在这个入口点了
Pos Cur, next;//给上两个变量,一个表示我当前所在位置,另一个表示我下一个即将走的位置
Cur = entry;
path.push(entry);//将 入口压栈,表示你已经走了路口点了
while (!path.empty()) //当栈不为空的时候你就继续执行,如果为空表示你已经出栈,说明你已经退出入口点了,迷宫没有出口
{
Cur = path.top();//取出当前的点
if (Cur.row == N - 1)//判断是否走出迷宫
{
return true;
}
next = Cur;//先将当前坐标进行拷贝
//上
next.row -= 1;//在这里将行退一步,表示你现在在往上走,然后next就成为下一个点了
if (maze[next.row * N + next.col] == 0)//判断往上走是否可以通过,可以就进入,不可以就继续这个循环
{
path.push(next);//如果进来了,将这个点压栈
maze[next.row * N + next.col] = 2;//并将当前坐标上的值改成2,表示我已经走过了
continue;//让循环从头开始,每一次都需要进行全方位的判断,不能漏掉某一个方向
}
next = Cur;
//下面的与上面的基本一致,只是方向略有不同 //下
next.row += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
//左
next.col -= 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
//右
next.col += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
continue;
}
next = Cur;
path.pop();//如果4个方向都没有进去,说明四个方向都不通了,那么此刻只能往回倒退,让这个点出栈,退回上一个点
}
return false;
}下面就是我的测试代码,我把结果也一同显示:
void TestMaze()
{
int maze[N][N];
GetMaze((int*)maze, N);
Pos entry{ 2, 0 };
maze[entry.row ][ entry.col] = 2;
stack<Pos> path;
stack<Pos> shortpath;
GetMazePath((int*)maze, N, entry, path);
cout << "是否找到迷宫出口" << !path.empty() << endl;
PrintMaze((int*)maze, N);
}最终输出的结果显示是:
递归的实现方法:
上面我讲了使用栈的方法,接着我就介绍一下递归的方法。
大家都知道,递归在某种程度上就可以当做是循环进行使用,那么我们只需要改一点点地方,就可以实现递归的方法了。
你可以仔细思考一下,我把代码先给你看
void GetMazePath_R(int* maze, size_t N, Pos entry, stack<Pos>& path)
{
Pos Cur, next;
Cur = entry;
if (Cur.row == N - 1)
{
return ;
}
next = Cur;
//上
next.row -= 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);//用栈保存一下数据
maze[next.row * N + next.col] = 2;
GetMazePath_R(maze, N, next, path);
}
next = Cur;
//下
next.row += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
GetMazePath_R(maze, N, next, path);
}
next = Cur;
//左
next.col -= 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
GetMazePath_R(maze, N, next, path);
}
next = Cur;
//右
next.col += 1;
if (maze[next.row * N + next.col] == 0)
{
path.push(next);
maze[next.row * N + next.col] = 2;
GetMazePath_R(maze, N, next, path);
}
}看完这个代码你看懂了没,其实方法很简单,你只需要将下一个你将走的点当做路口点再去调用就好了,每一次的调用都将会让你往后走,你只需要用栈保存一下这个点的坐标就好了。只要想通了其实是很简单的。下面我把测试函数和结果显示一下:
void TestMaze()
{
int maze[N][N];
GetMaze((int*)maze, N);
Pos entry{ 2, 0 };
maze[entry.row ][ entry.col] = 2;
stack<Pos> path;
stack<Pos> shortpath;
GetMazePath_R((int*)maze, N, entry, path);
cout << "是否找到迷宫出口" << !path.empty() << endl;
PrintMaze((int*)maze, N);
}在这个结果里面你会发现递归的一个坏处就是他会将所有可以走过的点都去遍历一遍。即便你已经走出了这个迷宫,它还是会将可以走的点都走一遍的。
注:代码是放在VS2013下进行编译执行的。

2819

被折叠的 条评论
为什么被折叠?



