P1443 马的遍历 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
- 广度优先搜索(BFS)的性质决定
- 遍历顺序要求:在广度优先搜索算法中,我们需要按照 “一层一层” 的顺序来遍历图(或在这个例子中是棋盘)。就像水波扩散一样,从起始点开始,先访问离起始点距离为 1 的所有节点,然后再访问距离为 2 的所有节点,以此类推。队列这种数据结构正好符合这个要求,它遵循先进先出(FIFO)的原则。
- 示例说明:以马的遍历为例,当马在初始位置时,它周围有 8 个可能的下一步位置。这些位置在搜索过程中是处于同一 “层” 的,我们需要先把这些位置都记录下来,然后再从这些位置出发去探索它们周围的位置。队列可以很好地实现这种记录,先将马初始位置周围的 8 个位置加入队列,然后按照加入的顺序依次取出进行处理。
- 方便管理待访问节点
- 避免重复访问:队列可以帮助我们避免重复访问已经处理过的节点。在马的遍历中,如果没有一个合适的数据结构来管理待访问节点,很可能会出现一个节点被多次访问的情况,这会导致计算步数出错(因为每次访问可能会更新步数)以及算法效率降低。通过将待访问节点放入队列,在访问一个节点时,将它的相邻未访问节点加入队列,并且在处理节点时可以通过判断节点是否在队列中来确定是否已经访问过。
- 节点状态的统一管理:使用队列可以将所有待访问的节点集中在一起管理。我们可以方便地知道当前还有哪些节点需要处理,并且可以根据队列的状态(是否为空)来判断搜索是否完成。在代码实现中,只要队列不为空,就说明还有节点需要探索,这样的逻辑判断很清晰。
- 代码实现的简洁性和可读性
- 标准库的支持:Java 的
Queue
接口(如LinkedList
实现了Queue
接口)提供了一系列方便的方法,如add
、poll
、peek
等。这些方法使得代码在实现广度优先搜索时更加简洁明了。相比于自己手动实现复杂的数据结构来管理待访问节点,使用队列可以让代码更专注于搜索算法本身的逻辑。 - 逻辑清晰易理解:从代码阅读者的角度来看,当看到使用队列进行广度优先搜索时,很容易理解代码是按照一种层次化的顺序在遍历图或者棋盘等结构。这种基于标准数据结构的实现方式符合常见的算法实现模式,使得代码的可读性更好。
- 标准库的支持:Java 的
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Arrays;
public class Main {
// 定义马在水平方向上每次移动可能的横坐标偏移量
static int dx[] = {0, 1, 1, -1, -1, 2, 2, -2, -2};
// 定义马在垂直方向上每次移动可能的纵坐标偏移量
static int dy[] = {0, -2, 2, 2, -2, 1, -1, 1, -1};
// 存储棋盘的行数
static int n;
// 存储棋盘的列数
static int m;
// 存储马初始位置的横坐标
static int x;
// 存储马初始位置的纵坐标
static int y;
// 定义二维数组用于表示棋盘的各个格子状态,
// 这里用于存储从马的初始位置到每个格子的最少步数,初始化为 -1 表示未访问过
static int a[][];
// 创建一个BufferedReader对象用于从控制台读取输入信息
static BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
// 广度优先搜索方法,用于计算从马的初始位置到棋盘其他位置的最少步数
public static void bfs(int x, int y) {
// 创建一个整数类型的队列,用于存储待访问的格子坐标信息(先存横坐标,再存纵坐标)
Queue<Integer> q = new LinkedList<>();
// 将马的初始位置的横坐标加入队列
q.add(x);
// 将马的初始位置的纵坐标加入队列
q.add(y);
// 将马初始位置对应的数组元素值设为0,表示从马的初始位置到自身的步数为0
a[x][y] = 0;
// 只要队列不为空,就继续进行广度优先搜索
while (!q.isEmpty()) {
// 从队列中取出一个格子的横坐标,此操作会移除队列头部元素
int cx = q.poll();
// 从队列中取出一个格子的纵坐标,此操作会移除队列头部元素
int cy = q.poll();
// 遍历马可能的8种移动方向
for (int i = 1; i <= 8; i++) {
// 计算马在当前移动方向下,下一个可能到达的格子的横坐标
int nx = cx + dx[i];
// 计算马在当前移动方向下,下一个可能到达的格子的纵坐标
int ny = cy + dy[i];
// 判断下一个格子是否在棋盘范围内(横坐标和纵坐标都要满足条件)
// 并且该格子是否还未被访问过(值为 -1)
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && a[nx][ny] == -1) {
// 将下一个格子的横坐标加入队列
q.add(nx);
// 将下一个格子的纵坐标加入队列
q.add(ny);
// 更新下一个格子到马初始位置的最少步数,
// 为当前格子到马初始位置的步数加1
a[nx][ny] = a[cx][cy] + 1;
}
}
}
}
public static void main(String[] args) throws IOException {
// 从控制台读取一行输入信息,并按空格分割成字符串数组
String s[] = bf.readLine().split(" ");
// 将读取到的第一个字符串转换为整数,作为棋盘的行数并赋值给n
n = Integer.parseInt(s[0]);
// 将读取到的第二个字符串转换为整数,作为棋盘的列数并赋值给m
m = Integer.parseInt(s[1]);
// 将读取到的第三个字符串转换为整数,作为马初始位置的横坐标并赋值给x
x = Integer.parseInt(s[2]);
// 将读取到的第四个字符串转换为整数,作为马初始位置的纵坐标并赋值给y
y = Integer.parseInt(s[3]);
// 创建一个二维数组a,用于表示棋盘状态,行数为n + 1,列数为m + 1
a = new int[n + 1][m + 1];
// 遍历每一行,将该行的所有元素初始化为 -1,表示未访问过
for (int i = 1; i <= n; i++) {
Arrays.fill(a[i], -1);
}
// 调用广度优先搜索方法,从马的初始位置(x, y)开始计算到棋盘其他位置的最少步数
bfs(x, y);
// 遍历整个棋盘的每一行
for (int i = 1; i <= n; i++) {
// 遍历当前行的每一列
for (int j = 1; j <= m; j++) {
// 输出当前格子到马初始位置的最少步数,后面跟一个空格
System.out.print(a[i][j] + " ");
}
// 换行,用于输出下一行的最少步数信息
System.out.println();
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class 马的遍历不用Queue {
// 定义马在水平方向上每次移动可能的横坐标偏移量
static int dx[] = {0, 1, 1, -1, -1, 2, 2, -2, -2};
// 定义马在垂直方向上每次移动可能的纵坐标偏移量
static int dy[] = {0, -2, 2, 2, -2, 1, -1, 1, -1};
// 存储棋盘的行数
static int n;
// 存储棋盘的列数
static int m;
// 存储马初始位置的横坐标
static int x;
// 存储马初始位置的纵坐标
static int y;
// 定义二维数组用于表示棋盘的各个格子状态,
// 这里用于存储从马的初始位置到每个格子的最少步数,初始化为 -1 表示未访问过
static int a[][];
// 创建一个BufferedReader对象用于从控制台读取输入信息
static BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
// 广度优先搜索方法,用于计算从马的初始位置到棋盘其他位置的最少步数
public static void bfs(int x, int y) {
// 创建一个二维数组来模拟队列,用于存储待访问的格子坐标信息
// 第一维表示队列中的节点,第二维长度为2,分别存储横坐标和纵坐标
int[][] queueArray = new int[n * m][2];
// 队列头指针,指向队列中第一个待处理的元素
int head = 0;
// 队列尾指针,指向队列中最后一个已添加的元素的下一个位置
int tail = 0;
// 将马的初始位置的横坐标和纵坐标存入队列模拟数组的末尾
queueArray[tail][0] = x;
queueArray[tail][1] = y;
// 队列尾指针后移一位,表示添加了一个元素
tail++;
// 将马初始位置对应的数组元素值设为0,表示从马的初始位置到自身的步数为0
a[x][y] = 0;
// 只要队列头指针不等于队列尾指针,就表示队列不为空,继续进行广度优先搜索
while (head < tail) {
// 取出队列模拟数组中当前队首元素的横坐标
int cx = queueArray[head][0];
// 取出队列模拟数组中当前队首元素的纵坐标
int cy = queueArray[head][1];
// 队首指针后移一位,表示处理完当前队首元素
head++;
// 遍历马可能的8种移动方向
for (int i = 1; i <= 8; i++) {
// 计算马在当前移动方向下,下一个可能到达的格子的横坐标
int nx = cx + dx[i];
// 计算马在当前移动方向下,下一个可能到达的格子的纵坐标
int ny = cy + dy[i];
// 判断下一个格子是否在棋盘范围内(横坐标和纵坐标都要满足条件)
// 并且该格子是否还未被访问过(值为 -1)
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && a[nx][ny] == -1) {
// 将下一个格子的横坐标和纵坐标存入队列模拟数组的末尾
queueArray[tail][0] = nx;
queueArray[tail][1] = ny;
// 队列尾指针后移一位,表示添加了一个元素
tail++;
// 更新下一个格子到马初始位置的最少步数,
// 为当前格子到马初始位置的步数加1
a[nx][ny] = a[cx][cy] + 1;
}
}
}
}
public static void main(String[] args) throws IOException {
// 从控制台读取一行输入信息,并按空格分割成字符串数组
String s[] = bf.readLine().split(" ");
// 将读取到的第一个字符串转换为整数,作为棋盘的行数并赋值给n
n = Integer.parseInt(s[0]);
// 将读取到的第二个字符串转换为整数,作为棋盘的列数并赋值给m
m = Integer.parseInt(s[1]);
// 将读取到的第三个字符串转换为整数,作为马初始位置的横坐标并赋值给x
x = Integer.parseInt(s[2]);
// 将读取到的第四个字符串转换为整数,作为马初始位置的纵坐标并赋值给y
y = Integer.parseInt(s[3]);
// 创建一个二维数组a,用于表示棋盘状态,行数为n + 1,列数为m + 1
a = new int[n + 1][m + 1];
// 遍历每一行,将该行的所有元素初始化为 -1,表示未访问过
for (int i = 1; i <= n; i++) {
Arrays.fill(a[i], -1);
}
// 调用广度优先搜索方法,从马的初始位置(x, y)开始计算到棋盘其他位置的最少步数
bfs(x, y);
// 遍历整个棋盘的每一行
for (int i = 1; i <= n; i++) {
// 遍历当前行的每一列
for (int j = 1; j <= m; j++) {
// 输出当前格子到马初始位置的最少步数,后面跟一个空格
System.out.print(a[i][j] + " ");
}
// 换行,用于输出下一行的最少步数信息
System.out.println();
}
}
}