前言:

        我们之前使用深度优先遍历的方法是 , 只派一个人去探索迷宫,记录其路线 和每个节点走过的方向 , 一条道走到黑 , 当路不通时候,回溯到上一个节点 的下一个方向 , 直到这个节点全部方向都遍历完,我们就放弃路线上的这个节点 ,接着回溯到这个节点的上一个节点的另一个方向 , 我们一定可以成功 ,因为我们就算不成功,早晚也可以回溯到起点 ,从起点的下一个方向出发 ,当把所有节点所有方向都遍历完,如果有解, 我们则可以找到出口 , 无解则输出无解 . 


这种方法 ,有两个弊端,一个好处 ,好处就是 ,不耗费存储空间 ,我们一直都是在一个栈里进行反复的弹压元素 ,

弊端就是我们反复回溯,耗费时间 , 并且我们所寻找的路线不一定是最佳路线,不是最短路线 ,我们可能一开始就不在最佳路线上,然后通过回溯,后续到了最佳路线中一个节点 , 然后到达终点 ,尽管我们成功了,但是我们第一时间没有在最佳路线上. 

       广撒网 ,团结就是力量  (深度优先遍历)

看了前面的单人冒险 ,的确让我们捉急 , 我们现在增大我们的团队实力 ,变成孙悟空 ,拔一根猴毛 ,即可变出猴子 , 前往分岔路口的不同方向 , 只要人够多 ,我们就可以到达终点 ,并且最优路线是最短的 ,我们的探索人数够多 ,只要有解 ,就一定能够在最短的时间, 去到达最优路线 .一旦到达终点 ,我们即可吹一口仙气 ,把孩儿们收回来,探索成功. 

我们现在用人脑来模拟一遍, 团队探索的过程:

第一步: 我们从(1,1)出发 ,  标号① ,孙悟空真身到达现场


 注意: 标号行列 ,(行 , 列)


第二步:  从①(1,1) 出发 , 我们遇到了两个方向 , 拔下猴毛,变成两个猴子,分别到(2,1) , (1,2) ,标号② (记作 标号记作第二次变身)



 第三步: 我们让二号变身猴开始走迷宫 ,我们现在又发现一个问题 ,两个②号猴同时走的话 ,会在(2,2)  相遇 ,我们怎么判断是哪一个路线上的猴子呢? 



我们此时仿佛遇到了瓶颈, 当两个路线的猴子,都在 (2,2)相遇的时候 ,我们需要判断是哪个路线上的猴子吗? 或者存储他们的信息吗? 方便后续寻找路线? 

引论:

我们现在要找的是 长度最短 ,寻找用时最少的 最优路线 ,就像我们"黄河之水天上来,奔流到海不复回" , 两个路线汇合 ,这个说明两个路线都可以到达此(2,2)处

我们看一下最终的图解路线:


(2,1)和(1,2)处的分身② ,进行第三次分身 到了(3,1),(2,2) ,标号为③

假如此时的目标地点是(3,2) ,我们两个路线都可以访问得到 ,所以两个路线交汇 ,不必记录是哪两个路线交汇此处,反正都是同一个级别的分身 所能访问到的地方 ,

当一条路堵死时候,我们就自动放弃, 寻找同一层次的分身进行下一个级别分身 


第四步 :三级分身③ (3,1)分身成(4,1),(3,2),标号为④,  同一级别的猴子③ (2,2),也交汇分身到(3,2),都为④


 第五步: 四级分身④ (4,1)分身成⑤(5,1) , 另一同级分身④(3,2) 分身成 ⑤(3,3)


第六步:五级分身 ⑤(5,1)分身成 ⑥(6,1) ,⑥(5,2) , 另一五级分身⑤ (3,3)分身成 ⑥(3,4)


 第七步 : 六级分身 ⑥(6,1) 分身成 ⑦(7,1) , 另一同级分身⑥(5,2) 分身成⑦(5,3) , 第三个同级分身 ⑥(3,4) 分身成 ⑦(2,4)


第八步 : 七级分身 ⑦ (7,1) ,无路可走,放弃   

另一分身 ⑦(5,3) 分身成八级分身 ⑧(6,3) , 

