【数据结构】详解用C语言回溯法实现迷宫的递归求解

迷宫问题是一个比较复杂的问题,但也是可以掌握的。它通常出现在数据结构的栈部分。然而,在用栈求解前,我们最好先用传统的递归方法编写一遍,再用栈改写优化。本文全篇参考殷人昆版《数据结构》,感谢高手推荐这本好书。欢迎评论区指正。

题干描述

请你帮助行人,在一个四周围上墙壁,中间部分是m\times n的有墙壁也有通路的迷宫中,找到一条从迷宫入口到出口的通路。

这里不要求找到的通路是“最短的”,“耗时最短的”,能走通就行。

方法介绍

回溯法(试探法):一步一步向前试探,当某一步有多种选择时,先选择任意一种(选任意一个方向走),若这种选择暂时可以(它的下一个格子是0),则继续向前;若发现达到某步后无法再前进,就回到上一步重新选择。

递归思想:递归是一种算法或函数的编程技巧,在递归过程中,函数通过调用自身来解决问题。递归算法通常使用一个或多个基本情况来停止递归,并使用递归调用来解决问题的其余部分。有些递归问题可以用栈改写为非递归,提升时间和空间效率。

思路准备

将“迷宫”抽象成二维数组,能通的地方用1表示,不能通的地方用0表示。抽象过程如下:

不考虑围墙:

考虑围墙:

最终这个由0和1组成的二维数组就是我们要的。该数组不一定是方阵,即行列数可以不相等。若迷宫本身是m\times n的数组,则用数组maze\left [ m+2 \right ]\left [ n+2 \right ]表示。多出来的两行两列是围墙。围墙格子(包括出入口)全部用1表示,至于为什么出入口也用1,后续会解释。

有了迷宫,我们还需要一个前进方向表表示和记录在每个格子的前进方向和前进距离。考虑构造一个数组,记录移动方向。在迷宫平面xOy中,以朝向出口的方向为正,以其反方向为负。如果我们建立的迷宫中,出口在入口的右下方,那么右和下的位移都记为+1。

在书中,该数组是一个结构体数组。它不仅记录了x,y方向的前进量,还有一个char类型的数据表示“从哪个方向来的(从哪个方位,NESW,前进的。)”思路中这一步或有画蛇添足之嫌,但这与最后的输出结果有关。

我们要用什么形式输出数组?由于递归调用,最先输出的是实际行走的最后一步,逐次输出之前的步直到实际行走的第一步。用NESW表示上一个点的行走方向会比较清晰。

输出解释

上图中的数组就是书中题目的数组。你也可以建立自己的迷宫。博主在这里直接用书上的了,在主函数中初始化及输出如下:

int maze[M][N]={//逐个键入数字0和1
1,1,1,1,1,1,1,1,1,1,1,1,1,
0,0,0,1,1,1,0,1,0,0,0,0,1,
1,1,0,1,0,0,0,1,0,1,0,1,1,
1,0,0,1,0,1,0,1,0,1,0,0,0,
1,0,0,0,0,1,0,1,0,1,1,1,1,
1,0,1,1,1,1,0,1,0,1,1,1,1,
1,1,1,0,0,0,0,1,0,0,0,0,1,
1,1,1,0,1,1,1,1,1,1,1,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,
};
int i,j;
printf("迷宫如下\n");
for (i = 0; i < M; i++)
{
	for (j = 0; j < N; j++)
		printf("%2d", maze[i][j]);
	printf("\n");
}

前进方向示意图

前进方向表

前进方向结构体代码
typedef struct offsets{//offsets:偏移量
     int a,b;//a为x轴方向的偏移量,b为y轴方向的偏移量
     char *dir;//dir由上一格从哪个方向得到
};
offsets move[4];//各方向偏移表

主函数中应有按照表格对move初始化的步骤如下(NESW编号颠倒不影响结果):

move[0].a=-1; move[0].b=0; move[0].dir='N';
move[1].a=0; move[1].b=1; move[1].dir='E';
move[2].a=1; move[2].b=0; move[2].dir='S';
move[3].a=0; move[3].b=-1; move[3].dir='W';
工具的准备并未到此结束。在迷宫求解中,一旦进行到某个位置,则标记该位置已经来过,不能再走,以免重复穷举。建立标记矩阵mark\left [ m+2 \right ]\left [ n+2 \right ],对于来过的位置,mark[i][j]=1.
初始化标记矩阵时,围墙全是1,迷宫内部全是0.
mark[M+2][N+2];int i,j;
for(i=0;i<M+2;i++)
{
  mark[i][0]=1;
  mark[i][N+1]=1;
}//围墙
for(j=0;j<M+2;j++)
{
  mark[0][j]=1;
  mark[M+1][j]=1;
} //围墙
for(i=1;i<M+1;i++)
{
  for(j=1;j<N+1;j++)
    mark[i][j]=0;
}//内部

