算法:數字推盤游戲--重排九宮(8-puzzle)


一、數字推盤游戲

  數字推盤游戲(n-puzzle)是一種最早的滑塊類游戲,常見的類型有十五數字推盤游戲和八數字推盤游戲等。也有以圖畫代替數字的推盤游戲。可能Noyes Palmer Chapman在1874年發明十五數字推盤,但Sam Loyd則在1891年也宣稱為其發明

  八數字推盤(又名重排九宮)則同樣是Noyes Palmer Chapman在1870年代發明,並且馬丁·加德納在科學科普雜志上尋求更快的解答。也有人宣稱重排九宮是傳統中國游戲,來自洛書,並且為華容道的祖先。

二、分支界定法

  給定一個具有 8 個圖塊的 3×3 板(每個圖塊都有一個 1 到 8 的數字)和一個空白空間(用 0 代表)。目的是將數字放置在圖塊上,以使用空白的空間匹配最終配置。我們可以將四個相鄰的(左,右,上方和下方)圖塊滑動到空白區域。

  通常可以使用 DFSBFS 搜索算法來進行暴力破解。本文利用分支界定法,來“智能”的排名函數(近似於成本函數)來加快對成本節點的搜索,這里每一個節點都存有當前移動后整個方塊的分布,從而避免在找不到最終答案的子樹繼續搜索。

  分支界定法基本上涉及三種類型的節點:

  1. 存活節點,是已生成但尚未生成其子節點的節點;
  2. 當前正在擴展的節點,探索它的子節點;
  3. 死亡節點,死節點是生成的節點,將不再擴展或探索。死節點的所有子節點均已擴展。

  成本函數: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 }
View Code


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM