拯救小Q (多传送门求最短路径问题)(bfs)

题目说明:        

小Q被邪恶的大魔王困在了迷宫里,love8909决定去解救她。迷宫里面有一些陷阱,一旦走到陷阱里,就会被困身亡:(,迷宫里还有一些古老的传送阵,一旦走到传送阵上,会强制被传送到传送阵的另一头。

现在请你帮助love8909算一算,他至少需要走多少步才能解救到小Q? (上下左右四个方向走,同一个传送门可以多次使用)

输入:第一行为一个整数T,表示测试数据组数。每组测试数据第一行为两个整数N,M,(1 <= N, M <= 50)表示迷宫的长和宽。接下来有N行,每行M个字符,是迷宫的具体描述。
'.'表示安全的位置,'#'表示陷阱,
'Q'表示小Q的位置,'L'表示love8909所在位置,
数据保证love8909只有一个,数据也保证小Q只有一个。小写字母'a'-'z'表示分别表示不同的传送阵,数据保证传送阵两两配对。

输出:每组数据输出一行,解救小Q所需的最少步数,如果无论如何都无法救小Q,输出-1。

样例:

2
5 5
....L
.###.
b#b#a
##.##
...Qa
5 5
....L
.###.
.#.#.
##.##
...Q. 

输出:

3
-1

题解思路:由题目我们不难看出这题是 BFS 类题目 小 L 到 小 Q的 最短距离   我们应该先有以下思路

1.如何去存储地图

2.如何存储传送点 和 读取传送点

3.如果去使用 bfs(广度优先搜索) 并利用它得到最后的答案 (如果不会,可以先去学习,本文将粗略讲解 )

4.坑项: 传送点可以被多次使用,我们该如何标记防止重复踩 或者 踩不到

第一点地图的存储:我们可以开一个 N * N 大小的 char数组(根据题目 N > 50即可) 

同时我们也可以把需要的数据全开了  这里我们采用临链表的方式对 传送的存储与查找 一下是作者开的变量(可能存在冗余)

坐标的点可以用 pair存储<i, j>

typedef pair<int,int> PII;
const int N = 60;
char g[N][N];//地图
int d[N][N]; //表示 小L到 点 g[i][j]的最短距离
int n, m;//n 层 m 列
int a[2][2];//a[0] a[1]分别存储 小L 和 小Q 的坐标 
int h[26], ne[N * N], idx; //h用于连接临链表 idx
int ch[N][N];//对走过的点进行标记 
PII q[N * N], e[N * N]; //q用于存储走过的点 e存储传送点坐标

第二: 我们在输入数据的同时 我们也可以进行查找 并存储 (传送点 和 小Q 小 L)

for (int i = 1; i <= n; i++)
{
	scanf("%s", g[i] + 1);//从 g[1] 开始取
	for (int j = 1; j <= m; j++)
	{
		if (g[i][j] >= 'a' && g[i][j] <= 'z')//传送点的存储 这里采用的是 临链表的方式
		{
            //将字母转换为 26 个数字 每个字母占要给位子
			int x = g[i][j] - 'a';
            //存储当前 字母的 坐标 第idx个字母占第idx的位置
			e[idx] = { i, j };
            //链表的连接: 将h[x]的尾接到 第idx的尾节点
			ne[idx] = h[x];
           //将h[x]的尾与 第idx 的头链接
			h[x] = idx++;
			continue;
		}
		if (g[i][j] == 'L')//存储 小L
		{
			a[0][0] = i;
			a[0][1] = j;
		}
		if (g[i][j] == 'Q')//存储 小Q
		{
			a[1][0] = i;
			a[1][1] = j;
		}
	}
}

而读取传送点我们就需要用到外接的函数了 这里我设了一个find函数 (查找并返回坐标点)

PII find(int x, int y)//传入的是 传送点的坐标
{
	int tmp = g[x][y] - 'a';
	for (int i = h[tmp]; i != -1; i = ne[i])
	{
		auto t = e[i];
		if (t.first != x || t.second != y)//如果有一个值不同 则说明是 不同传送点 返回传送点
		{
			return t;//输出的也是传送点的坐标(另一个)
		}
	}
}

由题目可知 传送点 以一组为单位出现 并且不会出现多个 可以用这种 判断直接找

接下来我们要对地图进行bfs搜索  (下面这段代码是 作者第一次写的错误写法)

int bfs()
{
   int hh = 0, tt = 0;
   memset(d, -1, sizeof d);
   q[0] = {a[0][0], a[0][1]};
   d[a[0][0]][a[0][1]] = 0;
   int dx[] = {0,1,0,-1}, dy[] = {1,0,-1,0};

   while(hh <= tt)
   {
       auto t = q[hh++];
       for(int i = 0; i < 4; i++)
       {
           int x = t.first + dx[i], y = t.second + dy[i];
           if(x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] != '#' && d[x][y] == -1)
           {
               if(g[x][y] >='a' && g[x][y] <= 'z')
               {
                   d[x][y] = d[t.first][t.second] + 1;
                   auto tmp = find(x , y);
                   x = tmp.first;
                   y = tmp.second;
               }
               d[x][y] = d[t.first][t.second] + 1;
               q[++tt] = {x , y};
           }
       }

   }
   return d[a[1][0]][a[1][1]];
}

