|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[773. 滑动谜题](https://leetcode-cn.com/problems/sliding-puzzle/solution/gong-shui-san-xie-fa-hui-a-suan-fa-zui-d-3go8/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「BFS」、「最小步数」、「AStar 算法」、「启发式搜索」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示. |
| 10 | + |
| 11 | +一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换. |
| 12 | + |
| 13 | +最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。 |
| 14 | + |
| 15 | +给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。 |
| 16 | + |
| 17 | +示例: |
| 18 | +``` |
| 19 | +输入:board = [[1,2,3],[4,0,5]] |
| 20 | +输出:1 |
| 21 | +解释:交换 0 和 5 ,1 步完成 |
| 22 | +``` |
| 23 | +``` |
| 24 | +输入:board = [[1,2,3],[5,4,0]] |
| 25 | +输出:-1 |
| 26 | +解释:没有办法完成谜板 |
| 27 | +``` |
| 28 | +``` |
| 29 | +输入:board = [[4,1,2],[5,0,3]] |
| 30 | +输出:5 |
| 31 | +解释: |
| 32 | +最少完成谜板的最少移动次数是 5 , |
| 33 | +一种移动路径: |
| 34 | +尚未移动: [[4,1,2],[5,0,3]] |
| 35 | +移动 1 次: [[4,1,2],[0,5,3]] |
| 36 | +移动 2 次: [[0,1,2],[4,5,3]] |
| 37 | +移动 3 次: [[1,0,2],[4,5,3]] |
| 38 | +移动 4 次: [[1,2,0],[4,5,3]] |
| 39 | +移动 5 次: [[1,2,3],[4,5,0]] |
| 40 | +``` |
| 41 | +``` |
| 42 | +输入:board = [[3,2,4],[1,5,0]] |
| 43 | +输出:14 |
| 44 | +``` |
| 45 | +提示: |
| 46 | +* board 是一个如上所述的 2 x 3 的数组. |
| 47 | +* board[i][j] 是一个 [0, 1, 2, 3, 4, 5] 的排列. |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +### 基本分析 |
| 52 | + |
| 53 | +这是八数码问题的简化版:将 3ドル * 3$ 变为 2ドル * 3,ドル同时将「输出路径」变为「求最小步数」。 |
| 54 | + |
| 55 | +通常此类问题可以使用「BFS」、「AStar 算法」、「康拓展开」进行求解。 |
| 56 | + |
| 57 | +由于问题简化到了 2ドル * 3,ドル我们使用前两种解法即可。 |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +### BFS |
| 62 | + |
| 63 | +为了方便,将原来的二维矩阵转成字符串(一维矩阵)进行处理。 |
| 64 | + |
| 65 | +这样带来的好处直接可以作为哈希 `Key` 使用,也可以很方便进行「二维坐标」与「一维下标」的转换。 |
| 66 | + |
| 67 | +由于固定是 2ドル * 3$ 的格子,因此任意的合法二维坐标 $(x, y)$ 和对应一维下标 $idx$ 可通过以下转换: |
| 68 | + |
| 69 | +* $idx = x * 3 + y$ |
| 70 | +* $x = idx / 3,y = idx \% 3$ |
| 71 | + |
| 72 | +其余的就是常规的 `BFS` 过程了。 |
| 73 | + |
| 74 | +代码: |
| 75 | +```Java [] |
| 76 | +class Solution { |
| 77 | + class Node { |
| 78 | + String str; |
| 79 | + int x, y; |
| 80 | + Node(String _str, int _x, int _y) { |
| 81 | + str = _str; x = _x; y = _y; |
| 82 | + } |
| 83 | + } |
| 84 | + int n = 2, m = 3; |
| 85 | + String s, e; |
| 86 | + int x, y; |
| 87 | + public int slidingPuzzle(int[][] board) { |
| 88 | + s = ""; |
| 89 | + e = "123450"; |
| 90 | + for (int i = 0; i < n; i++) { |
| 91 | + for (int j = 0; j < m; j++) { |
| 92 | + s += board[i][j]; |
| 93 | + if (board[i][j] == 0) { |
| 94 | + x = i; y = j; |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + int ans = bfs(); |
| 99 | + return ans; |
| 100 | + } |
| 101 | + int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; |
| 102 | + int bfs() { |
| 103 | + Deque<Node> d = new ArrayDeque<>(); |
| 104 | + Map<String, Integer> map = new HashMap<>(); |
| 105 | + Node root = new Node(s, x, y); |
| 106 | + d.addLast(root); |
| 107 | + map.put(s, 0); |
| 108 | + while (!d.isEmpty()) { |
| 109 | + Node poll = d.pollFirst(); |
| 110 | + int step = map.get(poll.str); |
| 111 | + if (poll.str.equals(e)) return step; |
| 112 | + int dx = poll.x, dy = poll.y; |
| 113 | + for (int[] di : dirs) { |
| 114 | + int nx = dx + di[0], ny = dy + di[1]; |
| 115 | + if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; |
| 116 | + String nStr = update(poll.str, dx, dy, nx, ny); |
| 117 | + if (map.containsKey(nStr)) continue; |
| 118 | + Node next = new Node(nStr, nx, ny); |
| 119 | + d.addLast(next); |
| 120 | + map.put(nStr, step + 1); |
| 121 | + } |
| 122 | + } |
| 123 | + return -1; |
| 124 | + } |
| 125 | + String update(String cur, int i, int j, int p, int q) { |
| 126 | + char[] cs = cur.toCharArray(); |
| 127 | + char tmp = cs[i * m + j]; |
| 128 | + cs[i * m + j] = cs[p * m + q]; |
| 129 | + cs[p * m + q] = tmp; |
| 130 | + return String.valueOf(cs); |
| 131 | + } |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +### A\* 算法 |
| 138 | + |
| 139 | +可以直接根据本题规则来设计 A* 的「启发式函数」。 |
| 140 | + |
| 141 | +比如对于两个状态 `a` 和 `b` 可直接计算出「理论最小转换次数」:**所有位置的数值「所在位置」与「目标位置」的曼哈顿距离之和(即横纵坐标绝对值之和)** 。 |
| 142 | + |
| 143 | +注意,我们只需要计算「非空格」位置的曼哈顿距离即可,因为空格的位置会由其余数字占掉哪些位置而唯一确定。 |
| 144 | + |
| 145 | +**A\* 求最短路的正确性问题:由于我们衡量某个状态 `str` 的估值是以目标字符串 `e=123450` 为基准,因此我们只能确保 `e` 出队时为「距离最短」,而不能确保中间节点出队时「距离最短」,因此我们不能单纯根据某个节点是否「曾经入队」而决定是否入队,还要结合当前节点的「最小距离」是否被更新而决定是否入队。** |
| 146 | + |
| 147 | +这一点十分关键,在代码层面上体现在 `map.get(nStr) > step + 1` 的判断上。 |
| 148 | + |
| 149 | +**我们知道,A\* 在有解的情况下,才会发挥「启发式搜索」的最大价值,因此如果我们能够提前判断无解的情况,对 A\* 算法来说会是巨大的提升。** |
| 150 | + |
| 151 | +而对于通用的 $N * N$ 数码问题,判定有解的一个充要条件是:**「逆序对」数量为偶数,如果不满足,必然无解,直接返回 $-1$ 即可。** |
| 152 | + |
| 153 | +对该结论的充分性证明和必要性证明完全不在一个难度上,所以建议记住这个结论即可。 |
| 154 | + |
| 155 | +代码: |
| 156 | +```Java [] |
| 157 | +class Solution { |
| 158 | + class Node { |
| 159 | + String str; |
| 160 | + int x, y; |
| 161 | + int val; |
| 162 | + Node(String _str, int _x, int _y, int _val) { |
| 163 | + str = _str; x = _x; y = _y; val = _val; |
| 164 | + } |
| 165 | + } |
| 166 | + int f(String str) { |
| 167 | + int ans = 0; |
| 168 | + char[] cs1 = str.toCharArray(), cs2 = e.toCharArray(); |
| 169 | + for (int i = 0; i < n; i++) { |
| 170 | + for (int j = 0; j < m; j++) { |
| 171 | + // 跳过「空格」,计算其余数值的曼哈顿距离 |
| 172 | + if (cs1[i * m + j] == '0' || cs2[i * m + j] == '0') continue; |
| 173 | + int cur = cs1[i * m + j], next = cs2[i * m + j]; |
| 174 | + int xd = Math.abs((cur - 1) / 3 - (next - 1) / 3); |
| 175 | + int yd = Math.abs((cur - 1) % 3 - (next - 1) % 3); |
| 176 | + ans += (xd + yd); |
| 177 | + } |
| 178 | + } |
| 179 | + return ans; |
| 180 | + } |
| 181 | + int n = 2, m = 3; |
| 182 | + String s, e; |
| 183 | + int x, y; |
| 184 | + public int slidingPuzzle(int[][] board) { |
| 185 | + s = ""; |
| 186 | + e = "123450"; |
| 187 | + for (int i = 0; i < n; i++) { |
| 188 | + for (int j = 0; j < m; j++) { |
| 189 | + s += board[i][j]; |
| 190 | + if (board[i][j] == 0) { |
| 191 | + x = i; y = j; |
| 192 | + } |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + // 提前判断无解情况 |
| 197 | + if (!check(s)) return -1; |
| 198 | + |
| 199 | + int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; |
| 200 | + Node root = new Node(s, x, y, f(s)); |
| 201 | + PriorityQueue<Node> q = new PriorityQueue<>((a,b)->a.val-b.val); |
| 202 | + Map<String, Integer> map = new HashMap<>(); |
| 203 | + q.add(root); |
| 204 | + map.put(s, 0); |
| 205 | + while (!q.isEmpty()) { |
| 206 | + Node poll = q.poll(); |
| 207 | + int step = map.get(poll.str); |
| 208 | + if (poll.str.equals(e)) return step; |
| 209 | + int dx = poll.x, dy = poll.y; |
| 210 | + for (int[] di : dirs) { |
| 211 | + int nx = dx + di[0], ny = dy + di[1]; |
| 212 | + if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; |
| 213 | + String nStr = update(poll.str, dx, dy, nx, ny); |
| 214 | + if (!map.containsKey(nStr) || map.get(nStr) > step + 1) { |
| 215 | + Node next = new Node(nStr, nx, ny, step + 1 + f(nStr)); |
| 216 | + q.add(next); |
| 217 | + map.put(nStr, step + 1); |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + return 0x3f3f3f3f; // never |
| 222 | + } |
| 223 | + String update(String cur, int i, int j, int p, int q) { |
| 224 | + char[] cs = cur.toCharArray(); |
| 225 | + char tmp = cs[i * m + j]; |
| 226 | + cs[i * m + j] = cs[p * m + q]; |
| 227 | + cs[p * m + q] = tmp; |
| 228 | + return String.valueOf(cs); |
| 229 | + } |
| 230 | + boolean check(String str) { |
| 231 | + char[] cs = str.toCharArray(); |
| 232 | + List<Integer> list = new ArrayList<>(); |
| 233 | + for (int i = 0; i < n * m; i++) { |
| 234 | + if (cs[i] != '0') list.add(cs[i] - '0'); |
| 235 | + } |
| 236 | + int cnt = 0; |
| 237 | + for (int i = 0; i < list.size(); i++) { |
| 238 | + for (int j = i + 1; j < list.size(); j++) { |
| 239 | + if (list.get(i) < list.get(j)) cnt++; |
| 240 | + } |
| 241 | + } |
| 242 | + return cnt % 2 == 0; |
| 243 | + } |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +### 最后 |
| 250 | + |
| 251 | +这是我们「刷穿 LeetCode」系列文章的第 `No.773` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。 |
| 252 | + |
| 253 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 254 | + |
| 255 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 256 | + |
| 257 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 258 | + |
0 commit comments