文章目录
题目
标题和出处
标题:滑动谜题
出处: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 -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。记录上一轮状态的空间取决于状态数,因此和原始问题的空间复杂度相同。