如果我们单纯用走过的距离d[i][j]去判断是否走过,这样会漏掉,返回的方向 (用其他方式也是可以的,作者这里采用的是另取一个数组来判断是否走过) 就比如以下数据

3 3

LaQ

.a#

###

这样的数据用上面的代码是 -1 明显是错误的(答案应该是 3)

那我们该怎么办呢

我们可以确定的是每个 只能走一次 所以作者这里采用的是 对每个点进行标记

如果进入了某个点查找 则进行标记  ch[i][j]

int bfs()
{
    //这里设 hh . tt 为了方便元素的进队与出队 
	int hh = 0, tt = 0;
	memset(d, -1, sizeof d);//初始化 d (距离)和 ch(判断是否走过)
	memset(ch, 0, sizeof ch);
    //q用于存储每次遍历点的坐标  首先将 L的坐标存入 q
	q[0] = { a[0][0], a[0][1] };
	d[a[0][0]][a[0][1]] = 0; //起始点坐标距离是 0
	int dx[] = { 0,1,0,-1 }, dy[] = { 1,0,-1,0 }; //对四个方向进行查找
    //当hh <= tt 时说明队列中还有元素
	while (hh <= tt)
	{
		auto t = q[hh++];
        //点进入 这个点正在被查找 要将进行标记 ch[i][j] = 1
		ch[t.first][t.second] = 1;
		for (int i = 0; i < 4; i++)
		{
			int x = t.first + dx[i], y = t.second + dy[i];
			if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] != '#' && ((ch[x][y] == 0 && (g[x][y] <= 'a' || g[x][y] >= 'z')) || (g[x][y] >= 'a' && g[x][y] <= 'z' && ch[find(x, y).first][find(x, y).second] == 0)))
            //这里又臭又长的 是为了 元素的正确性 
            //简单说就是 防止 小L 走到陷阱 走出边界 走路重复
			{
				if (g[x][y] >= 'a' && g[x][y] <= 'z')
				{
                    //如果查找到字母 就找到相应的 传送点坐标 并重新赋值
					auto tmp = find(x, y);
					x = tmp.first;
					y = tmp.second;
				}
                //这里是为了防止重复走(传送门可多次走 可能会发生重复 应该取最小值)
				if (d[x][y] != -1)
				{
					d[x][y] = min(d[x][y], d[t.first][t.second] + 1);
				}
                //正常则将 上一个点的距离 + 1
				else d[x][y] = d[t.first][t.second] + 1;
				q[++tt] = { x , y };
			}
		}

	}
    //最后返回最终的坐标 所需的距离 如果到不了 则是 -1 最开始已经初始化过了
	return d[a[1][0]][a[1][1]];
}

