国际象棋跳跃问题
问题描述
小U掌握了国际象棋中“象”和“马”的跳跃能力,在一个无限大的平面直角坐标系中,她每一步可以模仿象和马的跳跃方式移动。在每次询问中,小U需要计算从初始坐标 (x1,y1)(x1,y1) 到 (x2,y2)(x2,y2) 所需的最少步数。
- 象的跳跃:可以移动到 (x+k,y+k)(x+k,y+k) 或 (x+k,y−k)(x+k,y−k),其中 kk 是任意整数。
- 马的跳跃:可以移动到 (x+a,y+b)(x+a,y+b),其中 ∣a∣+∣b∣=3∣a∣+∣b∣=3 且 1≤∣a∣,∣b∣≤21≤∣a∣,∣b∣≤2。
你需要在每次询问中计算从起点到终点所需的最少步数。
测试样例
样例1:
输入:x1 = 0, y1 = 0, x2 = 1, y2 = 1 输出:1
样例2:
输入:x1 = 0, y1 = 0, x2 = 2, y2 = 1 输出:1
样例3:
输入:x1 = 0, y1 = 0, x2 = 3, y2 = 3 输出:1
样例4:
输入:x1 = -3, y1 = -2, x2 = 2, y2 = 1 输出:2
方法思路
广度优先搜索(BFS)
使用广度优先搜索(BFS)来查找从起点到终点的最短路径。
初始化: 创建一个队列和一个集合,将起始节点加入队列。集合用于记录已访问的节点。
主循环: 从队列中取出一个节点。检查该节点是否为目标节点,如果是,则返回当前步数。否则,将该节点的所有未访问过的邻居节点加入队列,并标记为已访问。
**终止条件:**如果队列为空且未找到目标节点,则返回-1(题目保证会有解,这里不会走到)。
import java.util.*;
public class Main {
public static int solution(int x1, int y1, int x2, int y2) {
// 马的跳跃方向
int[][] knightMoves = {{2, 1}, {2, -1}, {-2, 1}, {-2, -1}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2}};
// 初始化队列和已访问集合
Deque<int[]> queue = new ArrayDeque<>();
Set<String> visited = new HashSet<>();
queue.offer(new int[]{x1, y1, 0});
visited.add(x1 + "," + y1);
while (!queue.isEmpty()) {
int[] current = queue.poll();
int x = current[0], y = current[1], steps = current[2];
// 处理马的跳跃
for (int[] move : knightMoves) {
int nx = x + move[0], ny = y + move[1];
if (nx == x2 && ny == y2) return steps + 1;
String pos = nx + "," + ny;
if (!visited.contains(pos)) {
visited.add(pos);
queue.offer(new int[]{nx, ny, steps + 1});
}
}
// 处理象的跳跃
for (int k = -100; k <= 100; k++) { // 选择一个合理的范围
// 对角线移动
int nx1 = x + k, ny1 = y + k;
if (nx1 == x2 && ny1 == y2) return steps + 1;
String pos1 = nx1 + "," + ny1;
if (!visited.contains(pos1)) {
visited.add(pos1);
queue.offer(new int[]{nx1, ny1, steps + 1});
}
int nx2 = x + k, ny2 = y - k;
if (nx2 == x2 && ny2 == y2) return steps + 1;
String pos2 = nx2 + "," + ny2;
if (!visited.contains(pos2)) {
visited.add(pos2);
queue.offer(new int[]{nx2, ny2, steps + 1});
}
}
}
return -1; // 理论上不会到达这里,因为题目保证会有解
}
public static void main(String[] args) {
System.out.println(solution(0, 0, 1, 1) == 1);
System.out.println(solution(0, 0, 2, 1) == 1);
System.out.println(solution(0, 0, 3, 3) == 1);
System.out.println(solution(-3, -2, 2, 1) == 2);
}
}
具体流程
以 solution(0, 0, 1, 1) 为例:
初始化: 起始位置 (0, 0),目标位置 (1, 1)。
马的跳跃方向:{{2, 1}, {2, -1}, {-2, 1}, {-2, -1}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2}}。象的跳跃方向:对角线移动。将起始位置 (0, 0, 0) 加入队列,其中第三个元素表示步数。初始化已访问集合 visited,并将 (0, 0) 加入 visited。
主循环: 从队列中取出第一个节点 (0, 0, 0)。检查该节点是否为目标节点 (1, 1),结果为不是目标节点。
处理马的跳跃:计算所有可能的新位置 (nx, ny),并检查是否在 visited 中。如果不在 visited 中,将其加入队列和 visited。
处理象的跳跃:计算所有可能的新位置 (nx, ny),并检查是否在 visited 中。如果不在 visited 中,将其加入队列和 visited。
继续循环: 从队列中取出下一个节点 (2, 1, 1)。检查该节点是否为目标节点 (1, 1),不是。继续处理马的跳跃和象的跳跃。 从队列中取出节点 (1, 1, 1)。检查该节点是否为目标节点 (1, 1),是。返回步数 1。
迭代加深深度优先搜索 (IDDFS)
初始化: 从深度 0 开始,逐步增加搜索深度。每次调用 dfs 函数进行深度优先搜索,限制搜索的最大深度。
主循环: 在主函数 solution 中,使用一个无限循环,逐步增加搜索深度 depth。对于每个深度 depth,调用 dfs 函数进行深度优先搜索。如果在当前深度 depth 找到目标节点,则返回 depth 作为结果。
深度优先搜索(DFS): dfs 函数接收当前节点的坐标 (x, y)、目标节点的坐标 (x2, y2) 和当前搜索的最大深度 depth。如果 depth 为 0 且当前节点是目标节点,则返回 true。否则,递归地处理马的跳跃和象的跳跃的所有可能的新位置,并检查是否在当前深度内找到目标节点。
终止条件: 如果在某个深度 depth 内找到了目标节点,则返回该深度。由于题目保证一定有解,因此不会出现找不到解的情况。
import java.util.*;
public class Main {
private static final int[][] knightMoves = {{2, 1}, {2, -1}, {-2, 1}, {-2, -1}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2}};
public static int solution(int x1, int y1, int x2, int y2) {
for (int depth = 0; ; depth++) {
if (dfs(x1, y1, x2, y2, depth)) {
return depth;
}
}
}
private static boolean dfs(int x, int y, int x2, int y2, int depth) {
if (depth == 0) {
return x == x2 && y == y2;
}
// 处理马的跳跃
for (int[] move : knightMoves) {
int nx = x + move[0], ny = y + move[1];
if (dfs(nx, ny, x2, y2, depth - 1)) {
return true;
}
}
// 处理象的跳跃
for (int k = -100; k <= 100; k++) { // 选择一个合理的范围
int nx1 = x + k, ny1 = y + k;
if (dfs(nx1, ny1, x2, y2, depth - 1)) {
return true;
}
int nx2 = x + k, ny2 = y - k;
if (dfs(nx2, ny2, x2, y2, depth - 1)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Main solver = new Main();
System.out.println(solver.solution(0, 0, 1, 1) == 1);
System.out.println(solver.solution(0, 0, 2, 1) == 1);
System.out.println(solver.solution(0, 0, 3, 3) == 1);
System.out.println(solver.solution(-3, -2, 2, 1) == 2);
}
}
具体流程
以 solution(0, 0, 1, 1) 为例:
初始化: 起始位置 (0, 0)。目标位置 (1, 1)。
马的跳跃方向:{{2, 1}, {2, -1}, {-2, 1}, {-2, -1}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2}}。象的跳跃方向:对角线移动。
主循环: 从深度 0 开始,逐步增加搜索深度。在每次迭代中,调用 dfs 函数进行深度优先搜索。
第一次迭代(深度 0): 调用 dfs(0, 0, 1, 1, 0)。由于 depth 为 0 且当前节点不是目标节点,返回 false。
第二次迭代(深度 1): 调用 dfs(0, 0, 1, 1, 1)。
处理马的跳跃:新位置:(2, 1), (-2, 1), (2, -1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)递归调用 dfs 函数,但这些新位置都不是目标节点 (1, 1)。
处理象的跳跃:新位置:(k, k), (k, -k) (对于 -100 <= k <= 100)递归调用 dfs 函数,发现 (1, 1) 是目标节点,返回 true。
找到目标节点: 由于在深度 1 内找到了目标节点 (1, 1),返回 1。
总结 广度优先搜索 (BFS)
优点:
- 保证找到最短路径:BFS从起点开始逐层扩展,一旦找到目标节点,就能保证这是最短路径。
- 空间效率:对于有限深度的问题,BFS的空间复杂度相对较低,因为它只保留当前层和下一层的节点。
- 实现简单:BFS的实现相对直观,使用队列来存储待处理的节点。
缺点:
- 内存消耗:在无限大的棋盘上,BFS可能会消耗大量的内存,因为需要存储所有可能的中间状态。
- 时间效率:虽然BFS可以保证找到最短路径,但在某些情况下,它可能会探索大量的无用节点,特别是在棋盘非常大的情况下。
迭代加深度优先搜索(IDDFS)
优点:
- 结合DFS和BFS的优点:IDDFS逐步增加搜索深度,直到找到解,既避免了DFS的栈溢出问题,又保证了找到最短路径。
- 内存效率:每次搜索的深度是有限的,因此不会像BFS那样占用大量内存。
- 实现简单:IDDFS的实现相对简单,只需要在DFS的基础上增加一个深度限制。
缺点:
- 重复计算:每次搜索都需要从头开始,可能会有一些重复计算。
- 时间效率:虽然IDDFS可以在找到第一个解时停止,但由于每次搜索都从头开始,总体时间效率可能不如BFS高。