问题描述
利用A*算法进行表1到表2的转换,要求空白块移动次数最少。
转换规则为:空白块只可以与上下左右四个方向的相邻数字交换。
表1 起始状态 表2 目标状态
算法简介
A*算法是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。该算法综合了Best-First Search和Dijkstra算法的优点:在进行启发式搜索提高算法效率的同时,基于评估函数计算损失,保证找到一条最优路径。
算法能否找到最优解的关键在于评估函数的选择,A*算法的评估函数表示为:f(n) = g(n) + h(n)
f(n) 是从初始状态经由状态n到目标状态的代价估计
g(n) 是在状态空间中从初始状态到状态n的实际代价
h(n) 是从状态n到目标状态的最佳路径的估计代价例如在8数码问题中,g(n) 表示状态空间树中搜索的层数,h(n) 表示状态n与目标状态中元素位置不同的元素个数。
算法步骤
设定两个集合,open集,close集
1. 将起始点加入open集(设置父亲节点为空)
2. 在open集中选着一个f(n)值最小的节点作为当前节点
2.1 将当前节点从open集中移除,添加到close集
2.2 如果当前节点为终点节点,那么结束搜索
2.3 处理当前节点的所有邻接节点,规则如下:
如果不在open集中,那么就将其添加到open集,并将该节点的父节点为当前节点
如果已经添加到open集中,重新计算f(n)值,如果f(n)值小于先前的f(n)值,那么就更新open集中相应节点的f(n)
如果该节点不可通过或者已经被添加到close集,那么不予处理
3、如果open集不为空,那么转到步骤2继续执行。
评估函数
1. f(n) = 状态n状态空间树中的搜索深度 + 状态n与目标状态不同的元素个数
效果:与8数码问题使用了相同的评估函数,大概跑了30W步无法求出解
评价:效果极差,15数码问题的状态空间树要远复杂于8 数码问题,且15数码问题中空白块的移动更为复杂,此评估函数不适用。
2. f(n) = 状态n状态空间树中的搜索深度 + 状态n与目标状态各个位置数字偏差的绝对值
效果:随着搜索的进行,空白块的移动集中在表格上部,表格下部几乎不移动 ,无法求出解
评价:因为下部数字较大,移动后差值较大造成评估值较大,因此搜索集中在了数值较小的部分,效果很差。
3. f(n) = 状态n状态空间树中的搜索深度 + 状态n与目标状态各个元素的路径差值(一维数组各元素的距离差之和)
效果:空白块最终移动55步得到目标状态。
评价:效果比较理想,但h(n)还可继续优化。
4. f(n) = 状态n状态空间树中的搜索深度 + 状态n与目标状态各个元素的曼哈顿距离
效果:空白块最终移动41步得到目标状态。
评价:效果理想。
实际上,1和2的评估函数效果大致相同,都将搜索局限在了一部分导致无法计算出问题的解。3实际是以一维数组各元素的距离差之和估计状态n到目标状态的曼哈顿距离,但此估计方式和计算平面两点的曼哈顿距离存在较大误差,因此只求解出可行解。
参考资料
源代码(Java实现)
public class Node { private final static int goalMatrix[][] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 0 } }; // 奇序列 private final static int init[] = { 11, 9, 4, 15, 1, 3, 0, 12, 7, 5, 8, 6, 13, 2, 10, 14 }; private final static int initX = 1; private final static int initY = 2; public int data[]; public float cost; public int level; public int x, y; public int preDirection; public Node pre; public Node() { data = new int[16]; cost = 0; level = 0; x = 0; y = 0; preDirection = -1; pre = null; } public Node(Node node) { data = new int[16]; for (int i = 0; i < data.length; i++) { data[i] = node.data[i]; } cost = 0; level = node.level; x = node.x; y = node.y; preDirection = node.preDirection; } public void initData() { for (int i = 0; i < data.length; i++) { data[i] = init[i]; } x = initX; y = initY; cost = level + costDistance(data, goalMatrix); } public int costDistance(int[] a, int[][] b) { // 计算二维数组元素之间的曼哈顿距离 int c[][] = new int[4][4]; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { c[i][j] = a[4 * i + j]; } } int cost = 0; boolean flag = false; for (int x1 = 0; x1 < 4; x1++) { for (int y1 = 0; y1 < 4; y1++) { flag = false; for (int x2 = 0; x2 < 4; x2++) { for (int y2 = 0; y2 < 4; y2++) { if (c[x1][y1] == b[x2][y2]) { cost += (Math.abs(x1 - x2) + Math.abs(y1 - y2)); flag = true; } if (flag) { break; } } if (flag) { break; } } } } return cost; } public int hasSameData(List<Node> list) { // 判断重复状态 boolean isFind = false; float minCost = 1000000000; for (int j = 0; j < list.size(); j++) { if (isSame(data, list.get(j).data)) { isFind = true; if (list.get(j).cost < minCost) { minCost = list.get(j).cost; } } } if (isFind) { for (int i = 0; i < list.size(); i++) { if (list.get(i).cost == minCost && isSame(data, list.get(i).data)) { return i; } } } return -1; } public boolean isSame(int[] a, int[] b) { boolean flag = true; for (int i = 0; i < a.length; i++) { if (a[i] != b[i]) { flag = false; break; } } return flag; } public Node change(int direction) { // 空白块移动 Node node = new Node(this); int i = 0, j = 0; switch (direction) { case 0: // 上 i = node.x - 1; j = node.y; node.preDirection = 1; break; case 1: // 下 i = node.x + 1; j = node.y; node.preDirection = 0; break; case 2: // 左 i = node.x; j = node.y - 1; node.preDirection = 3; break; case 3: // 右 i = node.x; j = node.y + 1; node.preDirection = 2; break; default: break; } if (i >= 0 && i <= 3 && j >= 0 && j <= 3) { // 边界限定 int temp = node.data[4 * node.x + node.y]; node.data[4 * node.x + node.y] = node.data[4 * i + j]; node.data[4 * i + j] = temp; node.x = i; node.y = j; node.level++; node.cost(); node.pre = this; } else { return null; } return node; } public void cost() { // 计算评估函数 cost = 0; cost += (level + costDistance(data, goalMatrix)); } public boolean isFinish() { boolean result = true; for (int i = 0; i < data.length; i++) { if (data[i] != goal[i]) { result = false; break; } } return result; } public void show() { System.out.println("**************"); for (int i = 0; i < data.length; i++) { System.out.printf("%-4d", data[i]); if ((i + 1) % 4 == 0) { System.out.println(); } } System.out.println("level:" + level); System.out.println("cost:" + cost); System.out.println("**************"); } public void finish() { Stack<Node> stack = new Stack<>(); int steps = 0; stack.push(this); while (pre != null) { stack.push(pre); pre = pre.pre; } steps = stack.size() - 1; while (!stack.empty()) { stack.pop().show(); } System.out.println("finish in " + steps + " steps"); } } public class Main { public static void main(String[] a) { Node node = new Node(); node.initData(); function(node, new ArrayList<Node>()); } static void function(Node n, List<Node> openList) { openList.add(n); while (!openList.isEmpty()) { int index = minIndex(openList); Node node = openList.get(index); openList.remove(index); if (node.isFinish()) { node.finish(); return; } else { addNode(node, openList); } } } public static void addNode(Node n, List<Node> openList) { int direction = 0; while (direction < 4) { Node node = n.change(direction); if (node != null && n.preDirection != direction) { int indexOpen = node.hasSameData(openList); if (indexOpen >= 0) { if (node.cost < openList.get(indexOpen).cost) { openList.get(indexOpen).cost = node.cost; openList.get(indexOpen).pre = node.pre; } } else { openList.add(node); } } direction++; } } public static int minIndex(List<Node> list) { int index = 0; for (int i = 1; i < list.size(); i++) { if (list.get(i).cost < list.get(index).cost) { index = i; } } return index; } }