这里的关于点是否能走的问题 我 用分了两种情况:

第一:如果是非传送点 则仅仅只用考虑该点的ch[i][j]是否为 0即可

第二 : 如果是传送点 则需要考虑 传送点的另一个点 是否为0  

(ch[x][y] == 0 && (g[x][y] <= 'a' || g[x][y] >= 'z')) //条件1

(g[x][y] >= 'a' && g[x][y] <= 'z' && ch[find(x, y).first][find(x, y).second] == 0)//条件2

到这里 基本上整体架构思路已经讲清楚了

综合代码奉上:

#include <iostream>
#include<algorithm>
#include <vector>
#include <stdlib.h>
#include <cstring>
#include <queue>
#include <cmath>
using namespace std;

typedef pair<int, int> PII;

const int N = 60;
char g[N][N];
int d[N][N]; //分别表示地图 和 距离
int n, m, h[N * N], ne[N * N], idx, a[2][2], ch[N][N]; //a[0] L
PII q[N * N], e[N * N]; //q 存储 走过的 e存储传送点坐标

PII find(int x, int y)
{
	int tmp = g[x][y] - 'a';
	for (int i = h[tmp]; i != -1; i = ne[i])
	{
		auto t = e[i];
		if (t.first != x || t.second != y)
		{
			return t;
		}
	}
}

int bfs()
{
	int hh = 0, tt = 0;
	memset(d, -1, sizeof d);
	memset(ch, 0, sizeof ch);
	q[0] = { a[0][0], a[0][1] };
	d[a[0][0]][a[0][1]] = 0;
	int dx[] = { 0,1,0,-1 }, dy[] = { 1,0,-1,0 };

	while (hh <= tt)
	{
		auto t = q[hh++];
		ch[t.first][t.second] = 1;
		for (int i = 0; i < 4; i++)
		{
			int x = t.first + dx[i], y = t.second + dy[i];
			if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] != '#' && ((ch[x][y] == 0 && (g[x][y] <= 'a' || g[x][y] >= 'z')) || (g[x][y] >= 'a' && g[x][y] <= 'z' && ch[find(x, y).first][find(x, y).second] == 0)))
			{
				if (g[x][y] >= 'a' && g[x][y] <= 'z')
				{
					auto tmp = find(x, y);
					x = tmp.first;
					y = tmp.second;
				}
				if (d[x][y] != -1)
				{
					d[x][y] = min(d[x][y], d[t.first][t.second] + 1);
				}
				else d[x][y] = d[t.first][t.second] + 1;
				q[++tt] = { x , y };
			}
		}

	}
	return d[a[1][0]][a[1][1]];
}

int main()
{
	int t;
	cin >> t;

	while (t--)
	{
		idx = 0;
		memset(e, -1, sizeof e);
		memset(ne, -1, sizeof ne);
		memset(h, -1, sizeof h);
		scanf("%d%d", &n, &m);

		for (int i = 1; i <= n; i++)
		{
			scanf("%s", g[i] + 1);
			for (int j = 1; j <= m; j++)
			{
				if (g[i][j] >= 'a' && g[i][j] <= 'z')//传送点的存储
				{
					int x = g[i][j] - 'a';
					e[idx] = { i, j };
					ne[idx] = h[x];
					h[x] = idx++;
					continue;
				}
				if (g[i][j] == 'L')
				{
					a[0][0] = i;
					a[0][1] = j;
				}
				if (g[i][j] == 'Q')
				{
					a[1][0] = i;
					a[1][1] = j;
				}
			}
		}

		cout << bfs() << endl;
	}
}

这是作者第一份博客,如果有说不好的地方请见谅qwq    也可以给作者提出些意见

如果有问题 可以与作者交流 一起学习进步 |wp

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值