启发式搜索求解八数码问题(Java实现,八数码小项目已开源)

问题的定义

又称九宫问题。在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,空格可以不超过边界地上下左右移动

要求解决的问题是:以启发式搜索方法求解给定初始状态和目标状态的最优搜索路径。
例如,那么空格应该向上走一步:
在这里插入图片描述

问题的解决

解的表示

将九宫格中数字从上到下、从左到右顺序排列后形成一个9个数字的序列(空格用0表示)来标识九宫格的一种状态。那么初始状态为:

123405678

目标状态为:

103425678

那么如何唯一的确定一个状态?这里用到了康托展开。

康托展开

cantor展开的公式:
X = a n ∗ ( n − 1 ) ! + a n − 1 ∗ ( n − 2 ) ! + a n − 1 ∗ ( n − 3 ) ! + . . . + a 1 ∗ 0 ! X = a_n * (n-1)! + a_{n-1}*(n-2)! + a_{n-1} * (n-3)!+...+a_1*0! X=an(n1)!+an1(n2)!+an1(n3)!+...+a10!
其中, a i a_i ai为整数,且 0 < = a i < i , 1 < = i < = n 0 <= a_i < i, 1<=i<=n 0<=ai<i,1<=i<=n

a i a_i ai表示原数的第i位在当前未出现的元素中是排在第几个。

康拓展开可以求一个排列是所有排列中的第几大,给排列分配了一个唯一的id。


例如: ( 1 , 2 , 3 ) (1, 2, 3) 1,2,3组成的排列,怎样知道 x = 213 x = 213 x=213是所有排列中的第几大的数?

  • a 1 = 2 a_1 = 2 a1=2,比2小的只有1,那么小于2开头的数有 1 ∗ 2 ! 1*2! 12!个。第一个数固定是1,第二个数有两种可能的情况,第三个数只有一种情况。
    在这里插入图片描述
  • a 2 = 1 a_2 = 1 a2=1,比1小的数没有(1~n的排列),那么小于第二位为1的数有 0 ∗ 1 ! 0*1! 01
  • 因此小于213的 ( 1 , 2 , 3 ) (1, 2, 3) (1,2,3)排列数有 1 ∗ 2 ! + 0 ∗ 1 ! = 2 1*2! + 0*1! = 2 12!+01!=2个,可知213是第3大的数,可以认为数213的id为2。
  • 1 ∗ 2 ! + 0 ∗ 1 ! = 2 1*2! + 0*1! = 2 12!+01!=2就是一个康托展开式,实现了全排列到自然数的双向映射

const int factorial[]={1,1,2,6,24,120,720,5040,40320,362880,3628800};//阶乘0-10
//cantor展开,n表示是n位的全排列,num[]表示全排列的数(用数组表示)
int cantor(int num[],int n){
    int ans=0,sum=0; //sum中存放a[i]的值
    for(int i=1;i<n;i++){
        for(int j=i+1;j<=n;j++)
            if(num[j]<num[i])
                sum++;
        ans+=sum*factorial[n-i];//累积
        sum=0;//计数器归零
    }
    return ans+1;
}

逆康托展开

知道一个排列是第几大的数,同样可以反过来求出这个排列是什么。
但是注意反向求时用的是id,而不是第几大。比如刚刚的213是第3大的数,id是2。
那么对排列(1, 2, 3), n = 3 n = 3 n=3 i d = 2 id = 2 id=2做逆康托展开:

  • 2 / 2 ! = 1 2/2! = 1 2/2!=1余0,则 a 3 = 1 a_3 = 1 a3=1,可知比第一位小的数有1个,所以首位为2
  • 0 / 1 ! = 0 0/1! = 0 0/1!=0余1,则 a 2 = 1 a_2= 1 a2=1,比第二位小的数有0个,所以第二位为1
  • 自然最后一个数就是3了
  • 得到了213,实现了十进制数到全排列的双射
