搜索题目:滑动谜题

文章目录

题目

标题和出处

标题:滑动谜题

出处:773. 滑动谜题

难度

6 级

题目描述

要求

在一个 2 × 3 \texttt{2} \times \texttt{3} 2×3 的板上有 5 \texttt{5} 5 块砖瓦,用数字 1 \texttt{1} 1 到 5 \texttt{5} 5 表示,以及一块空缺用 0 \texttt{0} 0 表示。一次移动 定义为选择 0 \texttt{0} 0 与四个方向上相邻的一个数字交换。

当且仅当板的结果是 [[1,2,3],[4,5,0]] \texttt{[[1,2,3],[4,5,0]]} [[1,2,3],[4,5,0]] 时,完成谜板。

给定一个谜板的初始状态 board \texttt{board} board,返回完成谜板的最少移动次数,如果不能完成谜板,返回 -1 \texttt{-1} -1。

示例

示例 1:

输入: board = [[1,2,3],[4,0,5]] \texttt{board = [[1,2,3],[4,0,5]]} board = [[1,2,3],[4,0,5]]

输出: 1 \texttt{1} 1

解释:交换 0 \texttt{0} 0 和 5 \texttt{5} 5, 1 \texttt{1} 1 步完成。

示例 2:

输入: board = [[1,2,3],[5,4,0]] \texttt{board = [[1,2,3],[5,4,0]]} board = [[1,2,3],[5,4,0]]

输出: -1 \texttt{-1} -1

解释:不能完成谜板。

示例 3:

输入: board = [[4,1,2],[5,0,3]] \texttt{board = [[4,1,2],[5,0,3]]} board = [[4,1,2],[5,0,3]]

输出: 5 \texttt{5} 5

解释:完成谜板的最少移动次数是 5 \texttt{5} 5。

一种移动路径:

移动 0 \texttt{0} 0 次: [[4,1,2],[5,0,3]] \texttt{[[4,1,2],[5,0,3]]} [[4,1,2],[5,0,3]]

移动 1 \texttt{1} 1 次: [[4,1,2],[0,5,3]] \texttt{[[4,1,2],[0,5,3]]} [[4,1,2],[0,5,3]]

移动 2 \texttt{2} 2 次: [[0,1,2],[4,5,3]] \texttt{[[0,1,2],[4,5,3]]} [[0,1,2],[4,5,3]]

移动 3 \texttt{3} 3 次: [[1,0,2],[4,5,3]] \texttt{[[1,0,2],[4,5,3]]} [[1,0,2],[4,5,3]]

移动 4 \texttt{4} 4 次: [[1,2,0],[4,5,3]] \texttt{[[1,2,0],[4,5,3]]} [[1,2,0],[4,5,3]]

移动 5 \texttt{5} 5 次: [[1,2,3],[4,5,0]] \texttt{[[1,2,3],[4,5,0]]} [[1,2,3],[4,5,0]]

数据范围

  • board.length = 2 \texttt{board.length} = \texttt{2} board.length=2
  • board[i].length = 3 \texttt{board[i].length} = \texttt{3} board[i].length=3
  • 0 ≤ board[i][j] ≤ 5 \texttt{0} \le \texttt{board[i][j]} \le \texttt{5} 0≤board[i][j]≤5
  • board[i][j] \texttt{board[i][j]} board[i][j] 中每个值各不相同

解法

思路和算法

计算最少移动次数,可以使用广度优先搜索实现。

从初始状态开始,对于每个状态,将 0 0 0 向每个相邻方向移动之后得到相邻状态,如果相邻状态尚未访问则继续访问相邻状态,当遍历到目标状态时,即可得到最少移动次数。

由于需要计算最少移动次数,因此在广度优先搜索的过程中需要将状态分层,确保每一轮遍历的状态为同一层的全部状态,同一层的全部状态所需最少移动次数相同。

首先将初始状态入队列,初始状态位于第 0 0 0 层,最少移动次数是 0 0 0。每一轮遍历之前需要首先得到队列内的状态数,此时队列内的状态为同一层的全部状态,然后访问这些状态,并将与这些状态相邻且未访问的状态入队列。一轮遍历结束之后,当前层的全部状态都已经出队列并被访问,此时队列内的状态为下一层的全部状态,下一轮遍历时即可访问下一层的全部状态。该做法可以确保每一轮遍历的状态为同一层的全部状态。

具体做法是,将移动次数初始化为 − 1 -1 −1,表示尚未访问任何状态。每一轮遍历时,将移动次数加 1 1 1,然后遍历当前层的全部状态,对于当前层的每个状态,执行如下操作。

  1. 如果当前状态与目标状态相同,则完成谜板,返回移动次数。

  2. 如果当前状态与目标状态不同,则获得当前状态的所有相邻的未访问状态,将其设为已访问并入队列。

当队列为空时,遍历结束,如果未遇到与目标状态相同的状态,则不能完成谜板,返回 − 1 -1 −1。

实现