最后一个同级分身 ⑦(2,4) 分身成 ⑧(1,4)  , ⑧(2,5)


第九步:  第八级分身 ⑧(6,3)分身成 (6,4) ,

⑧(1,4)分身成 ⑨(1,5) 

⑧(2,5)分身交汇到 ⑨(1,5) 和 ⑨(2,6)


第十步 : 九级分身 ⑨(6,4) 分身成 ⑩(6,5) 

⑨(1,5) 分身成 ⑩(1,6) 

⑨(2,6) 分身交汇到 ⑩(1,6)


第十一步 : 十级分身 ⑩(6,5) 分身成 ⑪(5,5)  和 ⑪(7,5)

⑩(1,6) 遇到无解路线 ,放弃


第十二步 : 十一级分身 ⑪(5,5) 分身成 ⑫(4,5) 和 ⑫(5,6)

⑪(7,5) 分身成⑫(8,5)


第十三步: 十二级分身 ⑫(8,5) 分身成 ⑬(8,4) 和 ⑬(8,6) 

⑫(4,5) 分身成 ⑬(4,6) 

⑫(5,6) 分身交汇到 ⑬(4,6)

第十四步 : 十三级分身 ⑬(4,6) 分身成 ⑭(4,7)

⑬(8,6) 分身成 ⑭(8,7)


 第十五步 : 十四级分身 ⑭(8,7) 分身成⑮(8,8) 

已经达到终点 ,触发奖励 ,达到最优路线 


我们此时已经到达最优路线 ,线路最短 ,用时最短, 

能直接宣布胜利吗? 恐怕为时过早 

我们发现一个问题 ,⑭(4,7) 和 ⑭(5,8) ,如果我们先进行这两个路线的遍历

⑭(4,7)分解成 ⑮(3,7)和 ⑮(4,8)

⑭(5,8)分解成 ⑮(6,8),交汇⑮(4,8)

最后才判断 十四级分身 ⑭(8,7) 分身成⑮(8,8) 达到终点 ,

所以,我们在哪一级分身到达终点 ,需要把那一级的分身都分完 ,再宣布结果也不迟 (如果我们会发现那一级有两条最优路线呢?发现惊喜!)

那我们如何区分哪条路线是我们要的呢?

我们就需要从终点进行回溯了, 按照分身级别 

⑮(8,8)-->⑭(8,7)-->⑬(8,6)-->⑫(8,5)-->⑪(7,5)-->⑩(6,5)-->⑨(6,4)->⑧(6,3)-->⑦(5,3)------

----->⑥(5,2)-->⑤(5,1)-->④(4,1)-->③(3,1)-->②(2,1)-->①(1,1)

我们可以对路线上进行标记 ,然后我们把其输出即可 ,或者我们在回溯的时候 ,就把路线记下来 ,然后进行对应逆置即可. 

拓展 :当我们遇到两条最优路线时候 ,可以启用相关的存储,将其存储即可

我们的人脑模拟已经完成 ,接下来就交给机器了

下面让我们把思路转化为机器的程序:

我们回顾我们的过程 , 我们是在迷宫上进行探索的,首先把迷宫表示出来


我们要存储数组 ,就要知道数组的特性 , 涉及到二维数组 ,我们第一个字符代表一行 ,第二个字符代表一行的第几列 ,我们设 x  为行数  ,y 为列数(具体问题具体分析)

然后 ,我们从(1,1)出发 , 同时前往此节点能够通行的所有节点 ,并前进一步 ,直到成功 ,

接着进行回溯 ,这时候我们会发现, 如何找到上一个节点,这是个问题 , 我们人脑可以知道 ,逆序的数字 ,就是我们的路线 ,那机器是不知道的 ,所以我们最好在每个节点留下一点线索 ,以方便我们后续的回溯查找 , 

定义方块结构体

typedef struct
{
    int x,y;//方块的位置
    int pre; //方块在本路径上,上一个方块在队列中的下标
}Box;    //方块类型

我们接着模拟探索过程 ,我们既然从①开始探索 ,我们其实没有三头六臂 ,孙悟空拔猴毛的时候,也要看一看四周哪些是通路才变分身的

所以我们定义一个节点的方向 ,以及对应方向所能进行的移动操作