//康托展开逆运算
void decantor(int x, int n)
{
    vector<int> v;  // 存放当前可选数
    vector<int> a;  // 所求排列组合
    for(int i=1;i<=n;i++)
        v.push_back(i);
    for(int i=n;i>=1;i--)
    {
        int r = x % factorial[i-1];
        int t = x / factorial[i-1];
        x = r;
        sort(v.begin(),v.end());// 从小到大排序
        a.push_back(v[t]);      // 剩余数里第t+1个数为当前位
        v.erase(v.begin()+t);   // 移除选做当前位的数
    }
    for(int i=0; i<a.size(); i++){
    	cout<<a[i]<<" "; 
	}
}

那么就可以把每个状态用一个十进制数id来表示了,并能够方便的判断两个状态是否相同,搜索过程中经常要判断是否搜索到了目标状态。

需要注意的是,因为空格我是用0表示,所以康托展开和逆康托展开是对于0到n-1的排列做,和1到n的计算过程有一点点区别,当然也可以直接用9表示空格。

不可达状态的识别

如果用户输入的初始状态和目标状态本身不可达,而我们又能提前识别出这种不可达情况,就可以避免很多无谓的尝试和计算。

可以用两个状态的序列逆序值的奇偶性来判断是否可达。注意判断逆序性时不考虑0

  • 数的逆序值:位于这个数前面的比这个数大的数的个数。
  • 序列的逆序值:数列中每个数的逆序值之和

比如:

序列值23158467
逆序值00200211

那么这个序列的逆序值就是 0 + 0 + 2 + 0 + 0 + 2 + 1 + 1 = 6 0+0+2+0+0+2+1+1 = 6 0+0+2+0+0+2+1+1=6

结论:
如果两个状态的数字序列的逆序值奇偶性一致,则两状态互相可达。

证明肯定是不会证明的…

启发函数

启发式搜索就是利用知识来引导搜索,尽量避免搜索无效的搜索、减少搜索范围,降低时间复杂度。启发信息的强弱对搜索的过程有重大影响。

对于九宫格问题启发信息一般有两种,分别是:

  • 取目标状态与当前状态相同的节点个数
  • 当前状态每个结点到目标状态相应结点所需步数的总和(曼哈顿距离)

感觉两个都比较合理,但是第二个更加合适。

因为在第一种算出来相同的情况下,往往需要再看第二种谁的步数总和小说明哪个解更优秀。

open表和close表

  • open表中用来记录考察过的点
  • close表中用来记录当前待考察的点

因为已经实现了每个状态都用一个十进制数id标识,那么open表用一个int型的数组就可以了。

而close表需要时刻按照待考察的解的启发信息高低来排序,比较适合存在一个堆里。

其他

