熟悉和掌握启发式搜索的定义、估价函数和算法过程,并利用 A* 算法求解 N 数码难题,理解求解流程和搜索顺序。
盲⽬的搜索⽅法没有利⽤问题本身的特性信息,在决定要被扩展的节点时,都没有考虑该节点在解的路径上的可能性有多⼤,是否有利于问题求解以及求出的解是否最优;
启发式搜索要⽤到问题⾃身的某些特性信息,在选择节点时充分利⽤与问题有关的特征信息,估计出节点的重要性,在搜索时选择重要性较⾼的节点以利于求得最优解。
启发性信息和估价函数
⽤于指导搜索过程,且与具体问题有关的控制性信息称为启发性信息;⽤于评价节点重要性的函数称为估价函数。
估价函数记作:
f ( x ) = g ( x ) + h ( x ) f(x)=g(x)+h(x) f(x)=g(x)+h(x)
g ( x ) g(x) g(x)为从初始节点 S 0 S_0 S0到节点 x x x已经实际付出的代价
h ( x ) h(x) h(x)是从节点 x x x到⽬标节点 S g S_g Sg的最优路径的估计代价,体现了问题的启发性信息,称为启发函数
f ( x ) f(x) f(x)表示从初始节点经过节点 x x x到达⽬标节点的最优路径的代价估价值,其作⽤是⽤来评估OPEN表中各节点的重要性,决定其次序
问题描述:
3×3九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格,只能通过棋牌向空格的移动来改变棋盘的布局。根据给定初始布局(即初始状态)和目标布局(即目标状态),如何移动棋牌才能从初始布局到达目标布局,找到合法的走步序列。
问题分析:
对于八数码问题的解决,首先要考虑是否有答案。每一个状态可认为是一个1×9的矩阵,由数学知识可知,计算这两个有序数列的逆序值,如果两者都是偶数或奇数,则可通过变换到达,否则这两个状态不可达。这样,就可以在具体解决问题之前判断出问题是否可解,从而可以避免不必要的搜索。
搜索过程:
宽度优先搜索算法的主要涉及到两个对象数组的使用,分别是OPEN表和CLOSED表,用来记录已扩展但未搜索的节点和已搜索的节点。由于宽度搜索是盲目的,所以并不需要计算估计函数的值,只需要根据OPEN表节点的顺序依次进行遍历即可。
扩展节点的方法:首先找到数值“0”所在的坐标,根据坐标选择可扩展的方向,例如坐标为(2,0)只可向右扩展和向上扩展。同时,为了避免死循环,扩展的方向不可与父节点的方向相反。
扩展节点后,将其放入OPEN表的尾部,然后再进行新一轮的遍历,直到找到与目标节点相同的节点或者OPEN为空为止。
流程图如下:
全局择优搜索(Global Optimization Search)属于启发式搜索,即利用已知问题中的启发式信息指导问题求解,而非蛮力穷举的盲目搜索。
此处,启发函数定义为: f ( n ) = g ( n ) + h ( n ) f(n) = g(n) + h(n) f(n)=g(n)+h(n),其中 g ( n ) g(n) g(n)表示当前结点的深度,h(n)表示定义为当前节点与目标节点差异的度量。
搜索过程:
与宽度优先搜索相比,启发性的全局择优搜索则需要通过计算估计函数的值来决定搜索的方向,而不是盲目的进行遍历。
同样地,全局择优算法也会涉及OPEN表和CLOSED表的使用。但与宽度优先搜索不同的是,每一次扩展子节点都要计算该节点的估计函数值,并且,在将某节点的所有扩展子节点加入OPEN表的尾部后,需要根据估计函数值对OPEN表进行从小到大排序,之后再从OPEN表取出第一个节点放入CLOSED表中,进行新一轮的遍历,直到找到与目标节点相同的节点或者OPEN为空为止。
流程图如下:
A* 算法与全局择优算法均是启发性的搜索算法,但全局择优算法仅适合于状态空间是树状结构的情况,如果状态空间是一个有向图的话,在OPEN表中可能会出现重复的节点,节点的重复将导致大量的冗余搜索,使效率大大降低。
A*算法的优势在于 : 如果某一问题有解,那么利用 A* 搜索算法对该问题进行搜索一定能以搜索到最优的解结束。
搜索过程:
A* 算法在全局择优算法的基础上对其进行了改进:在扩展节点的时候加入了对子节点存在性的判断。如果扩展的某个新子节点已经在OPEN表或CLOSED表中出现过并且新子节点的估价值更小,则将原节点删除,并将新节点添加到OPEN表中,否则保留原节点,不进行改动。
流程图如下:
本实验共涉及到两种启发函数 h ( n ) h(n) h(n),具体定义如下:
实验原理:从数值0遍历到数值8,将每个数字不在位的个数作为启发信息。
实现原理:从数值0遍历到数值8,将每个数字从当前位置移动到目标位置的欧拉距离作为启发信息。
表 1 不同启发函数 h ( n ) h(n) h(n) 求解 8 数码问题的结果比较
启发函数 h(n) | |||
不在位数 | 欧拉距离 | 0 | |
初始状态 | 130824765 | 130824765 | 130824765 |
目标状态 | 123804765 | 123804765 | 123804765 |
最优解 | --第 0 步-- 1|3| --------- 8|2|4 --------- 7|6|5 --第 1 步-- 1| |3 --------- 8|2|4 --------- 7|6|5 --第 2 步-- 1|2|3 --------- 8| |4 --------- 7|6|5 |
--第 0 步-- 1|3| --------- 8|2|4 --------- 7|6|5 --第 1 步-- 1| |3 --------- 8|2|4 --------- 7|6|5 --第 2 步-- 1|2|3 --------- 8| |4 --------- 7|6|5 |
--第 0 步-- 1|3| --------- 8|2|4 --------- 7|6|5 --第 1 步-- 1| |3 --------- 8|2|4 --------- 7|6|5 --第 2 步-- 1|2|3 --------- 8| |4 --------- 7|6|5 |
扩展节点数 | 2 | 2 | 4 |
生成节点数 | 3 | 3 | 7 |
运行时间 | 0.0308ms | 0.1244ms | 0.126ms |
表 2 不同启发函数 h ( n ) h(n) h(n) 求解 15 数码问题的结果比较
启发函数 h(n) | |||
不在位数 | 欧拉距离 | 0 | |
初始状态 | 1,2,3,4,5,6,7,8,9,10, 11,12,13,14,15,0 |
1,2,3,4,5,6,7,8,9,10, 11,12,13,14,15,0 |
1,2,3,4,5,6,7,8,9,10, 11,12,13,14,15,0 |
目标状态 | 1,2,3,4,5,6,7,8,9,10, 11,12,0,13,14,15 |
1,2,3,4,5,6,7,8,9,10, 11,12,0,13,14,15 |
1,2,3,4,5,6,7,8,9,10, 11,12,0,13,14,15 |
最优解 | --第 0 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14|15| --第 1 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14| |15 --第 2 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13| |14|15 --第 3 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- |13|14|15 |
--第 0 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14|15| --第 1 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14| |15 --第 2 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13| |14|15 --第 3 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- |13|14|15 |
--第 0 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14|15| --第 1 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13|14| |15 --第 2 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- 13| |14|15 --第 3 步-- 1| 2| 3| 4 ------------------- 5| 6| 7| 8 ------------------- 9|10|11|12 ------------------- |13|14|15 |
扩展节点数 | 3 | 3 | 7 |
生成节点数 | 4 | 4 | 13 |
运行时间 | 0.1444ms | 0.3127ms | 2.3763ms |
本实验采用Java高级程序设计语言实现八数码问题及十五数码问题的可视化,对不同启发函数下的不同算法进行性能分析。
该算法的实现并没有使用OPEN和CLOSED表对节点进行操作,甚至都没有使用链表结构。主要依赖于Java中自带的队列数据结构,调用自带函数,用队列的”入队“与”出队“实现广度优先搜索。
将起点入队后只要队列非空,持续执行取队首、出队,先判断该节点是否为目标状态,若为目标状态则结束,否则由该节点进行四连通扩展,通过Map记录每种状态是否出现过,若未出现过则将扩展子节点入队。每次扩展节点都要保存每个子节点对应的父节点,这是为了方便输出路径时直接根据Map向前遍历即可。
该算法使用自定义链表类作为OPEN表,该链表的首节点为空,从其next节点开始保存状态信息;使用以String为键、以Boolean为值的Map作为CLOSED表,这是因为CLOSED表只需记录某个节点是否还可以进行扩展,因此无需为其创建链表。该算法并未采用对OPEN表按估计函数值从小到大对节点继续排序,而是每次去遍历OPEN表,找到估计函数值最小的节点作为扩展节点,这样可以有效的降低时间复杂度。
先向OPEN表中起点及其信息,只要OPEN表的首节点有后继节点就死循环,循环中先通过自定义函数找到OPEN链表中估计函数值最小的节点,如果该节点的状态与目标状态一致,则说明到达目标状态,退出;否则在此状态下确定空格的位置,让其与四个方向的数字交换,得到四种不同的状态,对于CLOSED表中存在的状态不予以考虑;若剩余的状态在不OPEN表则将其加入到OPEN表中。
与”全局择优搜索“基本一致,唯一不同的是:对于不在CLOSED表中的扩展子节点,若扩展到的状态在OPEN表中存在则还需要判断是已保存在OPEN表中的该状态对应的估计函数值与由当前节点扩展到该状态的估计函数值的大小关系,若由当前节点扩展到该状态的估计函数值更小,则需要将原节点删除并将该状态对应的新节点加入OPEN表中;若扩展到的状态在OPEN表中不存在,则该状态对应的节点无条件加入到OPEN表。
左侧九宫格(或十六宫格)对应初始状态,右侧九宫格(或十六宫格)对应目标状态,两种状态均可由用户向文本框中输入,若输入不合法则弹出警告框,又若输入的初始状态无法到达目标状态,则会根据算法判断逆序数的奇偶性,若无解会弹出警告框。
用户需先对靠上的下拉列表进行操作选择”八数码“还是”十五数码“,点击其下方的”确定“后宫格将切换成对应的样式;在方格内输入初始状态和目标状态后通过靠下的下拉列表选择采用”估价函数1“还是”估价函数2“,点击其下方的”确定“后开始执行算法。
左下方的选择卡总共三个TAB,分别对应三种算法,每个TAB中的文本域显示对应算法的执行过程,即从初始状态变换到目标状态的操作方式。右下方的文本域显示三种算法在选取不同的估价函数后得到结果的最佳步数和时间,可以根据显示对算法性能进行对比分析。
若不满足这些要求接下来的操作会弹出错误提示框无法继续执行,甚至程序崩溃。
若输入的初始状态无法到达目标状态,则弹出警告框。
采用启发函数1解决八数码问题,左侧九宫格为初始状态,右侧九宫格为目标状态。下图为默认状态,用户可以根据需要向九宫格中输入初始状态和目标状态。点击“八数码”下方的“确定”表示选定解决八数码问题,点击“估价函数1”下方的“确定”表示选定使用“估价函数1”作为启发函数解决此问题。
下方左侧选择卡中的文本域显示不同算法执行每一步得到的状态,右侧文本域显示采用不同算法解决该问题所需的时间与计算得到的最短移动次数。
操作靠上的下拉列表切换成”十五数码“后点击其下方的”确定“,可以实现将”初始状态“和”目标状态“的输入框转换为 4 × 4 4×4 4×4的格式。输入完成后操作靠下的下拉列表切换启发函数,点击其下方的”确定“后执行三种算法,时间与步数将不覆盖地显示在右下侧的文本域中,完整移动过程将覆盖地显示与不同算法各自的执行过程。
Window.java 程序
其中包含了界面设计的代码和三个核心算法。
package exp1;
import javax.swing.*;
import java.awt.*;
import java.util.*;
/**
* @author LJR
* @create 2021-11-04 18:52
* @description
* 要求:
* 1. 空格子内必须是一个空格!
* 2. 必须先确定是“八数码”还是“十五数码”!否则出错
* 不足:
* 1. 当一个方格内的输入存在两位字符时,在文本域中会出现显示不标准的情况
* 2. 未对“要求”中的错误情况进行健壮性处理
* 3. 未可视化空格的移动过程
* 4. 代码冗余度高
*/
public class Window extends JFrame {
private static final long serialVersionUID = -6740703588976621222L;
int size = 8;
int row_max = 0, col_max = 0; // 数码表的行数与列数
int width = 0, height = 0; // 数码表每格宽高
int left_x = 0, left_y = 0, right_x = 0, right_y = 0; // 初始数码表的左上角坐标,目标数码表的右上角坐标
int fontsize = 0;
int nums_original[] = {1, 3, 0, 8, 2, 4, 7, 6, 5}; // 八数码的测试
int nums_target[] = {1, 2, 3, 8, 0, 4, 7, 6, 5};
public Window() {
super("A* 算法求解 8 数码问题");
Container c = this.getContentPane();
c.add(getJButton());
c.setBackground(Color.white);
this.setSize(700, 850);
this.setUndecorated(false);
this.setLocationRelativeTo(null);
this.setVisible(true);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
JButton jButton, jb; // 确定按钮
JLabel label1, label2; // 文字标题
JComboBox box; // 下拉列表
JComboBox EorF; // 下拉列表
JTextField jf[][] = new JTextField[3][20]; // 九宫格或十六宫格
JTextArea textarea1, textarea2, textarea3; // 左下方三个文本域
JTextArea ja; // 右下方文本域
int v1steps = 0, v2steps = 0, v3steps = 0; // 全局择优、A*、BFS对应的步数
double v1time = 0, v2time = 0, v3time = 0; // 全局择优、A*、BFS对应的用时
public JPanel getJButton() {
JPanel jP = new JPanel();
jP.setOpaque(false);
jP.setLayout(null);// 设置空布局,即绝对布局
// “确认”按钮
jb = new JButton("确认");
jb.setBounds(293, 220, 100, 35);// 设置位置及大小
jb.setFont(new Font("华文行楷", Font.PLAIN, 18));
jb.setFocusPainted(false);
jButton = new JButton("确认");
jButton.setBounds(293, 315, 100, 35);// 设置位置及大小
jButton.setFont(new Font("华文行楷", Font.PLAIN, 18));
jButton.setFocusPainted(false);
jP.add(jb);
jP.add(jButton);
// "初始状态"
JLabel start = new JLabel("初始状态");
start.setBounds(110, 130, 500, 35);
start.setFont(new Font("华文行楷", Font.PLAIN, 20));
jP.add(start);
// "目标状态"
JLabel target = new JLabel("目标状态");
target.setBounds(490, 130, 500, 35);
target.setFont(new Font("华文行楷", Font.PLAIN, 20));
jP.add(target);
// 第一行文字
label1 = new JLabel("请分别填入初始状态(左)和目标状态(右)");
label1.setBounds(110, 30, 500, 35);
label1.setFont(new Font("华文行楷", Font.PLAIN, 25));
jP.add(label1);
// 第二行文字
label2 = new JLabel("点击“确定”按钮查看移动过程和分析结果");
label2.setBounds(140, 80, 500, 35);
label2.setFont(new Font("华文行楷", Font.PLAIN, 22));
jP.add(label2);
// 下拉列表
EorF = new JComboBox();
EorF.setBounds(277, 170, 130, 40);
EorF.setFont(new Font("华文行楷", Font.PLAIN, 18));
EorF.addItem(" 八数码");
EorF.addItem(" 十五数码");
box = new JComboBox();
box.setBounds(277, 265, 130, 40);
box.setFont(new Font("华文行楷", Font.PLAIN, 18));
box.addItem(" 估价函数 1");
box.addItem(" 估价函数 2");
jP.add(EorF);
jP.add(box);
// 显示数码时的格式
// int nums_original[] = {1, 3, 0, 8, 2, 4, 7, 6, 5}; // 八数码的测试
// int nums_target[] = {1, 2, 3, 8, 0, 4, 7, 6, 5};
// int nums_original[] = {1, 2, 3, 4, 5, 6, 7, 8, 0}; // 八数码的测试
// int nums_target[] = {1, 2, 3, 4, 6, 5, 7, 0, 8};
// int nums_original[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 11, 12, 13, 14, 15}; // 十五数码的测试
// int nums_target[] = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
if (size == 8) {
row_max = col_max = 3;
width = height = 60;
left_x = 60;
left_y = 170;
right_x = 445;
right_y = 170;
fontsize = 30;
} else if (size == 15) {
row_max = col_max = 4;
width = height = 45;
left_x = 60; // 40
left_y = 170;
right_x = 445; // 465
right_y = 170;
fontsize = 25;
}
// 为八数码按钮绑定点击监听
jb.addActionListener((actionEvent ->{
int eightOrFifteen = EorF.getSelectedIndex();
if (eightOrFifteen == 0) {
size = 8;
row_max = col_max = 3;
width = height = 60;
left_x = 60;
left_y = 170;
right_x = 445;
right_y = 170;
fontsize = 30;
nums_original = new int[]{1, 3, 0, 8, 2, 4, 7, 6, 5}; // 八数码的测试
nums_target = new int[]{1, 2, 3, 8, 0, 4, 7, 6, 5};
// 删除十六宫格文本域组件,并显示九宫格组件
for (int i = 0;i < 16;i ++) {
try {
jf[0][i].setVisible(false);
jf[1][i].setVisible(false);
} catch (Exception e) {
}
}
for (int row = 0; row < row_max; row++) // 按顺序依次创建方格并进行排列
for (int col = 0; col < col_max; col++) {
int left_ch = nums_original[row * row_max + col];
int right_ch = nums_target[row * row_max + col];
jf[0][row * row_max + col] = new JTextField(left_ch + ""); // 创建文本框
jf[1][row * row_max + col] = new JTextField(right_ch + "");
jf[0][row * row_max + col].setBounds(col * width + left_x, row * height + left_y, width, height);
jf[1][row * row_max + col].setBounds(col * width + right_x, row * height + right_y, width, height);
jf[0][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[1][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[0][row * row_max + col].setOpaque(false); // 文本框透明
jf[1][row * row_max + col].setOpaque(false);
jf[0][row * row_max + col].setDocument(new LimitedLength()); // 限制文本框中的内容不超过两个字符
jf[1][row * row_max + col].setDocument(new LimitedLength());
jf[0][row * row_max + col].setText(left_ch == 0 ? " " : left_ch + ""); // 文本框中的默认数字
jf[1][row * row_max + col].setText(right_ch == 0 ? " " : right_ch + "");
jf[0][row * row_max + col].setVisible(true);
jf[1][row * row_max + col].setVisible(true); // 设置可见后相当于覆盖
jP.add(jf[0][row * row_max + col]);
jP.add(jf[1][row * row_max + col]);
}
} else {
size = 15;
row_max = col_max = 4;
width = height = 45;
left_x = 60; // 40
left_y = 170;
right_x = 445; // 465
right_y = 170;
fontsize = 25;
nums_original = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0}; // 十五数码的测试
nums_target = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 13, 14, 15};
// 删除九宫格文本域组件
for (int i = 0;i < 16;i ++) {
try {
jf[0][i].setVisible(false);
jf[1][i].setVisible(false);
} catch (Exception e) {
}
}
for (int row = 0; row < row_max; row++)
for (int col = 0; col < col_max; col++) {
int left_ch = nums_original[row * row_max + col];
int right_ch = nums_target[row * row_max + col];
jf[0][row * row_max + col] = new JTextField(left_ch + "");
jf[1][row * row_max + col] = new JTextField(right_ch + "");
jf[0][row * row_max + col].setBounds(col * width + left_x, row * height + left_y, width, height);
jf[1][row * row_max + col].setBounds(col * width + right_x, row * height + right_y, width, height);
jf[0][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[1][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[0][row * row_max + col].setOpaque(false); // 文本框透明
jf[1][row * row_max + col].setOpaque(false);
jf[0][row * row_max + col].setDocument(new LimitedLength()); // 限制文本框中的内容不超过两个字符
jf[1][row * row_max + col].setDocument(new LimitedLength());
jf[0][row * row_max + col].setText(left_ch == 0 ? " " : left_ch + ""); // 文本框中的默认数字
jf[1][row * row_max + col].setText(right_ch == 0 ? " " : right_ch + "");
jf[0][row * row_max + col].setVisible(true);
jf[1][row * row_max + col].setVisible(true);
jP.add(jf[0][row * row_max + col]);
jP.add(jf[1][row * row_max + col]);
}
}
}));
// 为估价函数按钮绑定点击监听
jButton.addActionListener((actionEvent -> {
boolean flag = true, empty = false;
/* 将初始文本框和目标文本框中的内容存在String数组中,排序后逐一比较,要求完全相同方可执行算法 */
String[] left = new String[size + 1], right = new String[size + 1]; // 用于排序,方便判断初始和目标包含的数字是否一致
String[] left_copy = new String[size + 1], right_copy = new String[size + 1]; // 原位置
for (int i = 0; i <= size; i++) {
left[i] = new String(jf[0][i].getText());
right[i] = new String(jf[1][i].getText());
left_copy[i] = new String(jf[0][i].getText());
right_copy[i] = new String(jf[1][i].getText());
}
Arrays.sort(left); // 排序
Arrays.sort(right);
for (int i = 0; i <= size; i++) { // 判断是否存在不同的字符串,是否存在重复字符串
if (!left[i].equals(right[i]) || (i != size && left[i].equals(left[i + 1]))) {
flag = false;
break;
}
if (left[i].equals(" ")) empty = true; // 必须要存在空格
}
if (flag && empty) { // 左右文本框内容不存在异常
String string = box.getSelectedItem().toString();
char option = string.charAt(string.length() - 1);
int eightOrFifteen = EorF.getSelectedIndex();
if (calDe(left_copy) % 2 != calDe(right_copy) % 2)
JOptionPane.showMessageDialog(null, "无解", "错误", JOptionPane.ERROR_MESSAGE);
else {
/* 左侧GOS文本域 */
textarea1.setText(""); // 清空文本域
new GOS(left_copy, right_copy, Integer.parseInt(option + ""), eightOrFifteen);
/* 左侧A*文本域 */
textarea2.setText("");
new Astar(left_copy, right_copy, Integer.parseInt(option + ""), eightOrFifteen);
/* 左侧BFS文本域 */
textarea3.setText("");
new BFS(left_copy, right_copy, eightOrFifteen);
/* 选择的估价函数 */
if (option == '1') ja.append("h1(n),启发函数定义为当前节点与目标节点差异的度量:即当前节点与目标节点格局相比,位置不符的数字个数。\n\n");
else if(option == '2') ja.append("h2(n),启发函数定义为当前节点与目标节点距离的度量:当前节点与目标节点格局相比,位置不符的数字移动到目标节点中对应位置的最短距离之和。\n\n");
/* 右侧GOS时间与步数 */
ja.append("全局择优算法所需时间为:" + v1time + "ms," + "所需步数为:" + v1steps + "\n\n");
v1steps = 0;
/* 右侧A*时间与步数 */
ja.append("A*算法所需时间为:" + v2time + "ms," + "所需步数为:" + v2steps + "\n\n");
v2steps = 0;
/* 右侧BFS时间与步数 */
ja.append("BFS算法所需时间为:" + v3time + "ms," + "所需步数为:" + v3steps + "\n\n");
v3steps = 0;
ja.append("------------------------------------------\n");
/* 文本域自动滚动 */
ja.setCaretPosition(ja.getText().length());
}
} else { // 弹出警告框
JOptionPane.showMessageDialog(null, "初始数码与目标数码异常", "错误", JOptionPane.ERROR_MESSAGE);
}
}));
// 初始数码与目标数码的显示(默认,即刚打开程序得到的界面)
for (int row = 0; row < row_max; row++)
for (int col = 0; col < col_max; col++) {
int left_ch = nums_original[row * row_max + col];
int right_ch = nums_target[row * row_max + col];
jf[0][row * row_max + col] = new JTextField(left_ch + "");
jf[1][row * row_max + col] = new JTextField(right_ch + "");
jf[0][row * row_max + col].setBounds(col * width + left_x, row * height + left_y, width, height);
jf[1][row * row_max + col].setBounds(col * width + right_x, row * height + right_y, width, height);
jf[0][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[1][row * row_max + col].setFont(new Font("黑体", Font.PLAIN, fontsize));
jf[0][row * row_max + col].setOpaque(false); // 文本框透明
jf[1][row * row_max + col].setOpaque(false);
jf[0][row * row_max + col].setDocument(new LimitedLength()); // 限制文本框中的内容不超过两个字符
jf[1][row * row_max + col].setDocument(new LimitedLength());
jf[0][row * row_max + col].setText(left_ch == 0 ? " " : left_ch + ""); // 文本框中的默认数字
jf[1][row * row_max + col].setText(right_ch == 0 ? " " : right_ch + "");
jP.add(jf[0][row * row_max + col]);
jP.add(jf[1][row * row_max + col]);
}
// 选择卡
/* 将三个文本域分别加入到三个JScrollPane(滚动条面板)中,将三个滚动条面板加入同一个JTabbedPane(选择卡)的不同tab中 */
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.setBounds(40, 410, 275, 360);
/* 第一个文本域 */
textarea1 = new JTextArea();
textarea1.setLineWrap(true); // 文本自动换行
textarea1.setFont(new Font("宋体", Font.PLAIN, 15)); // 设置文本域字体
textarea1.setEditable(false); // 设置不可编辑文本域,只能通过程序执行修改
JScrollPane scrollPane1 = new JScrollPane(textarea1); // 创建滚动条面板
scrollPane1.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); // 总是出现滚动条
/* 第二个文本域 */
textarea2 = new JTextArea();
textarea2.setFont(new Font("宋体", Font.PLAIN, 15));
textarea2.setLineWrap(true);
textarea2.setEditable(false);
JScrollPane scrollPane2 = new JScrollPane(textarea2);
scrollPane2.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
/* 第三个文本域 */
textarea3 = new JTextArea();
textarea3.setFont(new Font("宋体", Font.PLAIN, 15));
textarea3.setLineWrap(true);
textarea3.setEditable(false);
JScrollPane scrollPane3 = new JScrollPane(textarea3);
scrollPane3.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
/* 设置选择卡 */
tabbedPane.setFont(new Font("宋体", Font.PLAIN, 12)); // 设置tab按键中的字体
tabbedPane.addTab("全局择优搜索", scrollPane1); // 设置tab按键中的文字
tabbedPane.addTab("A*搜索", scrollPane2);
tabbedPane.addTab("宽度优先搜索", scrollPane3);
jP.add(tabbedPane); // 添加选择卡面板
// 文本域(显示用时)
ja = new JTextArea();
JScrollPane sp = new JScrollPane();
sp.setBounds(370, 410, 275, 360); // 创建滚动条面板,也不用设置文本域的位置了
ja.setFont(new Font("宋体", Font.PLAIN, 12));
ja.setLineWrap(true); // 自动换行
sp.setViewportView(ja); // 将文本域加入到滚动条面板中
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); // 总是出现滚动条
ja.setEditable(false); // 设置不可编辑文本域,只能通过程序执行修改
jP.add(sp); // 只需将滚动条面板加入jP中,无需将文本域加入到jP中
// 左侧文本域上方文字信息:"执行过程"
JLabel label3 = new JLabel("执行过程");
label3.setBounds(120, 220, 275, 330);
label3.setFont(new Font("华文行楷", Font.PLAIN, 25));
jP.add(label3);
// 右侧文本域上方文字信息:"时间和最短移动次数"
JLabel label4 = new JLabel("时间和最短移动次数");
label4.setBounds(395, 220, 275, 330);
label4.setFont(new Font("华文行楷", Font.PLAIN, 25));
jP.add(label4);
// jP.setFocusable(false); // 让光标开始不聚焦
return jP;
}
public int calDe(String[] s) { // 计算逆序对数
int sum = 0;
for (int i = 0; i <= size; i++) {
for (int j = i + 1; j <= size; j++) {
if (s[j].equals(" ")) continue;
if (s[i].compareTo(s[j]) > 0) sum++;
}
}
return sum;
}
// A*
class Astar {
final int offset[][] = new int[][]{{-1, 1, 0, 0}, {0, 0, -1, 1}};
int evaluate_function = 1; // 启发函数1
int eightOrFifteen = 0; // 0:8 1:15
int size; // 方格总个数
int size_unit; // 方格宽高
Map<String, Boolean> closed_list = new HashMap<String, Boolean>();
String[] original_status;
String goal; // 即空格串
MyLink open_list = new MyLink();
AllNodes allNodes; // AllNodes是父类,单纯为了继承其目标状态
public Astar(String[] original_status, String[] target_status, int evaluate_function, int eightOrFifteen) {
allNodes = new AllNodes(target_status);
// 赋值
this.evaluate_function = evaluate_function;
this.eightOrFifteen = eightOrFifteen;
this.original_status = original_status;
this.size = (eightOrFifteen == 0 ? 9 : 16); // 总个数
this.size_unit = (eightOrFifteen == 0 ? 3 : 4); // 每行每列个数
this.goal = new String(" ");
v2time = AstarAlgorithm()*1.0/1e6;
}
public Node getMinfNode() { // 获取OPEN中最小f对应的节点
Node pp = open_list.head;
Node p = pp.next;
Node pres = pp;
Node res = p;
while (p != null) {
if (p.f < res.f) {
res = p;
pres = pp;
}
p = p.next;
pp = pp.next;
}
pres.next = res.next;
closed_list.put(changeStringArrayTo(res.status), true);
return res;
}
public void printPath(Node tnode) { // 显示路径,将路径添加到文本域中
if (v2steps == 0) v2steps = tnode.g;
if (tnode == null) return;
printPath(tnode.parent);
textarea2.append("\n--第 " + tnode.g + " 步--\n");
for (int i = 0; i < size_unit; i++) {
for (int j = 0; j < size_unit; j++) {
if (j == 0) textarea2.append(" ");
textarea2.append("" + tnode.status[i * size_unit + j] + (j == size_unit - 1 ? '\n' : '|'));
}
if (i != size_unit - 1) {
textarea2.append(" ");
for (int j = 0; j < 2 * size_unit - 1; j++)
textarea2.append("-");
textarea2.append("\n");
}
}
}
// 这个函数是为了方便Map的键值不用使用数组,将每个方格中的字符串以三个dollar符隔开,而译码的时候也按照三个dollar符切开即可;
// 之所以选三个dollar符是因为方格最多可以输入两个字符,三个字符作为分隔符最有效
public String changeStringArrayTo(String[] str_arr) {
String res = new String("");
for (int i = 0; i < size; i++) res += str_arr[i] + "$$$";
return res;
}
public long AstarAlgorithm() {
long starttime2 = System.nanoTime();
System.currentTimeMillis();
open_list.addNode(-1, original_status, null, evaluate_function, eightOrFifteen); // 插入起点
int tmp_cnt = 0;
while (open_list.head.next != null) {
Node current_node = getMinfNode();
// for (int i = 0;i < size;i ++) System.out.print(current_node.status[i] + " ");System.out.println(); // 测试输出
if (current_node.evaluate(evaluate_function, eightOrFifteen) == 0) {
long endtime = System.nanoTime();
// 找到最短路
System.out.println(tmp_cnt);
printPath(current_node);
return endtime - starttime2; // 时间
}
tmp_cnt++;
int goal_position = 0; // 获取goal位置
while (!current_node.status[goal_position].equals(goal)) goal_position++;
for (int k = 0; k < 4; k++) { // 四个方向
int x = goal_position / size_unit, y = goal_position % size_unit;
int tx = x + offset[0][k], ty = y + offset[1][k];
if (tx < 0 || ty < 0 || tx >= size_unit || ty >= size_unit) continue;
String[] temp_status = new String[size]; // 拷贝一份
for (int i = 0; i < size; i++) { // 拷贝并实现交换
if (i == x * size_unit + y)
temp_status[i] = new String(current_node.status[tx * size_unit + ty]);
else if (i == tx * size_unit + ty)
temp_status[i] = new String(current_node.status[x * size_unit + y]);
else temp_status[i] = new String(current_node.status[i]);
}
if (closed_list.containsKey(changeStringArrayTo(temp_status))) continue;
Node pp = open_list.head;
Node p = pp.next;
while (p != null) {
if (p.status == temp_status) break;
p = p.next;
pp = pp.next;
}
int h = new Node(temp_status, evaluate_function, eightOrFifteen).evaluate(evaluate_function, eightOrFifteen);
if (p == null || p.f > current_node.g + 1 + h) { // 比较f
open_list.addNode(current_node.g, temp_status, current_node, evaluate_function, eightOrFifteen);
}
}
}
return 0;
}
}
// BFS
class BFS {
final int offset[][] = new int[][]{{-1, 1, 0, 0}, {0, 0, -1, 1}};
public int eightOrFifteen = 0; // 0:8 1:15
public int size; // 方格总个数
public int size_unit; // 方格宽高
public Map<String, String> st = new HashMap<String, String>();
public String[] original_status;
public String target_string;
public String goal = " ";
public Queue<String> q = new LinkedList<String>();
public BFS(String[] original_status, String[] target_status, int eightOrFifteen) {
// 赋值
this.size = (eightOrFifteen == 0 ? 9 : 16);
this.size_unit = (eightOrFifteen == 0 ? 3 : 4);
this.eightOrFifteen = eightOrFifteen;
this.original_status = original_status;
this.target_string = changeStringArrayTo(target_status); // size的赋值要于此之前!
v3time = BFSAlgorithm()*1.0/1e6;
}
public String changeStringArrayTo(String[] str_arr) {
String res = new String("");
for (int i = 0; i < size; i++) res += str_arr[i] + "$$$";
return res;
}
// 解码
public String[] recoveryToStringArray(String str) {
return str.split("\\$\\$\\$");
}
public void printPath(String cur) {
int idx = 0;
Stack<String> stack = new Stack<String>(); // 路径输出使用的是栈
do {
stack.push(cur);
cur = st.get(cur);
} while (cur != null);
while (!stack.empty()) {
String[] s = recoveryToStringArray(stack.peek());
stack.pop();
textarea3.append("\n--第 " + idx + " 步--\n");
for (int i = 0; i < size_unit; i++) {
for (int j = 0; j < size_unit; j++) {
if (j == 0) textarea3.append(" ");
textarea3.append("" + s[i * size_unit + j] + (j == size_unit - 1 ? '\n' : '|'));
}
if (i != size_unit - 1) {
textarea3.append(" ");
for (int j = 0; j < 2 * size_unit - 1; j++)
textarea3.append("-");
textarea3.append("\n");
}
}
idx++;
}
v3steps = idx - 1;
}
public long BFSAlgorithm() {
long starttime = System.nanoTime();
String ori = changeStringArrayTo(original_status);
q.add(ori);
st.put(ori, null);
while (!q.isEmpty()) {
String top = q.peek();
q.poll();
String[] stringArrayOfTop = recoveryToStringArray(top);
if (top.equals(target_string)) {
long endtime = System.nanoTime();
printPath(top);
return endtime - starttime;
}
int goal_position = 0;
while (!stringArrayOfTop[goal_position].equals(goal)) goal_position++;
for (int k = 0; k < 4; k++) {
int x = goal_position / size_unit, y = goal_position % size_unit;
int tx = x + offset[0][k], ty = y + offset[1][k];
if (tx < 0 || ty < 0 || tx >= size_unit || ty >= size_unit) continue;
String[] temp_string = new String[size]; // 拷贝一份
for (int i = 0; i < size; i++) { // 拷贝并实现交换
if (i == x * size_unit + y) temp_string[i] = new String(stringArrayOfTop[tx * size_unit + ty]);
else if (i == tx * size_unit + ty)
temp_string[i] = new String(stringArrayOfTop[x * size_unit + y]);
else temp_string[i] = new String(stringArrayOfTop[i]);
}
String temp = changeStringArrayTo(temp_string);
if (st.containsKey(temp)) continue;
st.put(temp, top);
q.add(temp);
}
}
return 0;
}
}
// GOS
class GOS {
final int offset[][] = new int[][]{{-1, 1, 0, 0}, {0, 0, -1, 1}};
int evaluate_function = 1; // 启发函数1
int eightOrFifteen = 0; // 0:8 1:15
int size; // 方格总个数
int size_unit; // 方格宽高
Map<String, Boolean> closed_list = new HashMap<String, Boolean>();
String[] original_status;
String goal;
MyLink open_list = new MyLink();
AllNodes allNodes;
public GOS(String[] original_status, String[] target_status, int evaluate_function, int eightOrFifteen) {
allNodes = new AllNodes(target_status);
// 赋值
this.evaluate_function = evaluate_function;
this.eightOrFifteen = eightOrFifteen;
this.original_status = original_status;
this.size = (eightOrFifteen == 0 ? 9 : 16);
this.size_unit = (eightOrFifteen == 0 ? 3 : 4);
this.goal = new String(" ");
v1time = GOSAlgorithm()*1.0/1e6;
}
public Node getMinfNode() {
Node pp = open_list.head;
Node p = pp.next;
Node pres = pp;
Node res = p;
while (p != null) {
if (p.f < res.f) {
res = p;
pres = pp;
}
p = p.next;
pp = pp.next;
}
pres.next = res.next;
closed_list.put(changeStringArrayTo(res.status), true);
return res;
}
public void printPath(Node tnode) {
if (v1steps == 0) v1steps = tnode.g;
if (tnode == null) return;
printPath(tnode.parent);
textarea1.append("\n--第 " + tnode.g + " 步--\n");
for (int i = 0; i < size_unit; i++) {
for (int j = 0; j < size_unit; j++) {
if (j == 0) textarea1.append(" ");
textarea1.append("" + tnode.status[i * size_unit + j] + (j == size_unit - 1 ? '\n' : '|'));
}
if (i != size_unit - 1) {
textarea1.append(" ");
for (int j = 0; j < 2 * size_unit - 1; j++)
textarea1.append("-");
textarea1.append("\n");
}
}
}
public String changeStringArrayTo(String[] str_arr) {
String res = new String("");
for (int i = 0; i < size; i++) res += str_arr[i] + "$$$";
return res;
}
public long GOSAlgorithm() {
long starttime = System.nanoTime();
open_list.addNode(-1, original_status, null, evaluate_function, eightOrFifteen); // 插入起点
while (open_list.head.next != null) {
Node current_node = getMinfNode();
// for (int i = 0;i < size;i ++) System.out.print(current_node.status[i] + " ");System.out.println(); // 测试输出
if (current_node.evaluate(evaluate_function, eightOrFifteen) == 0) {
long endtime = System.nanoTime();
// 找到最短路
printPath(current_node);
return endtime - starttime;
}
int goal_position = 0; // 获取goal位置
while (!current_node.status[goal_position].equals(goal)) goal_position++;
for (int k = 0; k < 4; k++) {
int x = goal_position / size_unit, y = goal_position % size_unit;
int tx = x + offset[0][k], ty = y + offset[1][k];
if (tx < 0 || ty < 0 || tx >= size_unit || ty >= size_unit) continue;
String[] temp_status = new String[size]; // 拷贝一份
for (int i = 0; i < size; i++) { // 拷贝并实现交换
if (i == x * size_unit + y)
temp_status[i] = new String(current_node.status[tx * size_unit + ty]);
else if (i == tx * size_unit + ty)
temp_status[i] = new String(current_node.status[x * size_unit + y]);
else temp_status[i] = new String(current_node.status[i]);
}
if (closed_list.containsKey(changeStringArrayTo(temp_status))) continue;
Node pp = open_list.head;
Node p = pp.next;
while (p != null) {
if (p.status == temp_status) break;
p = p.next;
pp = pp.next;
}
int h = new Node(temp_status, evaluate_function, eightOrFifteen).evaluate(evaluate_function, eightOrFifteen);
if (p == null) {
open_list.addNode(current_node.g, temp_status, current_node, evaluate_function, eightOrFifteen);
}
}
}
return 0;
}
}
}
MyLink.java 程序
A*算法和全局择优搜索中使用的自定义链表数据结构,其中包含节点类和计算启发信息的函数。
import javafx.scene.Parent;
/**
* @author LJR
* @create 2021-11-05 15:38
*/
class AllNodes {
public static String[] target_status; // 静态
public AllNodes() {
}
public AllNodes(String[] status) {
target_status = status;
}
}
class Node extends AllNodes {
public int f, g, h;
public String[] status;
public Node next = null; // 链表中的next
public Node parent = null; // 结果路径中的parent的状态
public Node() {
}
public Node(Node node, int eightOrFifteen) {
f = node.f;
g = node.g;
h = node.h;
status = new String[(eightOrFifteen==0?9:16)];
for (int i = 0;i < (eightOrFifteen==0?9:16);i ++) status[i] = new String(node.status[i]);
parent = node.parent;
next = node.next;
}
public Node(String[] s, int option, int eightOrFifteen) {
this.status = s;
this.h = evaluate(option, eightOrFifteen);
this.f = g + this.h;
}
public Node(int g, String[] s, int option, int eightOrFifteen) { // 0:8 1:15
this.status = s;
this.h = evaluate(option, eightOrFifteen);
this.f = g + this.h;
this.g = g;
}
public Node(int g, String[] s, Node parent_node, int option, int eightOrFifteen) { // 0:8 1:15
this(g, s, option, eightOrFifteen);
this.parent = parent_node;
}
// 计算h(n)
public int evaluate(int option, int eightOrFifteen) {
int res = 0;
int size = (eightOrFifteen == 0 ? 9 : 16);
int size_unit = (eightOrFifteen == 0 ? 3 : 4);
if (option == 1) {
// System.out.println("h(n)定义为当前节点与目标节点差异的度量:即当前节点与目标节点格局相比,位置不符的数字个数。");
for (int i = 0; i < size; i++)
res += (status[i].equals(target_status[i]) ? 0 : 1);
} else if (option == 2) {
// System.out.println("h(n)定义为当前节点与目标节点距离的度量:当前节点与目标节点格局相比,位置不符的数字移动到目标节点中对应位置的最短距离之和。");
for (int i = 0; i < size; i ++) {
for (int j = 0;j < size; j ++) {
if (status[i].equals(target_status[j])) {
res += Math.abs((i / size_unit) - (j / size_unit)) + Math.abs((i % size_unit) - (j % size_unit));
break;
}
}
}
}
return res;
}
}
public class MyLink {
public Node head = new Node(-1, null, -1, -1);
public void addNode(int preNodeg, String[] status, Node parent, int option, int eightOrFifteen) {
Node newNode = new Node(preNodeg + 1, status, parent, option, eightOrFifteen); // 实例化一个节点
if (head.next == null) {
head.next = newNode;
return;
}
Node tmp = head.next;
while (tmp.next != null) {
tmp = tmp.next;
}
tmp.next = newNode;
}
}
LimitedLength.java 程序
该程序用于限制用户向方格中输入的字符数不得大于2。
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
public class LimitedLength extends PlainDocument {
private static final long serialVersionUID = 1L;
private int max_length = 2;
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
if (str == null) return ;
if(getLength() + str.length() > max_length) return ;
else super.insertString(offs, str, a);
}
}