上机实现

完整代码

如果上文令你产生了疑惑,建议仔细阅读下列代码好注释,或许有所帮助。有些细节与vs编译器自身有关。

#include <stdio.h>
#include <stdlib.h>
constexpr auto M = 11;//迷宫内部9行
constexpr auto N = 13;//迷宫内部11列
constexpr auto direct = 4;
constexpr auto OK = 1;
constexpr auto FALSE = 0;
typedef int Status;
typedef struct offsets {
	int a, b;
	char dir;
};
Status SeekPath(int maze[][N],int mark[][N],offsets move[],int x,int y,int s,int t,int m,int p)
{
	/*二维数组作形参,不要省略第二维的大小。*/
	/*maze是迷宫矩阵,mark是标记矩阵。x,y是初始位置,可以是迷宫内部的任意位置;s,t是入口;m,p是出口
	事实上,这里的x,y就定义成s,t的值。如果你想探寻从非入口走出迷宫的路,x,y,s,t就按照那个位置定义。
	否则找路函数return 0的条件会很难办*/
	int i, g, h;//记录当前所在位置
	char d;//记录方向
	if (x == m && y == p) return OK;//初始位置就在出口,直接返回1。这里不是递归出口。
	for (i = 0; i < direct; i++)//四个方向都做试探,哪个方向。。。。。。就退出循环
	{
		g = x + move[i].a;
		h = y + move[i].b;
		d = move[i].dir;//从[x][y]向NESW任一方向走一步到[g][h]
		if (!maze[g][h] && !mark[g][h])//新位置既不为1,又未被标记
		{
			mark[g][h] = 1;//从[g][h]位置开始递归试探,标记当前[g][h],以后不能再来这里
			if (SeekPath(maze, mark, move, g, h, s, t, m, p))//这里x,y变成g,h了!!
			/*再次调用找路函数,试探NESW四个方向任一。若*/
			{
				printf("(%d, %d, %c)", g, h, d);//这里是递归出口。走到这一步
				return 1;
			}
		}
	}
	if (x == s && y == t)//走到这一步说明所有可能都试探完了还在原点(出发点),说明这些可能都是死路。
		printf("迷宫中没有通路\n");
	return 0;
}
int main(void)
{
	int i, j;
	int maze[M][N] = {
		1,1,1,1,1,1,1,1,1,1,1,1,1,
		0,0,0,1,1,1,0,1,0,0,0,0,1,
		1,1,0,1,0,0,0,1,0,1,0,1,1,
		1,0,0,1,0,1,0,1,0,1,0,0,0,
		1,0,0,0,0,1,0,1,0,1,1,1,1,
		1,0,1,1,1,1,0,1,0,1,1,1,1,
		1,1,1,0,0,0,0,1,0,0,0,0,1,
		1,1,1,0,1,1,1,1,1,1,1,0,1,
		1,0,0,0,0,0,0,0,0,0,0,0,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,
	};//别忘了分号
	printf("迷宫如下\n");
	for (i = 0; i < M; i++)
	{
		for (j = 0; j < N; j++)
			printf("%2d", maze[i][j]);
		printf("\n");
	}
	int mark[M][N]{};
	for (i = 0; i < M; i++)
	{
		mark[i][0] = 1;
		mark[i][N - 1] = 1;
	}//围墙
	for (j = 0; j < N; j++)
	{
		mark[0][j] = 1;
		mark[M - 1][j] = 1;
	} //围墙
	for (i = 1; i < M - 1; i++)
	{
		for (j = 1; j < N - 1; j++)
			mark[i][j] = 0;
	}//内部
	offsets move[direct]{};
	move[1].a = 0; move[1].b = 1; move[1].dir = 'E';
	move[2].a = 1; move[2].b = 0; move[2].dir = 'S';
	move[3].a = 0; move[3].b = -1; move[3].dir = 'W';
	move[0].a = -1; move[0].b = 0; move[0].dir = 'N';
	int s = 1, t = 1, m = 3, p = 11;
	int x = 1, y = 1;
	printf("搜寻路径如下\n");
	SeekPath(maze, mark, move, x, y, s, t, m, p);
	printf("(%d, %d, E)", s, t);//如果这里不加这行,最终输出结果不含出口点(见图)。这里的方向是人工判断的。
	return 0;
}

运行结果

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值