一、數字推盤游戲
數字推盤游戲(n-puzzle)是一種最早的滑塊類游戲,常見的類型有十五數字推盤游戲和八數字推盤游戲等。也有以圖畫代替數字的推盤游戲。可能Noyes Palmer Chapman在1874年發明十五數字推盤,但Sam Loyd則在1891年也宣稱為其發明。
八數字推盤(又名重排九宮)則同樣是Noyes Palmer Chapman在1870年代發明,並且馬丁·加德納在科學科普雜志上尋求更快的解答。也有人宣稱重排九宮是傳統中國游戲,來自洛書,並且為華容道的祖先。
二、分支界定法
給定一個具有 8 個圖塊的 3×3 板(每個圖塊都有一個 1 到 8 的數字)和一個空白空間(用 0 代表)。目的是將數字放置在圖塊上,以使用空白的空間匹配最終配置。我們可以將四個相鄰的(左,右,上方和下方)圖塊滑動到空白區域。
通常可以使用 DFS、BFS 搜索算法來進行暴力破解。本文利用分支界定法,來“智能”的排名函數(近似於成本函數)來加快對成本節點的搜索,這里每一個節點都存有當前移動后整個方塊的分布,從而避免在找不到最終答案的子樹繼續搜索。
分支界定法基本上涉及三種類型的節點:
- 存活節點,是已生成但尚未生成其子節點的節點;
- 當前正在擴展的節點,探索它的子節點;
- 死亡節點,死節點是生成的節點,將不再擴展或探索。死節點的所有子節點均已擴展。
成本函數:C(x) = g(x) + h(x)。
g(x) 是當前節點到根節點的成本(即路徑長度)。h(x) 是當前除開空白塊外與答案節點錯位(放錯位置)的成本,假設在往上下左右任一方向移動圖塊的成本為 1。
給定初始狀態和目的狀態:
下圖顯示了上述算法從給定的8-Puzzle初始配置達到最終配置所遵循的路徑。注意,僅具有最小成本函數值的節點被擴展。
(注意:以上是本算法的流程圖)
三、分支界定法的實現
節點的構造:
1 /** 2 * 節點 3 */ 4 private class Node { 5 private Node parent; 6 private int[][] mat; 7 private int x, y; 8 private int cost; 9 private int level; 10 private Node() { 11 mat = new int[N][N]; 12 } 13 }
插入一個新節點:
1 /** 2 * 分配一個新節點 3 * 4 * @param mat 5 * @param x 6 * @param y 7 * @param newX 8 * @param newY 9 * @param level 10 * @param parent 11 * @return 12 */ 13 private Node newNode(int[][] mat, int x, int y, int newX, int newY, int level, Node parent) { 14 Node node = new Node(); 15 node.parent = parent; 16 17 copyMatrix(mat, node.mat); 18 19 swap(node.mat, x, y, newX, newY); 20 21 node.cost = Integer.MAX_VALUE; 22 node.level = level; 23 24 node.x = newX; 25 node.y = newY; 26 27 return node; 28 }
計算錯位成本:
1 /** 2 * 計算錯位方塊的數量, 即不在目標位置的非空白塊的數量 3 * 4 * @param initial 5 * @param finals 6 * @return 7 */ 8 private int calculateCost(int[][] initial, int[][] finals) { 9 int count = 0; 10 for (int i = 0; i < N; i++) 11 for (int j = 0; j < N; j++) 12 if (initial[i][j] != 0 && initial[i][j] != finals[i][j]) { 13 count++; 14 } 15 return count; 16 }
利用優先隊列(PriorityQueue)來實現分支界定法:
1 /** 2 * 分支界定法解決問題 3 * 4 * @param initial 5 * @param x 6 * @param y 7 * @param finals 8 */ 9 private void solve(int[][] initial, int x, int y, int[][] finals) { 10 // 創建優先級隊列以存儲搜索樹的活動節點 11 PriorityQueue<Node> pq = new PriorityQueue<>(new Comp()); 12 13 // 創建一個根節點並計算其成本 14 Node root = newNode(initial, x, y, x, y, 0, null); 15 root.cost = calculateCost(initial, finals); 16 17 // 將根添加到活動節點列表中; 18 pq.add(root); 19 20 // 查找成本最低的活動節點, 21 // 將其子級添加到活動節點列表中,並最后將其從列表中刪除。 22 while (!pq.isEmpty()) { 23 // 查找估計成本最低的活動節點, 找到的節點將從活動節點列表中刪除 24 Node min = pq.poll(); 25 26 // 如果min是一個答案節點 27 if (min.cost == 0) { 28 printPath(min); 29 return; 30 } 31 32 // 為每個min節點的孩子 33 // 一個節點最多4個孩子 34 for (int i = 0; i < 4; i++) { 35 if (isSafe(min.x + row[i], min.y + col[i])) { 36 // 創建一個子節點並計算它的成本 37 Node child = newNode(min.mat, min.x, min.y, 38 min.x + row[i], min.y + col[i], 39 min.level, min); 40 child.cost = calculateCost(child.mat, finals); 41 42 // 將min的孩子添加到活動節點列表 43 pq.add(child); 44 } 45 } 46 } 47 }
分支定界是一種算法設計范例,通常用於解決組合優化問題。 這些問題通常在時間復雜度上呈指數關系(2^N),在最壞的情況下可能需要探索所有可能的排列(擴展完堆中所有可能的節點)。 分支界定相對較快地解決了這些問題。但是在最壞的情況下,我們需要完全計算整個樹。充其量,我們只需要完全計算一條穿過樹的路徑,然后修剪其余路徑即可。
本文源代碼:

1 package algorithm; 2 3 import java.util.Comparator; 4 import java.util.PriorityQueue; 5 6 /** 7 * 重排九宮,或者稱之為八碼數問題,或是說數字推盤問題4,使用分支界定法實現 8 */ 9 public class EightPuzzle { 10 // 方陣邊長 11 private static final int N = 3; 12 13 // 坐標的行列索引向下、左、上、右 14 private static final int[] row = {1, 0, -1, 0}; 15 private static final int[] col = {0, -1, 0, 1}; 16 17 /** 18 * 節點 19 */ 20 private class Node { 21 private Node parent; 22 private int[][] mat; 23 private int x, y; 24 private int cost; 25 private int level; 26 private Node() { 27 mat = new int[N][N]; 28 } 29 } 30 31 /** 32 * 用於堆排序的比較對象 33 */ 34 class Comp implements Comparator<Node> { 35 @Override 36 public int compare(Node o1, Node o2) { 37 return (o1.cost + o1.level) - (o2.cost + o2.level); 38 } 39 } 40 41 /** 42 * 打印矩陣 43 * 44 * @param mat 45 */ 46 private void printMatrix(int[][] mat) { 47 for (int i = 0; i < N; i++) { 48 for (int j = 0; j < N; j++) 49 System.out.print(mat[i][j] + " "); 50 System.out.println(); 51 } 52 } 53 54 /** 55 * 交換二維矩陣中的值 56 * 57 * @param mat 58 * @param x 59 * @param y 60 * @param newX 61 * @param newY 62 */ 63 private void swap(int[][] mat, int x, int y, int newX, int newY) { 64 int tmp = mat[x][y]; 65 mat[x][y] = mat[newX][newY]; 66 mat[newX][newY] = tmp; 67 } 68 69 /** 70 * 矩陣復制 71 * 72 * @param arr1 73 * @param arr2 74 */ 75 private static void copyMatrix(int[][] arr1, int[][] arr2) { 76 for (int i = 0; i < arr1.length; i++) 77 System.arraycopy(arr1[i], 0, arr2[i], 0, arr1[0].length); 78 79 } 80 81 /** 82 * 分配一個新節點 83 * 84 * @param mat 85 * @param x 86 * @param y 87 * @param newX 88 * @param newY 89 * @param level 90 * @param parent 91 * @return 92 */ 93 private Node newNode(int[][] mat, int x, int y, int newX, int newY, int level, Node parent) { 94 Node node = new Node(); 95 node.parent = parent; 96 97 copyMatrix(mat, node.mat); 98 99 swap(node.mat, x, y, newX, newY); 100 101 node.cost = Integer.MAX_VALUE; 102 node.level = level; 103 104 node.x = newX; 105 node.y = newY; 106 107 return node; 108 } 109 110 /** 111 * 計算錯位方塊的數量, 即不在目標位置的非空白塊的數量 112 * 113 * @param initial 114 * @param finals 115 * @return 116 */ 117 private int calculateCost(int[][] initial, int[][] finals) { 118 int count = 0; 119 for (int i = 0; i < N; i++) 120 for (int j = 0; j < N; j++) 121 if (initial[i][j] != 0 && initial[i][j] != finals[i][j]) { 122 count++; 123 } 124 return count; 125 } 126 127 /** 128 * 檢查(x,y)是否為有效矩陣坐標 129 * 130 * @param x 131 * @param y 132 * @return 133 */ 134 private boolean isSafe(int x, int y) { 135 return (x >= 0 && x < N && y >= 0 && y < N); 136 } 137 138 /** 139 * 打印路徑 140 * 141 * @param root 142 */ 143 private void printPath(Node root) { 144 if (root == null) 145 return; 146 printPath(root.parent); 147 printMatrix(root.mat); 148 System.out.println(); 149 } 150 151 /** 152 * 分支界定法解決問題 153 * 154 * @param initial 155 * @param x 156 * @param y 157 * @param finals 158 */ 159 private void solve(int[][] initial, int x, int y, int[][] finals) { 160 // 創建優先級隊列以存儲搜索樹的活動節點 161 PriorityQueue<Node> pq = new PriorityQueue<>(new Comp()); 162 163 // 創建一個根節點並計算其成本 164 Node root = newNode(initial, x, y, x, y, 0, null); 165 root.cost = calculateCost(initial, finals); 166 167 // 將根添加到活動節點列表中; 168 pq.add(root); 169 170 // 查找成本最低的活動節點, 171 // 將其子級添加到活動節點列表中,並最后將其從列表中刪除。 172 while (!pq.isEmpty()) { 173 // 查找估計成本最低的活動節點, 找到的節點將從活動節點列表中刪除 174 Node min = pq.poll(); 175 176 // 如果min是一個答案節點 177 if (min.cost == 0) { 178 printPath(min); 179 return; 180 } 181 182 // 為每個min節點的孩子 183 // 一個節點最多4個孩子 184 for (int i = 0; i < 4; i++) { 185 if (isSafe(min.x + row[i], min.y + col[i])) { 186 // 創建一個子節點並計算它的成本 187 Node child = newNode(min.mat, min.x, min.y, 188 min.x + row[i], min.y + col[i], 189 min.level, min); 190 child.cost = calculateCost(child.mat, finals); 191 192 // 將min的孩子添加到活動節點列表 193 pq.add(child); 194 } 195 } 196 } 197 } 198 199 public static void main(String[] args) { 200 int[][] initial = 201 { 202 {1, 2, 3}, 203 {5, 6, 0}, 204 {7, 8, 4} 205 }; 206 207 int[][] finals = 208 { 209 {1, 2, 3}, 210 {5, 8, 6}, 211 {0, 7, 4} 212 }; 213 214 // 0的位置(空白塊) 215 int x = 1, y = 2; 216 217 EightPuzzle eightPuzzle = new EightPuzzle(); 218 eightPuzzle.solve(initial, x, y, finals); 219 220 } 221 }