迷宫-广度优先搜索-最短路径并打印该条最短路径

迷宫类的广度优先搜索与树的层次遍历类似但是又有不同,比如树的层次遍历不需要判重即不需要标记,树也不基本需要判断是否可以入列(即下一节点是否合法);而图需要标记进行判重,并且入列时还需要考虑状态是否合法,合法才入列。

目录

1.本文例子的迷宫如下:

2.帮助理解代码:

3.BFS的一般步骤

4.本文的队列结构

5.广度优先搜索核心

6.所有代码

7.结果


1.本文例子的迷宫如下:

0

0

1

0

0

0

0

0

0

0

1

0

0

0

1

0

       这里介绍一种极简DP,可以记录迷宫上的每个点到起点的最短距离,有时候题目变形,只需要对DP操作即可,这种同时也可以将DP用于判重的记录表;

       如果求最短路径不要求记录路径的话,则队列节点中不需要多余的指针;这里加上只要碰到需要求最短的路径并打印都可以使用。要求打印所有路径目前还没想好怎么做,对于这句话不是很理解:“如果是求所有路径,注意此时,状态转换图是DAG,即允许两个父节点指向同一个子节点。具体实现时,每个节点要“延迟”加入到已访问集合 visited ,要等一层全部访问完后,再加入到 visited 集合。”目前的想法是如果是所有路径,考虑深度优先实现

2.帮助理解代码

      (1)方向表示:把地图左上角当坐标原点,横着的是y增长方向,竖的是x增长方向,最后一行的1表示(3,2)及3行2列

   ,那么方向左就是“y轴”减1,即列减1,(0,-1)表示左,(0,1)表示右

	/*方向的模拟,坐标原点在左上方,向左的话其实是列减1*/
static const int s_dirs[MAX_DIR][DIM] = {{0,-1},{0,1},{-1,0},{1,0}};//左,右,上,下

(2)DP数组:本文的DP数组其实就是地图的矩阵,值为-1的点表示节点未遍历,非-1的值表示节点已经遍历,并且是该点到起点的最短距离,如dp[2][2]=3,表示2行2列的点到起点的距离是3

(3)路径打印:用到递归,当然也可以用栈,但是比较麻烦,又需要实现,虽然不难

//**********递归打印***********
void print_path(POINT_T* point)
{
	if(point->pre == NULL)
	{
		printf("(%d,%d)",point->x,point->y);//为了把第一个打出来
		return;
	}
	else
	{
		print_path(point->pre);
		printf("->(%d,%d)",point->x,point->y);
	}

(4)二维数组的访问

对于直接“栈上变量二维数组”,通过参数传进来,对元素的访问一般转为一维数组的形式访问。

a.调用者A定义

	int maze[MAZE_MAX_X][MAZE_MAX_Y] = {
		{0,0,1,0},
		{0,0,0,0},
		{0,0,1,0},
		{0,0,1,0},
	};
    B(maze,max_x,max_y);

b. B中的访问maze的元素则用一维的

int B(int** maza,int max_x,int max_y)
{
    //访问maze[i][j]
    *(((int*)maze)+max_y*i+j) = 6;
}

当然如下两种maze的定义则不需要,传参后仍然可以maze[i][j].

a.一维度和二维度都是堆上的

int **maze;
maze = (int**)malloc(sizeof(int*)*4);
for(i = 0;i < 4;i++)
{
    maze[i] = (int*)malloc(sizeof(int)*4);
    memset(maze[i],0,sizeof(int)*4);
}
  • 一维是栈上的,二维度是堆上的
int *maze[4]={0};
for(i = 0;i < 4;i++)
{
    maze[i] - malloc(sizeof(int)*4);
    memset(maze[i],0,sizeof(int)*4)
}

3.BFS的一般步骤

1) 查找给定的结点s可以达到的点,并加入广度优先搜索树中(以队列实现,下称队列)同时标记为已访问;

2) 遍历队列,判断队列中的结点是否满足给定的条件或要求,满足则结束搜索;若不满足结束条件,对队列中的每个结点继续查找其可以达到并且未使用过的结点,加入队列中,并记录为已使用。

3)重复2。

注意:第二步中的记录已访问需要根据实际情况选择是每个加入后立马记录(这样的话,同级的兄弟结点也不能重复使用),还是同级的全部完成后再记录已访问(这样同级的兄弟结点允许重复使用)。

4.本文的队列结构

队列节点相当于实现为链式的可以找到其前面一个坐标点

typedef struct Node_
{
	int x;
	int y;
	//前驱节点,为了能打印出路径(这利用到递归打印了),因为入队列的不一定都是有用的
	struct Node_ *pre;
}POINT_T;

typedef struct Que_
{
	int size;
	int cap;
	int head;
	int tail;
	//队列中这个搞成数组的形式
	POINT_T* path;
}QUE_LINE;

5.广度优先搜索核心

 

int BfsMAZE_ShortPath_WithPathPrint(int** maze,int max_x,int max_y,int *start,int *end,QUE_LINE* que,int **dp)
{
	POINT_T point = {0};
	POINT_T next  = {0};
	POINT_T* cur = NULL;
	
	int i = 0;
	point.x = start[0];
	point.y = start[1];
	point.pre = NULL;
	//起点入队列并标记
	Queue_Push(que,&point);
	*(((int*)dp)+point.x * max_y + point.y) = 0;
	//printf("que.size = %d,que->cap = %d,que->head = %d,que->tail = %d\n",que->size,que->cap,que->head,que->tail);
	
	while(Queue_Is_Empty(que) != 1)
	{
		cur = Queue_Pop(que);
		//printf("cur:(%d,%d)\n",cur->x,cur->y);
		//尝试四个方向,如果下一个状态合法,则入队列
		for(i = 0; i <= MAX_DIR;i++)
		{
			next.x = cur->x + s_dirs[i][0];
			next.y = cur->y + s_dirs[i][1];
			next.pre = cur;
			//如果是边界,状态不合法,不能入队
			if(Point_Is_Boundry(&next,max_x,max_y) == 1)
			{
				printf("(%d,%d) is bondry\n",next.x,next.y);
				continue;
			}
			//如果是墙,也不合法,不能入队
			if(*(((int*)maze)+next.x * max_y + next.y) == 1)
			{
				//printf("*(((int*)maze)+max_y * next[0] + next[1]) = %d,maze is 1\n",*(((int*)maze)+max_y * next.x + next.y));
				continue;
			}
			//如果节点已经遍历过了,也不能入队
			if(*(((int*)dp)+next.x * max_y + next.y) != -1)
			{
				//printf("*(((int*)dp)+max_x * next[0] + next[1]) = %d,dp is 1\n",	*(((int*)dp)+max_y * next.x + next.y));
				continue;
			}
			//如果找到了重点,停止搜索,并打印路径;如果没有要求打印路径的话把return去掉可以把//DP给记录完全,及把地图上所有点到起点的路径都给记下来
			if(next.x == end[0] && next.y == end[1])
			{
				print_path(&next);
				return *(((int*)dp)+cur->x * max_y + cur->y)+1;
			}
			
			//printf("now (%d,%d) is push queue\n",next.x,next.y);
			Queue_Push(que,&next);
			*(((int*)dp)+next.x * max_y + next.y) = *(((int*)dp)+cur->x * max_y + cur->y) + 1;
			
		}
		
	}
	return 0;
	
}

6.所有代码

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 /*
BFS一般三步走即可:

1) 查找给定的结点s可以达到的点,并加入广度优先搜索树中(以队列实现,下称队列)同时标记为已访问;

2) 遍历队列,判断队列中的结点是否满足给定的条件或要求,满足则结束搜索;若不满足结束条件,对队列中的每个结点继续查找其可以达到并且未使用过的结点,加入队列中,并记录为已使用。

3)重复2。

注意:第二步中的记录已访问需要根据实际情况选择是每个加入后立马记录(这样的话,同级的兄弟结点也不能重复使用),还是同级的全部完成后再记录已访问(这样同级的兄弟结点允许重复使用)。
*/

/*
1. 是求路径长度,还是路径本身(或动作序列)?
i. 如果是求路径长度,则状态里面要存路径长度(或双队列+一个全局变量)
ii. 如果是求路径本身或动作序列
i. 要用一棵树存储宽搜过程中的路径
ii. 是否可以预估状态个数的上限?能够预估状态总数,则开一个大数组,用树的双亲表示法;如
果不能预估状态总数,则要使用一棵通用的树。这一步也是第4步的必要不充分条件。
2. 如何表示状态?即一个状态需要存储哪些些必要的数据,才能够完整提供如何扩展到下一步状态的所
有信息。一般记录当前位置或整体局面。
3. 如何扩展状态?这一步跟第2步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据
结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,
对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。
4. 如何判断重复?如果状态转换图是一颗树,则永远不会出现回路,不需要判重;如果状态转换图是一
个图(这时候是一个图上的BFS),则需要判重。
i. 如果是求最短路径长度或一条路径,则只需要让“点”(即状态)不重复出现,即可保证不出现回路
ii. 如果是求所有路径,注意此时,状态转换图是DAG,即允许两个父节点指向同一个子节点。具体
实现时,每个节点要“延迟”加入到已访问集合 visited ,要等一层全部访问完后,再加入
到 visited 集合。
iii. 具体实现
i. 状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。
ii. 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如 unordered_set )来判
重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和
next,表示哈希表,参考第 ??? 节方案2。
iii. 如果存在,则可以开一个大布尔数组,来判重,且此时可以精确计算出状态总数,而不仅仅是
预估上限。
5. 目标状态是否已知?如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始状态出
发,正向广搜;也可以从目标状态出发,逆向广搜;也可以同时出发,双向广搜。
代码模板
总结
288
广搜需要一个队列,用于一层一层扩展,一个hashset,用于判重,一棵树(只求长度时不需要),用于存
储整棵树。
对于队列,可以用 queue ,也可以把 vector 当做队列使用。当求长度时,有两种做法:
1. 只用一个队列,但在状态结构体 state_t 里增加一个整数字段 level ,表示当前所在的层次,当碰
到目标状态,直接输出 level 即可。这个方案,可以很容易的变成A*算法,把 queue 替换
为 priority_queue 即可。
2. 用两个队列, current, next ,分别表示当前层次和下一层,另设一个全局整数 level ,表示层数
(也即路径长度),当碰到目标状态,输出 level 即可。这个方案,状态里可以不存路径长度,只需
全局设置一个整数 level ,比较节省内存;
对于hashset,如果有完美哈希方案,用布尔数组( bool visited[STATE_MAX] 或 vector<bool>
visited(STATE_MAX, false) )来表示;如果没有,可以用STL里的 set 或 unordered_set 。
对于树,如果用STL,可以用 unordered_map<state_t, state_t > father 表示一颗树,代码非常简
洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组( state_t nodes[STATE_MAX] ),
即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。
*/


/*
极简动态规划配合BFS求解P2P最短路径,dp数组的元素有效时表示起点到该点的最短距离,也即表示访问过了,同样需要一个队列,这里实现为线性队列
*/

	/*队列实现*/
#include<stdlib.h>
#include<stdio.h>
#include<string.h>


typedef struct Node_
{
	int x;
	int y;
	//前驱节点,为了能打印出路径(这利用到递归打印了),因为入队列的不一定都是有用的
	struct Node_ *pre;
}POINT_T;

typedef struct Que_
{
	int size;
	int cap;
	int head;
	int tail;
	//队列中这个搞成数组的形式
	POINT_T* path;
}QUE_LINE;

void Queue_Reinit(QUE_LINE* que)
{
	que->size = 0;
	que->head = 0;
	que->tail = 0;
}
void Queue_Init(QUE_LINE* que,int cap)
{
	
	que->cap = cap;
	que->path = (POINT_T*)malloc(sizeof(POINT_T)*cap);
	memset(que->path,0,sizeof(POINT_T)*cap);
	Queue_Reinit(que);
}
int Queue_Is_Empty(QUE_LINE *que)
{
	return (que->size == 0) ? 1 :0;
}
int Queue_Is_Full(QUE_LINE *que)
{
	return (que->size >= que->cap) ? 1 :0;
}
int Queue_Push(QUE_LINE *que,POINT_T* point)
{
	if(Queue_Is_Full(que) != 1)
	{
		que->size++;
		memcpy((POINT_T*)&que->path[que->tail],(POINT_T*)point,sizeof(POINT_T));
		que->tail = (++que->tail)%que->cap;
		return 1;
	}
	return 0;
}
POINT_T* Queue_Pop(QUE_LINE* que)
{
	int res_index = que->head;
	if(Queue_Is_Empty(que) != 1)
	{
		que->size--;
		que->head = (++que->head)%que->cap;
		return &que->path[res_index];
	}
	return NULL;
}

void Queue_Destroy(QUE_LINE* que)
{
	int i = 0;
	for(i = 0; i <= que->size - 1; i++)
	{
		free(&que->path[i]);
	}
}

//**********递归打印***********
void print_path(POINT_T* point)
{
	if(point->pre == NULL)
	{
		printf("(%d,%d)",point->x,point->y);//为了把第一个打出来
		return;
	}
	else
	{
		print_path(point->pre);
		printf("->(%d,%d)",point->x,point->y);
	}
}

