算法:数字推盘游戏--重排九宫(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
posted @ 2019-12-22 22:32  賣贾笔的小男孩  阅读(4142)  评论(0编辑  收藏  举报