求解八数码问题
八数码问题也称为九宫问题。在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,
与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。
所谓问题的一个状态就是棋子在棋盘上的一种摆法。棋子移动后,状态就会发生改变。解八数码问题实际上就是找出从初始状态到达目标状态所经过的一系列中间过渡状态。
import java.awt.*; import java.awt.event.*; import java.awt.font.GlyphVector; import java.awt.image.BufferedImage; import javax.swing.*; /** * 窗口类,实现可视化动画效果 * * @author Administrator * */ public class EightNumberWindow extends JFrame { private static final long serialVersionUID = 1L; private JTextField[] startText = new JTextField[9]; // 开始状态的文本输入框 private JTextField[] goalText = new JTextField[9]; // 目标状态的文本输入框 private JTextField queText = new JTextField(); private DrawPanel drawPanel = new DrawPanel(); private final String[] fang = { "U", "D", "L", "R" }; // 方向 State sta = new State(new int[] { 2, 0, 8, 1, 6, 3, 7, 5, 4 }); State end = new State(new int[] { 1, 2, 3, 8, 0, 4, 7, 6, 5 }); public EightNumberWindow() { this.setTitle("八数码问题(张平)"); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置窗口关闭时退出程序 this.setSize(600, 400); this.setLocationRelativeTo(null); this.setResizable(false); // 固定窗口大小 this.setLayout(new BorderLayout(20, 27)); /* 添加左边组件 */ JPanel left = new JPanel(new BorderLayout()); left.setPreferredSize(new Dimension(180, this.getHeight())); JPanel leftTop = new JPanel(null); Color c = new Color(215, 225, 235); leftTop.setBackground(c); leftTop.setBorder(BorderFactory.createLineBorder(Color.gray)); // 设置边框 JPanel leftBottom = new JPanel(null); leftBottom.setBackground(c); leftBottom.setBorder(BorderFactory.createLineBorder(Color.gray)); // 设置边框 // 添加文本框 for (int i = 0, j = 0; i < startText.length; i++) { if (i != 0 && i % 3 == 0) j++; int x = 20 + (i % 3) * 40 + (i % 3) * 10; int y = 20 + j * 40 + j * 10; startText[i] = new JTextField(2); startText[i].setBounds(x, y, 40, 25); leftTop.add(startText[i]); } // 添加文本框 for (int i = 0, j = 0; i < goalText.length; i++) { if (i != 0 && i % 3 == 0) j++; int x = 20 + (i % 3) * 40 + (i % 3) * 10; int y = 20 + j * 40 + j * 10; goalText[i] = new JTextField(2); goalText[i].setBounds(x, y, 40, 25); leftBottom.add(goalText[i]); } JPanel leftCenter = new JPanel(new GridLayout(2, 1, 8, 8)); leftCenter.add(leftTop); leftCenter.add(leftBottom); left.add(new JLabel("开始状态", JLabel.CENTER), BorderLayout.NORTH); left.add(new JLabel("目标状态", JLabel.CENTER), BorderLayout.SOUTH); left.add(leftCenter); this.add(left, BorderLayout.WEST); /* 添加右边(中间)组件 */ JPanel right = new JPanel(new BorderLayout(10, 10)); drawPanel.setBackground(Color.pink); right.add(drawPanel, BorderLayout.CENTER); JPanel rightBottom = new JPanel(new BorderLayout()); rightBottom.add(new JLabel("问题解:"), BorderLayout.WEST); rightBottom.add(queText, BorderLayout.CENTER); final JButton calBtn = new JButton("计算步骤"); JButton drawBtn = new JButton("动画演示"); JPanel btnPanel = new JPanel(); btnPanel.add(calBtn); btnPanel.add(drawBtn); rightBottom.add(btnPanel, BorderLayout.EAST); right.add(rightBottom, BorderLayout.SOUTH); this.add(right, BorderLayout.CENTER); calBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { EightNumber eightNumber = new EightNumber(); // 从文本输入框获取八数码值 for (int i = 0; i < sta.state.length; i++) { sta.state[i] = Integer.parseInt(startText[i].getText() .trim()); end.state[i] = Integer.parseInt(goalText[i].getText() .trim()); } // 检查八数码是否符合 if (!isStateNumber(sta.state)) { JOptionPane.showMessageDialog(EightNumberWindow.this, "“开始状态”的八数码包含错误,请重新输入!"); return; } if (!isStateNumber(end.state)) { JOptionPane.showMessageDialog(EightNumberWindow.this, "“目标状态”的八数码包含错误,请重新输入!"); return; } String path = eightNumber.getEightNumber(sta, end); System.out.println("计算得的八数码其解为:" + path); if (path != null) { queText.setText(path + " 共" + path.length() + "步"); } else { queText.setText("未能找到解决路径"); } } }); drawBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String str = queText.getText().trim(); // 获取路径文本框中的文字 // 筛选可用路径方向标识符 String path = ""; for (int i = 0; str != null && i < str.length(); i++) { char c = str.charAt(i); if (isExistForArray(fang, c + "")) { path += c; } } if (path.isEmpty()) { JOptionPane.showMessageDialog(EightNumberWindow.this, "没有任何可用路径,请先单击“" + calBtn.getText() + "”按钮!"); if (drawPanel.isDrawing()) drawPanel.stopDrawing(); return; } drawPanel.startDraw(path, sta.state); } /** * 判断字符串是否是指定字符数组的值 * * @param arr * @param str * @return */ private boolean isExistForArray(String[] arr, String str) { for (int i = 0; i < arr.length; i++) { if (arr[i].equals(str)) return true; } return false; } }); initDate(); } /** * 初始化数据 */ private void initDate() { for (int i = 0; i < startText.length; i++) { startText[i].setText(sta.state[i] + ""); } for (int i = 0; i < goalText.length; i++) { goalText[i].setText(end.state[i] + ""); } } /** * 检查八数码数字是否正确 * * @param state * 八数码状态int型数组,length=9 * @return */ private boolean isStateNumber(int[] state) { if (state == null || state.length != 9) return false; boolean[] flag = new boolean[state.length]; for (int i = 0; i < state.length; i++) { if (state[i] < 0 || state[i] >= state.length) return false; if (flag[state[i]]) return false; flag[state[i]] = true; } return true; } public static void main(String[] args) { new EightNumberWindow().setVisible(true); } /** * 动画类 * */ private final class DrawPanel extends JPanel implements Runnable { private static final long serialVersionUID = 1L; private BufferedImage image; private boolean isDrawing = false; private Block[] blocks = new Block[9]; private Font font = new Font("宋体", Font.PLAIN, 34); private String path = ""; /** * 构造方法 */ public DrawPanel() { // 绘制初始的八数码 System.out.print("初始 => "); initBlocks(null); draw(null); } @Override public void paint(Graphics g) { g.setColor(Color.LIGHT_GRAY); g.fillRect(0, 0, getWidth(), getHeight()); g.drawImage(image, 0, 0, null); } /** * 开始绘制动画 * * @param path * @param state */ public void startDraw(String path, int[] state) { if (isDrawing()) { stopDrawing(); return; } if (path == null || path.trim().equals("")) return; this.path = path; System.out.println("动画路径方向:" + this.path); initBlocks(state); new Thread(this).start(); } /** * 初始化blocks数组 */ private void initBlocks(int[] state) { if (state == null || state.length != 9) { // 如果传入的数组为空或者不符合 state = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; } for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int index = j * 3 + i; int x = 70 + (i % 3) * 80 + (i % 3) * 10; int y = 30 + j * 80 + j * 10; int value = index; if (index < state.length) value = state[index]; Color c = Color.red; if (value == 0) c = Color.BLUE; blocks[index] = new Block(value, x, y, c); } } } /** * 绘制动画 */ public boolean draw(String msg) { if (image == null) { int w = this.getWidth(); int h = this.getHeight(); // 判断宽高,避免初始化时getWidth()和getHeight()的值是0 if (w < 10) w = 394; if (h < 10) h = 324; image = new BufferedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR); } Graphics g = image.getGraphics(); g.setColor(Color.LIGHT_GRAY); g.fillRect(0, 0, getWidth(), getHeight()); g.setFont(font); for (int i = 0; i < blocks.length; i++) { Block block = blocks[i]; g.setColor(block.getBackColor()); g.fillRect(block.getX(), block.getY(), 80, 80); g.setColor(Color.WHITE); g.drawString("" + block.getValue(), block.getX() + 32, block.getY() + 50); } // 绘制消息字符串 if (msg != null) { Font msgFont = new Font("华文行楷", Font.BOLD, 56); // 实现文字描边 GlyphVector v = msgFont.createGlyphVector( getFontMetrics(msgFont).getFontRenderContext(), msg); Shape shape = v.getOutline(); Rectangle bounds = shape.getBounds(); Graphics2D g2d = (Graphics2D) g; g2d.translate((getWidth() - bounds.width) / 2 - bounds.x, (getHeight() - bounds.height) / 2 - bounds.y); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setColor(Color.WHITE); g2d.fill(shape); g2d.setColor(Color.yellow.darker().darker()); g2d.setStroke(new BasicStroke(2)); g2d.draw(shape); } System.out.print("blocks = "); for (int i = 0; i < blocks.length; i++) { System.out.print(blocks[i].getValue() + " "); } System.out.println("\n"); // 两个换行 repaint(); return true; } /** * 下一个状态 * * @param fx */ private void nextState(String fx) { if (fx == null || fx.length() != 1) return; int zero = 0; for (; zero < blocks.length; zero++) { if (blocks[zero].getValue() == 0) break; } int x = zero / 3;// 获取行号 int y = zero % 3; // 获取列号 if (fx.equals(fang[0])) { // 上 if (x < 1) return; Block block1 = blocks[x * 3 + y]; Block block2 = blocks[(x - 1) * 3 + y]; // 交换 int value = block1.getValue(); Color color = block1.getBackColor(); block1.setValue(block2.getValue()); block1.setBackColor(block2.getBackColor()); block2.setValue(value); block2.setBackColor(color); } else if (fx.equals(fang[1])) { // 下 if (x >= 2) return; Block block1 = blocks[x * 3 + y]; Block block2 = blocks[(x + 1) * 3 + y]; // 交换 int value = block1.getValue(); Color color = block1.getBackColor(); block1.setValue(block2.getValue()); block1.setBackColor(block2.getBackColor()); block2.setValue(value); block2.setBackColor(color); } else if (fx.equals(fang[2])) { // 左 if (y < 1) return; Block block1 = blocks[x * 3 + y]; Block block2 = blocks[x * 3 + (y - 1)]; // 交换 int value = block1.getValue(); Color color = block1.getBackColor(); block1.setValue(block2.getValue()); block1.setBackColor(block2.getBackColor()); block2.setValue(value); block2.setBackColor(color); } else if (fx.equals(fang[3])) { // 右 if (y >= 2) return; Block block1 = blocks[x * 3 + y]; Block block2 = blocks[x * 3 + (y + 1)]; // 交换 int value = block1.getValue(); Color color = block1.getBackColor(); block1.setValue(block2.getValue()); block1.setBackColor(block2.getBackColor()); block2.setValue(value); block2.setBackColor(color); } else { } } @Override public void run() { isDrawing = true; // 标记动画开始 String firstMsg = "动画即将开始"; // 首次提示语 draw(firstMsg); System.out.println(firstMsg); firstMsg = null; try { int count = 30; while (isDrawing && count-- > 0) Thread.sleep(100); // 延长消息显示的时间 } catch (InterruptedException e) { e.printStackTrace(); } boolean r = true; int s = 0; image = null; // 置图片为空,使其能够重新适应Panel while (isDrawing && r) { r = draw(firstMsg); if (s >= path.length()) break; String fx = path.charAt(s) + ""; // 获取一个字符 nextState(fx); System.out.println("s=" + s + "\t当前方向标记值:" + fx); s++; try { int count = 10; while (isDrawing && count-- > 0) Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } isDrawing = false; System.out.println("动画结束!"); } /** * 是否正在进行动画 * * @return */ public boolean isDrawing() { return isDrawing; } /** * 停止动画 * * @return */ public void stopDrawing() { isDrawing = false; try { Thread.sleep(300); // 等待动画线程结束 } catch (InterruptedException e) { e.printStackTrace(); } draw(null); } } /** * 方块类 * */ class Block { private int value = -1; private int x = 0; private int y = 0; private Color backColor = Color.WHITE; public Block(int value, int x, int y, Color backColor) { this.value = value; this.x = x; this.y = y; this.backColor = backColor; } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public Color getBackColor() { return backColor; } public void setBackColor(Color backColor) { this.backColor = backColor; } } } /** * 八数码问题求解类 */ import java.util.Arrays; public class EightNumber { public final int MAX = 1000000; // 状态数组 private State[] states = new State[MAX]; // 目标状态 private State goal; // 距离数组 private int[] dist = new int[MAX]; // 方向移动数组 private int[] fangX = { -1, 1, 0, 0 }; private int[] fangY = { 0, 0, -1, 1 }; private final String[] fang = { "U", "D", "L", "R" }; private int[] fa = new int[MAX]; private int[] move = new int[MAX]; private int front = 1; // 当前下标 private int rear = 2; private boolean[] flags = new boolean[MAX]; private int[] fact = new int[9]; /** * 八数码问题bfs查找 * * @return */ private int bfsSearch() { initFact(); flags = new boolean[MAX]; // 初始化 while (front < rear) { State s = states[front]; if (Arrays.equals(s.state, goal.state)) {// 如果当前状态跟目标状态相同 return front;// 返回当前指针 } int zero; for (zero = 0; zero < 9; zero++) { // 找出当前状态0的位置 if (0 == s.state[zero]) { break; } } int x = zero / 3;// 获取行号 int y = zero % 3; // 获取列号 // 各个方向尝试 for (int d = 0; d < 4; d++) { // System.out.println("进入方位循环"); int newx = x + fangX[d];// 移动目标的横坐标 int newy = y + fangY[d];// 移动目标的纵坐标 int newz = newx * 3 + newy;// 移动目标在下一状态的位置 if (newx >= 0 && newx < 3 && newy >= 0 && newy < 3) {// 如果移动合法 // System.out.println("移动合法" + newx + "||" + newy); int[] temp = Arrays.copyOf(s.state, 9); states[rear] = new State(temp); states[rear].state[newz] = s.state[zero]; states[rear].state[zero] = s.state[newz]; dist[rear] = dist[front] + 1; // System.out.println("在第" + dist[rear] + "层"); if (insert(states[rear])) { fa[rear] = front; move[rear] = d; rear++; } } else { // System.out.println("移动不合法" + newx + "||" + newy); } } front++; } System.out.println("end"); return 0; } /** * 尝试插入状态值 * * @param state * @return true 成功,false 失败 */ private boolean insert(State state) { int code = 0; for (int i = 0; i < 9; i++) { int count = 0; for (int j = i + 1; j < 9; j++) { if (state.state[j] < state.state[i]) count++; } code += fact[8 - i] * count; } if (flags[code]) { // 是否已标记 return false; } else { flags[code] = true; return true; } } /** * 初始化fact数组 */ private void initFact() { fact[0] = 1; for (int i = 1; i < 9; i++) fact[i] = fact[i - 1] * i; } /** * 获得解决的路径方向字符串 * * @param cur * @return */ private String getPath(int cur) { StringBuffer path = new StringBuffer(); while (cur != 1) { path.insert(0, fang[move[cur]]); cur = fa[cur]; } return path.toString(); } /** * 获得八数码问题的解 * * @param start * 开始状态 * @param end * 目标结束状态 * @return 代表移动方向的UDLR字符串 */ public String getEightNumber(State start, State end) { dist[1] = 0; // 初始状态 states[1] = start; // 目标状态 this.goal = end; int ans = bfsSearch(); if (ans <= 0) { return null; } else { return getPath(ans); } } public static void main(String[] args) { // 测试 EightNumber eig = new EightNumber(); State start = new State(new int[] { 2, 0, 8, 1, 6, 3, 7, 5, 4 }); State end = new State(new int[] { 1, 2, 3, 8, 0, 4, 7, 6, 5 }); System.out.println("解决路径:" + eig.getEightNumber(start, end)); } } /** * 状态类,其每个对象都保存一个状态 */ class State { int[] state; // 长度应为9 public State(int[] state) { this.state = state; } }