leetcode
leetcode 651 ~ 700
滑动谜题

滑动谜题

难度:

标签:

题目描述

在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示。一次 移动 定义为选择 0 与一个相邻的数字(上下左右)进行交换.

最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。

给出一个谜板的初始状态 board ,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1

 

示例 1:

输入:board = [[1,2,3],[4,0,5]]
输出:1
解释:交换 0 和 5 ,1 步完成

示例 2:

输入:board = [[1,2,3],[5,4,0]]
输出:-1
解释:没有办法完成谜板

示例 3:

输入:board = [[4,1,2],[5,0,3]]
输出:5
解释:
最少完成谜板的最少移动次数是 5 ,
一种移动路径:
尚未移动: [[4,1,2],[5,0,3]]
移动 1 次: [[4,1,2],[0,5,3]]
移动 2 次: [[0,1,2],[4,5,3]]
移动 3 次: [[1,0,2],[4,5,3]]
移动 4 次: [[1,2,0],[4,5,3]]
移动 5 次: [[1,2,3],[4,5,0]]

 

提示:

  • board.length == 2
  • board[i].length == 3
  • 0 <= board[i][j] <= 5
  • board[i][j] 中每个值都 不同

代码结果

运行时间: 28 ms, 内存: 0.0 MB


/*
题目思路:
使用Java Stream API来实现广度优先搜索(BFS)。
我们需要使用Stream来处理队列的操作,具体步骤与常规Java解法类似,只是使用Stream来替代部分操作。
1. 将初始状态转换成字符串以便处理。
2. 使用Stream来创建和处理队列,进行BFS搜索。
3. 在每次循环中,使用Stream来处理当前状态,生成新的状态。
4. 记录已经访问过的状态,避免重复处理。
5. 最终返回最少的移动次数,若找不到目标状态则返回-1。
*/
import java.util.*;
import java.util.stream.*;
 
public class SlidingPuzzleStream {
    private static final String TARGET = "123450";
    private static final int[][] NEIGHBORS = {{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}};
 
    public int slidingPuzzle(int[][] board) {
        StringBuilder sb = new StringBuilder();
        for (int[] row : board) {
            for (int num : row) {
                sb.append(num);
            }
        }
        String start = sb.toString();
        if (start.equals(TARGET)) return 0;
 
        Set<String> visited = new HashSet<>();
        visited.add(start);
        List<String> queue = Collections.singletonList(start);
        int moves = 0;
 
        while (!queue.isEmpty()) {
            List<String> nextQueue = new ArrayList<>();
            for (String current : queue) {
                if (current.equals(TARGET)) return moves;
                int zeroIndex = current.indexOf('0');
                for (int neighbor : NEIGHBORS[zeroIndex]) {
                    String next = swap(current, zeroIndex, neighbor);
                    if (visited.add(next)) {
                        nextQueue.add(next);
                    }
                }
            }
            queue = nextQueue;
            moves++;
        }
        return -1;
    }
 
    private String swap(String s, int i, int j) {
        char[] chars = s.toCharArray();
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
        return new String(chars);
    }
}

解释

方法:

本题解采用广度优先搜索(BFS)来解决滑动谜题。首先,将二维数组board转换为一维状态字符串,例如[[1,2,3],[4,0,5]]转化为'123405'。设定目标状态为'123450'。使用队列q来存储每一步的状态及其对应的移动次数,同时使用集合seen来记录已经访问过的状态,避免重复处理。对于当前状态,通过交换0与其相邻位置的数字来生成新的状态,并检查是否已达到目标状态或是否已被访问过。若达到目标状态,则返回对应的移动次数。如果队列被耗尽还未找到目标状态,则返回-1表示无法解开谜题。

时间复杂度:

O(1)

空间复杂度:

O(1)

代码细节讲解

🦆
为什么选择广度优先搜索(BFS)而不是深度优先搜索(DFS)来解决这个滑动谜题?
广度优先搜索(BFS)是解决此类最短路径或最少步骤问题的首选算法,因为它按层次遍历所有状态,确保一旦找到目标状态,此路径即为最短路径。相比之下,深度优先搜索(DFS)可能会首先探索较深的路径,导致找到的可能不是最短路径。此外,在具有大量状态的问题中,DFS可能会导致过深的递归,而BFS更易于管理和实现。
🦆
在使用BFS算法时,你如何处理可能重复的状态,以避免不必要的计算?
在BFS算法中,使用一个集合(在题解中是`seen`)来记录已经访问过的状态。每次生成新状态时,会检查该状态是否已存在于集合中。如果已存在,表明该状态已被处理过,因此跳过以避免重复工作和不必要的计算。这样做不仅节省了资源,还能防止无限循环。
🦆
在`get`函数中,如何确定0的邻居位置,并且为什么选择使用这种特定的邻居关系数组`NEIGHBORS`?
`get`函数中首先将状态字符串转为列表,并找出0的位置索引。使用`NEIGHBORS`数组可以快速查找0可以交换的位置。这个数组是根据0在一维数组中的位置预先定义的,描述了可能的交换位置。例如,如果0在位置1,它可以与位置0、2、4的元素交换。选择使用这种数组是因为它简化了查找过程并提高了效率,避免了在每次交换时重新计算邻居位置。
🦆
为什么在初始状态即为目标状态'123450'时,直接返回0而不进一步执行搜索过程?
如果初始状态已经是目标状态,这意味着不需要任何移动就已达到目标。在这种情况下,返回0是因为从初始状态到目标状态的最短路径长度为0,即不需要任何步骤。继续执行搜索过程将是不必要的,因为我们已经处于解决方案状态。

相关问题