当我们初始在(1,1)时候 ,方向先设置为 -1 ,然后通过累加, 进行各个方向的模拟通行, 然后如果那个方向是 0 ,则代表通路 ,我们就可以进行对应的分身 ,此分身要表明 自己的坐标 ,从哪一个分身来的 ,以便于后续回溯

我们先遍历到右方向 , 然后分身 ②(1,2) ,  下方向,分身②(2,1)

此时我们又遇到一个难题 , 如何进行存储 分身呢?

        我们先看一下此次信息插入和信息检索的特点 , 然后进行对应的数据结构存储

我们从初始地开始 ,①(1,1) 通过遍历到有通路的点②(1,2) , ② (2,1)

接着进行分身 ,我们就进行分身存储 ,然后 再接着遍历存储的 二级分身 ,

二级分身的所有通路遍历完了 ,再将通路存储 


我们其实是在一端进行探索 ,一端进行存储后续节点 

这很符合队列的操作


我们在队首节点进行探索 ,探索到通路之后 ,就可以把通路节点加入到队尾 , 我们因为先来后到的原因 ,我们总是能先把同一级别的分身探索完, 才能探索后面的节点 , 这也保证了 ,我们在平等的探索同一级别的分身 

但世间哪有绝对的平等 ,同一级别的分身 , 我们在探索一个分身时候 , 另外的分身处于空闲状态 ,这就造成了 上面的交汇的可能 ,其中一个分身把四周都标记分身完了 ,另一个同级分身 ,进行分身的时候 ,看到已经被同级的分身占领 ,只能进行入流 ,或者放弃通路 


例如, 上面的(3,2) 节点 , 我们可以由(3,1) 或 (2,2) 分身得到 ,但是如果那样的话 ,就需要我们对每一个级别的分身占领的节点 ,进行特殊的标记 ,然后这也会造成回溯的问题 ,这可能需要后续我们使用树形结构来解决了 , 


由于作者能力有限 ,对于这种同一级别的分身交汇现象 ,由于 队列的局限性 ,我们虽然保证了同一级别的分身 ,优先进行探索,并且在队尾加入后继节点 ,但是 我们在同一级分身,在探索到理论上都可以通过的节点的时候 ,由于同一级别的节点还是有先后顺序 ,这就造成了理论上同一节点都可以占领的节点 ,被其中一个节点占用了 ,另一个同级节点在探索的时候 , 只能选择放弃 ,这也就造成了 ,我们只能选择出一条最优路线 , 而可能的多条最优路线 ,就 因为交汇而不能通过了


拓展联想:

如果再在每一个分身层级加一个标记的话 ,如果探索到同类标记就可以进行标记探索 , 而进行回溯的时候 ,如果遇到多个可行路线的话 ,我们可以进行分叉存储 ,这可能使用到后续的树形结构 ,我们现在只能利用队列 ,来求出一条最优路线

利用队列存储节点 , 队首遍历通路 ,队尾插入分身节点 ,求最优路线的可行性

 我们派大量人力 , 进行分头查找的时候 , 当探索到一条最优路线的时候 , 我们就会停止其他探索 ,

说明我们是在一个共同的数据路线上进行的探索 , 探索过的节点 ,我们不会再次踏足 ,这也说明了此算法的有穷性 .

虽然

我们在探索的时候 , 在同一级别的分身 ,都可以通过一个节点 ,但是被另一个同一级别的节点抢占标记了,回溯的时候 ,也只会回溯那个节点 ,而不会回溯被抢占的节点 , 这也就造成了,无法探索出多条最优路线 ,

但是

这不影响我们探索出最优路线,到达终点

有一句话,叫做"黄河之水天上来 ,奔流到海不复回" ,我们虽然没有对交汇的点进行标记,但是同一级别的分身进行了标记, 当发生交汇的时候 ,只有一种可能 ,就是同一级别的分身,处在对角线 ,而两者都是最优路线 ,因为我们路线长度都一样 ,我们因为队列先后顺序的限制和存储结构的限制 ,不能对两者都进行标记 ,所以也就印证了之前的分析 ,我们利用队列最多只能探索出一条最优路线 .

但是此队列一定也是最优路线 