#define MAX_DIR 4
#define DIM 2

	/*方向的模拟,坐标原点在左上方,向左的话其实是列减1*/
static const int s_dirs[MAX_DIR][DIM] = {{0,-1},{0,1},{-1,0},{1,0}};//左,右,上,下

int Point_Is_Boundry(POINT_T* point,int max_x,int max_y)
{
	if(point->x < 0 || point->x >= max_x || point->y < 0 || point->y >= max_y)
		return 1;
	return 0;
}

int BfsMAZE_ShortPath_WithPathPrint(int** maze,int max_x,int max_y,int *start,int *end,QUE_LINE* que,int **dp)
{
	POINT_T point = {0};
	POINT_T next  = {0};
	POINT_T* cur = NULL;
	
	int i = 0;
	point.x = start[0];
	point.y = start[1];
	point.pre = NULL;
	//入队列并标记
	Queue_Push(que,&point);
	*(((int*)dp)+point.x * max_y + point.y) = 0;
	//printf("que.size = %d,que->cap = %d,que->head = %d,que->tail = %d\n",que->size,que->cap,que->head,que->tail);
	
	while(Queue_Is_Empty(que) != 1)
	{
		cur = Queue_Pop(que);
		//printf("cur:(%d,%d)\n",cur->x,cur->y);
		//尝试四个方向
		for(i = 0; i <= MAX_DIR;i++)
		{
			next.x = cur->x + s_dirs[i][0];
			next.y = cur->y + s_dirs[i][1];
			next.pre = cur;
			//如果是边界,状态不合法,不能入队
			if(Point_Is_Boundry(&next,max_x,max_y) == 1)
			{
				//printf("(%d,%d) is bondry\n",next.x,next.y);
				continue;
			}
			//如果是墙,也不合法,不能入队
			if(*(((int*)maze)+next.x * max_y + next.y) == 1)
			{
				//printf("*(((int*)maze)+max_y * next[0] + next[1]) = %d,maze is 1\n",*(((int*)maze)+max_y * next.x + next.y));
				continue;
			}
			//如果节点已经遍历过了,也不能入队
			if(*(((int*)dp)+next.x * max_y + next.y) != -1)
			{
				//printf("*(((int*)dp)+max_x * next[0] + next[1]) = %d,dp is 1\n",	*(((int*)dp)+max_y * next.x + next.y));
				continue;
			}
			//如果找到了重点,停止搜索,并打印路径;
			if(next.x == end[0] && next.y == end[1])
			{
				print_path(&next);
				return *(((int*)dp)+cur->x * max_y + cur->y)+1;
			}
			
			//printf("now (%d,%d) is push queue\n",next.x,next.y);
			Queue_Push(que,&next);
			*(((int*)dp)+next.x * max_y + next.y) = *(((int*)dp)+cur->x * max_y + cur->y) + 1;
			
		}
		
	}
	return 0;
	
}


#define MAZE_MAX_X 4
#define MAZE_MAX_Y 4

int main()
{
	int len = 0, i =0,j = 0;
	int dp[MAZE_MAX_X][MAZE_MAX_Y] = {0};
	int maze[MAZE_MAX_X][MAZE_MAX_Y] = {
		{0,0,1,0},
		{0,0,0,0},
		{0,0,1,0},
		{0,0,1,0},
	};
	//起点、终点,即坐标
	int start[2] = {0,1};
	int end[2] = {1,3};
	
	QUE_LINE que = {0};
	
	Queue_Init(&que,100);
	
	//注意是搞成-1
	memset((int*)dp,-1,sizeof(int)*MAZE_MAX_X*MAZE_MAX_Y);
	len = BfsMAZE_ShortPath_WithPathPrint((int**)maze,MAZE_MAX_X,MAZE_MAX_Y,start,end,&que,(int**)dp);
	printf("len = %d\n",len);
	for(i = 0; i <= MAZE_MAX_X - 1;i ++ )
	{
		for(j = 0; j <= MAZE_MAX_X - 1;j++)
		{
			printf("%d ",dp[i][j]);
		}
		printf("\n");
	}
	return 0;
}

最短的一条带路径打印:https://pan.baidu.com/s/1x5od2I-s9WkaQTynWAcbXw

最短的路径不带打印:https://pan.baidu.com/s/1NfES9yTyHuvVsd2FaWq6xw

7.结果

本例子的起点是(0,1),中点事(1,3)见代码定义

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值