广度优先搜索需要使用哈希集合存储已经访问过的状态。这道题中的状态是矩阵,哈希集合不能判断两个矩阵是否相等,因此需要转换表示方法。由于谜板的大小固定是 2 × 3 2 \times 3 2×3,因此可以使用长度为 6 6 6 的字符串表示状态,对于 0 ≤ i < 2 0 \le i < 2 0≤i<2 和 0 ≤ j < 3 0 \le j < 3 0≤j<3,谜板的第 i i i 行第 j j j 列的元素位于字符串的下标 i × 3 + j i \times 3 + j i×3+j 处。

计算每个状态的相邻状态时,需要得到 0 0 0 的位置,将 0 0 0 向每个相邻方向移动之后得到相邻状态。由于每个状态的长度是 6 6 6,因此可以直接遍历状态得到 0 0 0 的位置,也可以额外记录 0 0 0 的下标,减少遍历时间。

代码

java 复制代码
class Solution {
    static final int ROWS = 2, COLS = 3;
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    class Pair {
        String state;
        int zeroIndex;

        public Pair(String state) {
            this.state = state;
            int length = ROWS * COLS;
            for (int i = 0; i < length; i++) {
                if (state.charAt(i) == '0') {
                    zeroIndex = i;
                    break;
                }
            }
        }

        public Pair(String state, int zeroIndex) {
            this.state = state;
            this.zeroIndex = zeroIndex;
        }
    }

    public int slidingPuzzle(int[][] board) {
        String target = "123450";
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < ROWS; i++) {
            for (int j = 0; j < COLS; j++) {
                sb.append(board[i][j]);
            }
        }
        String start = sb.toString();
        Set<String> visited = new HashSet<String>();
        visited.add(start);
        Queue<Pair> queue = new ArrayDeque<Pair>();
        queue.offer(new Pair(start));
        int moves = -1;
        while (!queue.isEmpty()) {
            moves++;
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                Pair pair = queue.poll();
                String state = pair.state;
                char[] arr = state.toCharArray();
                int zeroIndex = pair.zeroIndex;
                if (target.equals(state)) {
                    return moves;
                }
                List<Integer> adjacentIndices = getAdjacentIndices(zeroIndex);
                for (int adjacentIndex : adjacentIndices) {
                    swap(arr, zeroIndex, adjacentIndex);
                    String newState = new String(arr);
                    if (visited.add(newState)) {
                        queue.offer(new Pair(newState, adjacentIndex));
                    }
                    swap(arr, zeroIndex, adjacentIndex);
                }
            }
        }
        return -1;
    }

    public List<Integer> getAdjacentIndices(int zeroIndex) {
        List<Integer> adjacentIndices = new ArrayList<Integer>();
        int row = zeroIndex / COLS, col = zeroIndex % COLS;
        for (int[] dir : dirs) {
            int newRow = row + dir[0], newCol = col + dir[1];
            if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
                adjacentIndices.add(newRow * COLS + newCol);
            }
        }
        return adjacentIndices;
    }

    public void swap(char[] state, int index1, int index2) {
        char temp = state[index1];
        state[index1] = state[index2];
        state[index2] = temp;
    }
}

复杂度分析

  • 时间复杂度: O ( ( m × n ) ! × ( m × n ) ) O((m \times n)! \times (m \times n)) O((m×n)!×(m×n)),其中 m m m 和 n n n 分别是谜板的行数和列数,这道题中 m = 2 m = 2 m=2, n = 3 n = 3 n=3。谜板的状态数是 O ( ( m × n ) ! ) O((m \times n)!) O((m×n)!),对于每个状态最多有 3 3 3 个相邻状态,每个状态的生成和遍历相邻状态需要 O ( m × n ) O(m \times n) O(m×n) 的时间,因此时间复杂度是 O ( ( m × n ) ! × ( m × n ) ) O((m \times n)! \times (m \times n)) O((m×n)!×(m×n))。

  • 空间复杂度: O ( ( m × n ) ! × ( m × n ) ) O((m \times n)! \times (m \times n)) O((m×n)!×(m×n)),其中 m m m 和 n n n 分别是谜板的行数和列数,这道题中 m = 2 m = 2 m=2, n = 3 n = 3 n=3。队列中的状态数是 O ( ( m × n ) ! ) O((m \times n)!) O((m×n)!),每个状态需要 O ( m × n ) O(m \times n) O(m×n) 的空间。

拓展问题

问题描述

原始问题只要求计算最少移动次数,在原始问题的基础上,可以提出拓展问题:移动次数最少时的移动序列是什么?如果有多种方案,返回其中任意一种。如果如果不能完成谜板,返回空序列。

解法分析

为了记录移动序列,需要记录相邻状态之间的关系,相邻状态指的是从其中一个状态可以经过一次移动达到另一个状态。对于每个状态,其相邻状态包括上一轮状态和下一轮状态,由于同一个状态有一个上一轮状态、一个或多个下一轮状态,为了得到明确的序列,应该记录每个状态的上一轮状态。

需要使用哈希表记录每个状态对应的上一轮状态。从目标状态开始遍历,当到达初始状态时,即可得到移动序列。

