【刷题笔记】I'm stuck! (迷宫)-----java方案

时间:2022-06-17 13:52:39

题目描述 :

给定一个R行C列的地图,地图的每一个方格可能是'#', '+', '-', '|', '.', 'S', 'T'七个字符中的一个,分别表示如下意思:

  '#': 任何时候玩家都不能移动到此方格;

  '+': 当玩家到达这一方格后,下一步可以向上下左右四个方向相邻的任意一个非'#'方格移动一格;

  '-': 当玩家到达这一方格后,下一步可以向左右两个方向相邻的一个非'#'方格移动一格;

  '|': 当玩家到达这一方格后,下一步可以向上下两个方向相邻的一个非'#'方格移动一格;

  '.': 当玩家到达这一方格后,下一步只能向下移动一格。如果下面相邻的方格为'#',则玩家不能再移动;

  'S': 玩家的初始位置,地图中只会有一个初始位置。玩家到达这一方格后,下一步可以向上下左右四个方向相邻的任意一个非'#'方格移动一格;

  'T': 玩家的目标位置,地图中只会有一个目标位置。玩家到达这一方格后,可以选择完成任务,也可以选择不完成任务继续移动。如果继续移动下一步可以向上下左右四个方向相邻的任意一个非'#'方格移动一格。

  此外,玩家不能移动出地图。

  请找出满足下面两个性质的方格个数:

  1. 玩家可以从初始位置移动到此方格;

  2. 玩家不可以从此方格移动到目标位置。

格式及示例:

输入格式----输入的第一行包括两个整数R 和C,分别表示地图的行和列数。(1 ≤ R, C ≤ 50)。

  接下来的R行每行都包含C个字符。它们表示地图的格子。地图上恰好有一个'S'和一个'T'。

输出格式----如果玩家在初始位置就已经不能到达终点了,就输出“I'm stuck!”(不含双引号)。否则的话,输出满足性质的方格的个数。

样例输入----

5 5

--+-+

..|#.

..|##

S-+-T

####.

样例输出----2

2

样例说明----

如果把满足性质的方格在地图上用'X'标记出来的话,地图如下所示:

  --+-+

  ..|#X

  ..|##

  S-+-T

  ####X


						请大家先思考一下

这个题目感觉还是很复杂,长长的题干已经把我吓到了,不过慢慢分析一下还是找得到一些门路。首先思路是先找出所有可以到达目标位置的方格,再找出所有起点位置可以到达的方格,然后很直接的取出满足起始点可以到达而且无法到达目标点的位置即为最终的结果。简单明了,而且还很有效,接下来就是实现它。

需要注意的情况在于,起始点如果无法到达目标位置则直接结束,所以我们要先找到所有可以到达目标的方格,如果此时可以结束就免去了后续的运算。另一个陷阱在于内容是' . '的位置,如果它正好紧邻着目标的话,很明显从目标点是可以到达该点的,但是只有该点在目标点的正上方时,才可以到达目标,其它位置是无法到达目标的,因此也需要注意。

我们采用二维数组来存储整个迷宫,读取输入的字符并在相应的位置进行填充,同时我们需要记录迷宫中S和T点的坐标,当做我们的起始坐标(此处应当是起始坐标与结束坐标,但是在找可以到达目标位置的方格时,我们可以从终点开始,所以也叫作起始坐标了)。因为我们知道不能出迷宫的范围,所以我们在存储的时候在迷宫周围添加一圈' # '表示不能到达,从而保证后续的过程中无论是边界上的点还是迷宫内部的点都可以一视同仁地计算。

接下来开始找符合条件的点,首先从T点出发,找到所有可以到达目标T的点。这个过程起始就是一次遍历,我选择的是广度优先的遍历,所以需要一个队列来记录待访问的节点(如果是深度优先的话,应当需要一个栈结构来记录待访问的点)。过程中还需要一个同样大小的数组来记录哪些点是已经访问过的,防止陷入一个环里,同样的结果也要存在一个数组中,记录所有可以到达目标T点的方格。遍历过程中,需要注意我们要找的是可以到达目标点的方格,而不是从目标点可以到达的方格,所以对于' . '字符的位置需要注意,只有' . '在当前遍历节点正上方相邻位置时,才认为是可到达的。

同上一步相似,现在需要找到起始点S可以到达的方格。同样的,从S点开始进行遍历,标记所有可以从S点到达的方格。因为我们在初始化迷宫的时候,在外围加了一圈不可到达的字符,因此用来记录节点是否可以被访问的数组在初始化的时候应该将外圈的统一标记为True或者False。

以我的智商呢,也就到此为止了。程序繁琐而且效率并不够高,可能是因为中间有太多的判断分支,导致了时间浪费。偶然间看到了大神的答案,非常巧妙,但是由于不知道大神的名字,所以在这里只好直接借鉴分析一下了。如果侵权的话还请告知,得罪了。


接下来,分析一下大神的程序,尽量理解大神的思路。首先对于遍历而言,需要对每个节点的上下左右四个方向进行判断,对于这个操作可以直接记录为两个数组,如下:

int[] dr = new int[] {0, -1, 0, 1};
int[] dc = new int[] {1, 0, -1, 0};

当然,每个方向都访问一次数组查找相应的坐标,肯定是比不上直接+1、-1计算来得快,但是数组的优势在于可以把“选一个方向,判断相邻节点是否已访问,记录相应节点属性并进行队列操作”这样的一系列操作模块化,这样的话,对于每一个节点的访问,只需要四次循环,依次去上面的坐标即可。

最精妙的地方在于,根据上面的数组简化后续的判断操作,先看代码如下:

if ((board[nr][nc] & (1 << ((i + 2) % 4))) != 0) {
canReachT[nr][nc] = true;
queue.add(nr);
queue.add(nc);
visited[nr][nc] = true;
}

这是判断部分的代码,与之对应的是board即迷宫内存储的数据,如下:

int[][] board = new int[R + 2][C + 2];
char c = line.charAt(j - 1); //c是输入的字符
switch (c) {
case '#': break;
case '-': board[i][j] = 5; break;
case '|': board[i][j] = 0xA; break;
case '+':
case 'S':
case 'T':
board[i][j] = 0xF; break;
case '.': board[i][j] = 0x8; break;
default: break;
}

如上面的代码所示,board数组存储了这个迷宫的数据,并且给' . '、' + '、' - '、' | '这几种不同的字符对应了不同的数字,当然数字也并不是随意给出的,我们来看一下可行性。关键在于上面的判断函数,我们把这一句提出来单独分析一下,

if ((board[nr][nc] & (1 << ((i + 2) % 4))) != 0)

举例来说,如果是字符' - '且方向是向左(即i的值为2)的话,判断结果是(0101 & 0001)结果不为0,跟预计的是一样的。大家可以尝试一下其他的数据组合,都是正确的。观察一下可以发现,因为方向数组的0123分别表示右上左下,因此表示左右方向的' - '符中从低到高的第1第3位应该为1,也就是0101;表示上下方向的' | '符中从低到高的第2第4位应该为1,也就是1010;表示全方向的' + '符全部为1,也就是1111;表示左右方向的' . '符中从低到高的第4位应该为1,也就是1000。跟赋值时的数据一致,到此分析结束。

读者如果看懂了可以自己写程序试一下,如果有问题的话,欢迎讨论指教。