如果 ③(3,1)  抢先对④(3,2) 进行标记 ,说 ④(3,2)来自(3,1) ,那到③(2,2) 对④(3,2)进行探索的时候 ,发现已经被占领了 ,由于队列的局限性 ,我们不能回溯多个路线 ,所以,我们就放弃③(2,2)

对④(3,2)的交汇标记 , 所以我们就只能探索出一条最优路线了

我们接下来就对队列存储结构进行定义

队列存储,我们选用较为简单的数组存储 ,然后用队头和队尾指针指向,

typedef struct
{
	Box data[MaxSize];
	int front,rear;
}QuType;

定义了存储分身的结构, 我们下面就开始队头探索通路

我们从队头开始模拟

第一步: 我们在(1,1)出发 ,没有上一个节点pre ,就暂且置为-1

 第二步 : 我们探索队列队首的[0]处节点的通路 ,需要从四个方向,全部探索完 ,然后把通路节点入队

 第三步 : 接着探索队头[2]处通路 ,然后把通路节点入队 ,并且我们入队的坐标信息包括坐标位置和从哪个节点过来的 (为了后续回溯方便,我们在数组队列里面存储上一个节点的信息是,上一个节点的数组坐标序号)

 第 ....步: 我们逐个探索队头的节点 ,如果有通路 ,就把通路节点入队 ,无通路则探索下一个 ,当我们入队的节点到达终点时 ,我们即可跳出遍历

然后 ,我们就需要把路线输出了 

我们使用数组存储路线元素 ,一个方便之处就是 ,我们虽然逻辑上出队了元素 ,但是我们的数组里面的节点路线并没有删除 ,只要数组不是循环数组 ,我们就可以根据终点的队尾节点的信息 ,进行回溯 ,通过数组位序 ,就可以找到最优路线上一个节点的信息 ,通过上一个节点的信息又可以探索上上个节点的数组位序 ,从而把相应的坐标都做标记, 通过遍历 ,即可输出最优路线.

下面把我们的逻辑进行程序化

探索从(1,1) 到(X,Y)的路径
(1)首先将(1,1)入队
(2)在队列qu不为空时循环:
出队一次(由于不是环形队列 该出队元素仍在队列中),称该出兑的方块为当前方块,
front为该方块在qu中的下标.
    ①如果当前方块是出口 ,则输出路径并结束
    ②否则按顺时针方向找出当前方块的四个方位中可走的相邻方块(对应的mg数组值为0),
    将这些可走的相邻方块均插入到队列qu中,其pre设置为本搜索路径中上一方块在qu中的下标
    值,也就是当前方块的front值,并将相邻方块对应的mg数组值置为-1 ,以避免回过来重复搜索
(3)若队列为空仍未找到出口,即不存在路径

代码实操:

注意:我们设置了出口,一个是,比较队头元素,如果和目的坐标重合时候,我们就可以输出路径,

但是大部分时间,我们都是通过队尾元素优先访问到目的地了,我们此时就应该停止探索了,直接调用

 输出路径的函数了,注意传入的参数,是找到目的地后,队列数组的坐标

//头文件
#include <stdio.h>
//定义运算规模
#define MaxSize 100
//定义迷宫规模
#define M 8
#define N 8
//int mgpath(int xi,int yi,int xe,int ye);
//void print(QuType qu,int front);
//用数组来描述迷宫
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}
};
//定义方块的信息,包括坐标和回溯需要的信息
typedef struct
{
	int i,j;	//方块在地图的位置
	int	pre;	//本路径中上一个方块在队列的下标
}Box;
//定义队列的数据结构(包括队首和队尾指针,存储数据节点的数组)
typedef struct
{
	Box data[MaxSize];
	int front,rear;	 //队头指针和队尾指针
}QuType;			 //定义顺序队类型

