1.问题描述与实现思路
给定下面的迷宫,起点(1,1),终点(8,8),找到一个可能的路径,对于每个位置,约定按照右下左上的顺序查找。使用栈,利用深度优先算法即可找到。
具体操作为从(1,1)开始,Push(1,1),在每个位置P,先判断是否是终点,若是,则完成任务,若不是,则依次判断右下左上有无可行的路径。如果一个新的位置Q(紧邻P)是可以到达的,也即没有墙、也没有走过,那么走到Q,也即Push(Q)。如果P的四周都不能走,那么Pop(P),再次根据上面的要求寻找下一个Q。若最终得到了一个空栈,则问题无解!
-
Initially: (1, 1);
-
Push (1, 2): (1, 1)→(1, 2);
-
Push (2, 2): (1, 1)→(1, 2)→(2, 2);
-
Push (3, 2): (1, 1)→(1, 2)→(2, 2)→(3, 2);
-
Push (3, 3): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3);
-
Push (3, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4);
-
Push (2, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4);
-
Push (2, 5): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)
-
Push (2, 6): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6);
-
Push (1, 6): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6)→(1, 6);
-
Push (1, 5): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6)→(1, 6)→(1, 5);
-
Push (1, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6)→(1, 6)→(1, 5)→(1, 4);
-
Pop(1, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6)→(1, 6)→(1, 5);
-
Pop(1, 5): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6)→(1, 6);
-
Pop(1, 6): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5)→(2, 6);
-
Pop(2, 6): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4)→(2, 5);
-
Pop(2, 5): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4)→(2, 4);
-
Pop(2, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3)→(3, 4);
-
Pop(3, 4): (1, 1)→(1, 2)→(2, 2)→(3, 2)→(3, 3);
-
Pop(3, 3): (1, 1)→(1, 2)→(2, 2)→(3, 2);
-
......
按照上面的步骤不断进行就可以找到一条路径,限于篇幅不再赘述。
可以分析,最坏情况下复杂度O(N*M)。
2.C语言代码实现
2.1 栈的实现
栈的定义如下:
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#define EmptyTOS -1
struct StackRecord
{
int Capacity;//栈的容量
int TopOfStack;//栈顶标志
T* Array;//栈本身,T需要通过typedef为某一个确定的数据类型
};
typedef struct StackRecord* Stack;
/*栈的基本操作*/
int IsEmpty(Stack S);//判空
int IsFull(Stack S);//判满
Stack CreateStack(int MaxElements);//创建堆栈
Stack DisposeStack(Stack S);//销毁堆栈
void MakeEmpty(Stack S);//置空
void Push(T X, Stack S);//入栈
T Top(Stack S);//获取栈顶元素
T Pop(Stack S);//出栈
栈的基本操作实现:
/*栈的操作实现*/
Stack CreateStack(int MaxElements)
{
Stack S;
S = (Stack)malloc(sizeof(struct StackRecord));
if (S == NULL)
{
printf("Out of Space!!!\n");
return NULL;
}
S->Array = (T*)malloc(sizeof(T) * MaxElements);
if (S->Array == NULL)
{
printf("Out of Space!!!\n");
return NULL;
}
S->Capacity = MaxElements;//给容量赋值
MakeEmpty(S);//清空栈
return S;
}
void MakeEmpty(Stack S)
{
S->TopOfStack = EmptyTOS;
}
Stack DisposeStack(Stack S)
{
if (S != NULL)
{
free(S->Array);
free(S);
}
S = NULL;
return S;
}
int IsEmpty(Stack S)
{
return S->TopOfStack == EmptyTOS;
}
int IsFull(Stack S)
{
return S->TopOfStack >= (S->Capacity - 1);
}
void Push(T X, Stack S)
{
if (IsFull(S))
printf("Full Stack!!!\n");
else
{
S->TopOfStack++;//标记移动
S->Array[S->TopOfStack] = X;
}
}
T Top(Stack S)
{
if (!IsEmpty(S))
return S->Array[S->TopOfStack];
}
T Pop(Stack S)
{
if (IsEmpty(S))
printf("Empty Stack!!!\n");
else {
T X = Top(S);
S->TopOfStack--;
return X;
}
}
以上我们便实现了一个基本的栈。
2.2 深度优先算法走迷宫
首先我们需要定义一个Point结构体来储存迷宫坐标:
struct Position {
int x;
int y;
};
typedef struct Position Point;
typedef Point T;//此处类似于C++模板编程,增加栈的泛用性
我们把迷宫本体储存在一个二维数组中,以1代表可行的位置,以0代表墙体,下面的函数实现了迷宫的打印,实际上它也是打印一般矩阵的函数,参数不需要矩阵的行数和列数,只需要一个二级指针即可:
void printMatrix(int** matrix) {
int row = (int)_msize(matrix) / (int)sizeof(int*) - 2;//计算行数
int col = (int)_msize(*matrix) / (int)sizeof(int) - 2;//计算列数
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf("%d ", *(*(matrix + i) + j));
}
printf("\n");
}
}
完成了上面的准备工作,下面就是深度优先算法走迷宫的核心代码,它实际上就是把上文中提到的思路进行了具体实现。进入函数,先计算迷宫的行数和列数,然后创建起点位置的Point结构体入栈,接着开始循环,循环退出条件为空栈,每次进入循环,先判断是否达到终点,若是,则直接break,否则就寻找当前位置的右下左上四个方向那个位置还能够进入,将这个位置入栈,如果不存在可以深入的位置,则说明走到死胡同,出栈。直到最终找到终点或者空栈退出循环。如果有解,那么最终栈里面存在的位置就是一条通路,依次出栈就能够得到结果:
void Solution(int** maze, int x_start, int y_start, int x_end, int y_end) {
int row = (int)_msize(maze) / (int)sizeof(int*) - 1;//计算行数
int col = (int)_msize(*maze) / (int)sizeof(int) - 1;//计算列数
int canSolve = 0;//记录是否有解的标记
Stack S = CreateStack(row * col);
Point start = { x_start, y_start};//起点,{x坐标,y坐标}
Push(start, S);
while (!IsEmpty(S)) {
Point temp = Top(S);
if (temp.x == x_end && temp.y == y_end) {
canSolve = 1;//有解
break;
}
else {
*(*(maze + temp.y) + temp.x) = 0;//走过的地方就不能在走了,视作墙壁
}
if (temp.x + 1 < col && *(*(maze + temp.y) + temp.x + 1) == 1) {
Point right = { temp.x + 1,temp.y};
Push(right, S);
}//当前位置右侧可行,入栈
else if (temp.y + 1 < row && *(*(maze + temp.y + 1) + temp.x) == 1) {
Point down = { temp.x,temp.y + 1};
Push(down, S);
}//当前位置下侧可行,入栈
else if (temp.x - 1 >= 0 && *(*(maze + temp.y) + temp.x - 1) == 1) {
Point left = { temp.x - 1,temp.y};
Push(left, S);
}//当前位置左侧可行,入栈
else if (temp.y - 1 >= 0 && *(*(maze + temp.y - 1) + temp.x) == 1) {
Point up = { temp.x,temp.y - 1};
Push(up, S);
}//当前位置上侧可行,入栈
else {
Pop(S);
}//无处可去,只能出栈
}
if (IsEmpty(S)) {
canSolve = 0;//空栈,无解
}
if (!canSolve) {
printf("There is no solution to this maze!\n");
}
else {
/*有解,开始回溯*/
printf("The maze can be solved, the shortest path is (row,column):\n");
while (!(S->TopOfStack == 0)) {
Point temp = Pop(S);
printf("(%d,%d)<--", temp.y, temp.x);
}
printf("(%d,%d)\n", Top(S).y, Top(S).x);
}
}
2.3 人机交互部分
上文中只提到了如何去实现栈和走迷宫,下面给出main函数的实现,其中也包含了从键盘或者文本文件读取迷宫的部分。
int main() {
char source;
int is_file = 0;//记录输入来源,1-文本文件,0-键盘
printf("Please select whether you want to input data from the keyboard or read data from a text file?\nK-form the board\tF-from a file\n");
while (scanf("%c", &source)) {
char error;
if (source == 'F' || source == 'f') {
is_file = 1;
/*处理潜在的错误输入*/
while ((error = getchar()) != '\n' && error != EOF);
break;
}
else if (source == 'K' || source == 'k') {
while ((error = getchar()) != '\n' && error != EOF);
break;
}
printf("The character you entered is not within the required range, please re-enter!\n");
while ((error = getchar()) != '\n' && error != EOF);
}
if (is_file) {
printf("Please enter the absolute address of the data file to be read (for example, \"C:\\user\\data.txt\"), and press Enter after entering.\n");
char file_address[100];
gets(file_address);
FILE* file = freopen(file_address, "r", stdin);
if (file == NULL) {
printf("Error:Failed to open the file. Please restart the application and check the file path and try again.\n");
exit(-1);
}
printf("The file was successfully opened, and the program will read data from the file.\n");
}
if(!is_file)
printf("Please enter the size of the maze (m rows, n columns). Use a space to separate m and n and press ENTER after entering.\n");
int m, n;
while (1) {//接收输入,同时进行一定的错误输入处理
int return_value = scanf("%d %d", &m, &n);
if (m > 0 && n > 0 && return_value == 2) {
break;
}
printf("Input error, please input again!\n");
char error;
while ((error = getchar()) != '\n' && error != EOF);
}
int** maze = (int**)malloc(sizeof(int*) * (m + 2));//此处多申请一个内存位置,防止越界
if (maze != NULL) {
for (int i = 0; i < n; i++) {
*(maze + i) = (int*)malloc(sizeof(int) * (n + 2));
}
}
if (!is_file) {
for (int i = 0; i < m; i++) {
printf("Please enter the row %d of the maze(1 represents the pathway, 0 represents walls and obstacles).\n", i + 1);
for (int j = 0; j < n; j++) {
int temp;
scanf("%d", &temp);
if (!(temp == 0 || temp == 1)) {
/*处理可能的错误输入*/
printf("Input error, please enter 1 or 0!\n");
char error;
while ((error = getchar()) != '\n' && error != EOF);
--j;
continue;
}
*(*(maze + i) + j) = temp;
}
}
}
else {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int temp;
scanf("%d", &temp);
*(*(maze + i) + j) = temp;
}
}
}
printf("The maze you is:\n");
printMatrix(maze);//打印迷宫
if(!is_file)
printf("Please the starting point (row, column), row between %d to %d, column between %d to %d.\n", 0, m - 1, 0, n - 1);
int x_start, y_start;
while (1) {//接收输入,同时进行一定的错误输入处理
scanf("%d %d", &y_start, &x_start);
if (x_start >= 0 && y_start >= 0 && x_start <= n - 1 && y_start <= m - 1) {
break;
}
printf("Input error, please input again!\n");
char error;
while ((error = getchar()) != '\n' && error != EOF);
}
if(!is_file)
printf("Please the ending point (row, column), row between %d to %d, column between %d to %d.\n", 0, m - 1, 0, n - 1);
int x_end, y_end;
while (1) {//接收输入,同时进行一定的错误输入处理
scanf("%d %d", &y_end, &x_end);
if (x_end >= 0 && y_end >= 0 && x_end <= n - 1 && y_end <= m - 1) {
break;
}
printf("Input error, please input again!\n");
char error;
while ((error = getchar()) != '\n' && error != EOF);
}
printf("The starting point is (%d,%d), the ending point is (%d,%d).\n", y_start, x_start, y_end, x_end);
Solution(maze, x_start, y_start, x_end, y_end);
system("pause");
return 0;
}
上面这段代码对于C语言初学者而言还是有一定参考价值的,其中设计了如何重定向输入流,如何处理用户的非法输入,如何创建和使用二维数组等等。下面让我们看看实际效果。
3.实际运行结果
上图是一个走一般迷宫的实例,可以分析该程序能够实现任务,并且最后给出路径。
当然,深度优先算法实际上是存在不足的,比如上面的这个迷宫,很明显有更快的路径走过去,但是由于我们设计它按照右下左上的顺序去遍历,结果它就走了一条最远的路径,这个问题实际上可以通过采用广度优先算法,或者在走迷宫中称作“洪水算法”去弥补,详情请见后续文章~
当然,对于无解的情况也是可以很轻松应对的。
4.代码与测试用例
写在最后,我本人在学习过程中也经常使用优快云查资料,但是发现很多资源都需要开会员或者付费才能下载,我个人很不喜欢这种方式,当然,我不是不认可为知识付费这一行为,只是更喜欢开源精神,因此,最后也把本文中完整的代码和测试用例附上,希望能够帮到你~