为了最后输出步骤,需要一个int型数组parent来指示这个状态是怎样从前一个状态到达的,也就是走的方向,故需要记录前驱结点信息(包括id和方向;

为了中间的搜索过程能够动态展示出来,开启了新线程控制组件更新。
Java Swing开启多线程实现实时内容更新

搜索过程

参考文档

Created with Raphaël 2.3.0 开始 将初始状态放入close表 从close表中取出“最具潜力”的状态(解) 该状态是目标状态? 输出搜索步骤 结束 扩展该状态,并将扩展的状态加入close表 yes no

结果演示

数字会随着指令移动。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

源代码

Java实现(IDE为IDEA),已上传至github

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Java实现a*搜索算法求解8数码问题,不需要窗体: ``` import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.PriorityQueue; class Node { int[] puzzle; // 用一维数组表示8数码问题的状态 int cost; // 当前的代价函数值(启发函数值 + 已经扩展的步数) int steps; // 已经扩展的步数 Node parent; // 父节点,用于在搜索结束后回溯得到解路径 Node(int[] puzzle, int cost, int steps, Node parent) { this.puzzle = puzzle; this.cost = cost; this.steps = steps; this.parent = parent; } // 定义计算曼哈顿距离的方法 int manhattan() { int distance = 0; for (int i = 0; i < 9; i++) { if (puzzle[i] != 0) { int row = Math.abs((puzzle[i] - 1) / 3 - i / 3); int col = Math.abs((puzzle[i] - 1) % 3 - i % 3); distance += (row + col); } } return distance; } // 判断当前状态是否为目标状态 boolean isGoal() { for (int i = 0; i < 9; i++) { if (puzzle[i] != i) { return false; } } return true; } // 扩展当前状态,并返回扩展出的所有子节点 Node[] expand() { Node[] children = new Node[4]; int zeroIndex = 0; while (puzzle[zeroIndex] != 0) { zeroIndex++; } int row = zeroIndex / 3, col = zeroIndex % 3; int[][] moves = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} }; for (int i = 0; i < moves.length; i++) { int r = row + moves[i][0], c = col + moves[i][1]; if (r >= 0 && r < 3 && c >= 0 && c < 3) { int newIndex = r * 3 + c; int[] newPuzzle = Arrays.copyOf(puzzle, 9); newPuzzle[zeroIndex] = newPuzzle[newIndex]; newPuzzle[newIndex] = 0; Node child = new Node(newPuzzle, cost + 1 + manhattan(newPuzzle), steps + 1, this); children[i] = child; } } return children; } // 重写equals方法,用于将节点添加到HashSet中 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Node node = (Node) o; return Arrays.equals(puzzle, node.puzzle); } // 重写hashCode方法,用于将节点添加到HashSet中 @Override public int hashCode() { return Arrays.hashCode(puzzle); } } public class AStarAlgorithm { public static void main(String[] args) { int[] puzzle = {2, 8, 3, 1, 6, 4, 7, 0, 5}; // 起始状态 PriorityQueue<Node> openList = new PriorityQueue<>(Comparator.comparingInt(n -> n.cost)); HashSet<Node> closedList = new HashSet<>(); Node start = new Node(puzzle, 0 + start.manhattan(puzzle), 0, null); // 初始节点 openList.add(start); while (!openList.isEmpty()) { Node current = openList.poll(); if (current.isGoal()) { printPath(current); break; } closedList.add(current); Node[] children = current.expand(); for (Node child : children) { if (child != null && !closedList.contains(child)) { if (openList.contains(child)) { Node existing = null; for (Node n : openList) { if (n.equals(child)) { existing = n; break; } } if (existing.cost > child.cost) { openList.remove(existing); openList.offer(child); } } else { openList.offer(child); } } } } } // 回溯搜索路径 static void printPath(Node node) { int steps = 0; while (node != null) { System.out.println("Step " + steps++ + ":"); printPuzzle(node.puzzle); node = node.parent; } } // 打印8数码问题的状态 static void printPuzzle(int[] puzzle) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { System.out.print(puzzle[i * 3 + j] + " "); } System.out.println(); } System.out.println(); } } ``` 该实现使用了启发式搜索算法a*搜索来求解8数码问题。具体而言,它使用了曼哈顿距离作为启发函数,即每个数字与它在目标状态中位置之间的水平距离与垂直距离之和。由于每个数字移动的距离至少为1,因此该启发函数是一致的,并且可以保证找到的解路径是最短的。该算法使用了一个优先队列来维护已经生成但未扩展的节点,并按照每个节点的代价函数值进行排序。在扩展节点时,算法根据当前状态生成四个子节点,分别对应于移动空格到上下左右四个方向上的位置。算法将生成的每个子节点加入到优先队列中,并进行如下处理:如果该子节点已经在关闭列表中,直接忽略;如果该子节点已经在打开列表中,比较它的新代价函数值与已有代价函数值的大小,如果更小,则更新已有的节点;否则忽略。 这里提供的实现未包含图形用户界面或用户交互,而是直接输入初始状态,并输出找到的解路径。你可以改写该实现以添加图形用户界面或其他交互方式,以更好地展示该算法的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值