//当找到最优路线,制作输出路径的函数
//从队列qu中输出路径, 当front探索到终点时候
//传入路径最后的数组位序front
void print(QuType qu,int front)	  
{
	//当然是回溯,然后标记路径,然后输出最优路线坐标
	int k = front;
	int j;
	int ns = 0;
	printf("\n");
    //反向找到最短路径,将该路径上的方块的pre成员设置成-1	
	do
	{
		j=k;
		k=qu.data[k].pre;
		qu.data[j].pre = -1;
	}
	while(k!=0);	//初始地就是数组位序为0处
	printf("迷宫路径如下:\n");
	k=0;
	//正向搜索pre为-1的方块,即构成正向的路径
	while(k<=front)
	{
		//如果数组元素pre为-1,则将其元素的坐标输出
		if(qu.data[k].pre == -1)
		{
			//这里可以设置换行输出
			ns++;
			printf("\t(%d,%d)",qu.data[k].i,qu.data[k].j);
			if(ns%5==0)
			{
				printf("\n");	//没输出5个方块后换行
			}			
		}
		k++;
	}
	printf("\n");
}

//我们首先传入出发地和终点的坐标
//搜索路径为(xi,yi)-->(xe,ye)
int mgpath(int xi,int yi,int xe,int ye)
{
	//定义存储坐标的变量
	int i,j;
	//然后我们要存储节点数据,所以先把队列定义
	QuType qu;
	//初始的时候,队头和队首指针指向-1,方便后续累加使用
	qu.front = qu.rear = -1;
	//现在让猴哥落地到出发地,队尾指针加一
	qu.rear++;
	//对应队列元素的节点坐标
	qu.data[qu.rear].i = xi;
	qu.data[qu.rear].j = yi;//(xi,yi)进队
	//因为此时是初始节点入队,其未有上个路线节点,我们置为-1
	qu.data[qu.rear].pre = -1;
	//相应地图坐标已经占用
	//将其赋值-1,以避免回过来重复搜索
	mg[xi][yi] = -1;	
	//接下来开始探索队首节点的节点,然后将其通路节点入队
	//队列不为空且未找到路径时循环
	int find =0; //未找到路径
	//我们入队元素,就表明我们已经触发寻找机制,直到队首和队尾重合
	//即使把所有节点都探索完为止
	int di; //定义方向
//	int k_1=0;  //测试次数
	while(qu.front!=qu.rear && !find)
	{
	 //出队,由于不是环形队列,该出队元素仍在队列中
		qu.front++;
		//对比队列对头的元素,是否为出口坐标
		i = qu.data[qu.front].i;
		j = qu.data[qu.front].j;
		if(i == xe && j==ye) 
		{
//			printf("你是傻逼\n");
//			printf("k的值是:%d\n",k_1);
			//如果找到重点,则标记find,跳出循环
			find = 1;
			//调用print函数输出路径
			print(qu,qu.front);
			return (1);
		}
		//如果没有找到,我们则
		//循环扫描每个方位,把每个可走的方块插入队列中
		for(di = 0;di<4;di++)
		{
			switch(di)
			{
            case 0:
                i=qu.data[qu.front].i-1;
                j=qu.data[qu.front].j;
                break;
            case 1:
                i=qu.data[qu.front].i;
                j=qu.data[qu.front].j+1;
                break;
            case 2:
                i=qu.data[qu.front].i+1;
                j=qu.data[qu.front].j;
                break;
            case 3:
                i=qu.data[qu.front].i, j=qu.data[qu.front].j-1;
                break;
			}
			//如果对应的方向可走,则进行入队操作
			if(mg[i][j]==0)
			{
				//队指针加一
				qu.rear++;
				//队数组元素坐标赋值
				qu.data[qu.rear].i=i;
				qu.data[qu.rear].j=j;
				
				//队元素回溯数据,指向路径中上一个方块的下标
				//此时到此节点的数据,就是队首指针指向的元素
				qu.data[qu.rear].pre = qu.front;
				//对应地图坐标被占用,记作-1
				//将其赋值-1,以避免回过来重复搜索
				mg[i][j]=-1;

//				k_1++;
//				printf("k运行次数%d\n",k_1);

			}
		}
	}
	//当访问了,所有能到达的所有节点,仍未找到则跳出,返回0	
	return (0);
}

//主函数传入入口和重点
int main()
{
	int h;
	h = mgpath(1,1,M,N);
	if(h==1)
	{
		printf("恭喜你,找到出口!\n");
	}else if(h==0)
	{
		printf("很抱歉,未能找到出口!\n");		
	}
	return 0;
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