代码

下面的代码中,返回值是一个列表,包含从初始状态到目标状态完整序列中的所有状态,每个状态是一个字符型矩阵。

java 复制代码
class Solution {
    static final int ROWS = 2, COLS = 3;
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    class Pair {
        String state;
        int zeroIndex;

        public Pair(String state) {
            this.state = state;
            int length = ROWS * COLS;
            for (int i = 0; i < length; i++) {
                if (state.charAt(i) == '0') {
                    zeroIndex = i;
                    break;
                }
            }
        }

        public Pair(String state, int zeroIndex) {
            this.state = state;
            this.zeroIndex = zeroIndex;
        }
    }

    public List<char[][]> slidingPuzzle(int[][] board) {
        Map<String, String> stateMap = new HashMap<String, String>();
        String target = "123450";
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < ROWS; i++) {
            for (int j = 0; j < COLS; j++) {
                sb.append(board[i][j]);
            }
        }
        String start = sb.toString();
        Set<String> visited = new HashSet<String>();
        visited.add(start);
        Queue<Pair> queue = new ArrayDeque<Pair>();
        queue.offer(new Pair(start));
        int moves = -1;
        while (!queue.isEmpty()) {
            moves++;
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                Pair pair = queue.poll();
                String state = pair.state;
                char[] arr = state.toCharArray();
                int zeroIndex = pair.zeroIndex;
                if (target.equals(state)) {
                    return getSequence(target, stateMap);
                }
                List<Integer> adjacentIndices = getAdjacentIndices(zeroIndex);
                for (int adjacentIndex : adjacentIndices) {
                    swap(arr, zeroIndex, adjacentIndex);
                    String newState = new String(arr);
                    if (visited.add(newState)) {
                        queue.offer(new Pair(newState, adjacentIndex));
                        stateMap.put(newState, state);
                    }
                    swap(arr, zeroIndex, adjacentIndex);
                }
            }
        }
        return new ArrayList<char[][]>();
    }

    public List<Integer> getAdjacentIndices(int zeroIndex) {
        List<Integer> adjacentIndices = new ArrayList<Integer>();
        int row = zeroIndex / COLS, col = zeroIndex % COLS;
        for (int[] dir : dirs) {
            int newRow = row + dir[0], newCol = col + dir[1];
            if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
                adjacentIndices.add(newRow * COLS + newCol);
            }
        }
        return adjacentIndices;
    }

    public void swap(char[] state, int index1, int index2) {
        char temp = state[index1];
        state[index1] = state[index2];
        state[index2] = temp;
    }

    public List<char[][]> getSequence(String target, Map<String, String> stateMap) {
        List<char[][]> sequence = new ArrayList<char[][]>();
        String state = target;
        while (state != null) {
            sequence.add(stateToBoard(state));
            state = stateMap.get(state);
        }
        Collections.reverse(sequence);
        return sequence;
    }

    public char[][] stateToBoard(String state) {
        char[][] board = new char[ROWS][COLS];
        int length = state.length();
        for (int i = 0; i < length; i++) {
            board[i / COLS][i % COLS] = state.charAt(i);
        }
        return board;
    }
}

复杂度分析

  • 时间复杂度: O ( ( m × n ) ! × ( m × n ) ) O((m \times n)! \times (m \times n)) O((m×n)!×(m×n)),其中 m m m 和 n n n 分别是谜板的行数和列数,这道题中 m = 2 m = 2 m=2, n = 3 n = 3 n=3。遍历过程中对于每个状态记录上一轮状态的时间是 O ( m × n ) O(m \times n) O(m×n),移动序列的长度不超过状态数,因此和原始问题的时间复杂度相同。

  • 空间复杂度: O ( ( m × n ) ! × ( m × n ) ) O((m \times n)! \times (m \times n)) O((m×n)!×(m×n)),其中 m m m 和 n n n 分别是谜板的行数和列数,这道题中 m = 2 m = 2 m=2, n = 3 n = 3 n=3。记录上一轮状态的空间取决于状态数,因此和原始问题的空间复杂度相同。

相关推荐
伟大的车尔尼1 天前
搜索题目:颜色交替的最短路径
广度优先搜索
伟大的车尔尼5 天前
搜索题目:验证二叉树
并查集·深度优先搜索·广度优先搜索
伟大的车尔尼13 天前
搜索题目:单词接龙
广度优先搜索
伟大的车尔尼14 天前
搜索题目:最小基因变化
广度优先搜索
伟大的车尔尼16 天前
搜索题目:可能的二分法
并查集·深度优先搜索·广度优先搜索
伟大的车尔尼20 天前
搜索题目:边界着色
深度优先搜索·广度优先搜索
伟大的车尔尼1 个月前
搜索题目:二进制矩阵中的最短路径
广度优先搜索
伟大的车尔尼1 个月前
搜索题目:被围绕的区域
并查集·深度优先搜索·广度优先搜索
伟大的车尔尼1 个月前
搜索题目:地图分析
动态规划·广度优先搜索