1、前言
这篇博文,我不仅解决了八皇后问题(自己的见解),同时我还实现了用回溯法动态搜索的八皇后问题路径的GUI展示。这是全网独家的教程,我也相信这个教程(输出的结果)能对要入门该算法的同学有一个非常直观的学习体验。其中,八皇后问题的搜索算法我使用了经典的回溯法。
申明:无论是从八皇后算法到整个GUI框架的搭建,都由我独立完成。如果对你有帮助,希望能点赞、收藏加关注(手动doge.png)~
2、什么是八皇后问题
2.1 问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在
8
×
8
8 \times 8
8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
那什么是回溯法呢?我参考了维基百科的解释:
回溯法(backtracking)是暴力搜索算法的一种。它采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案
- 在尝试了所有可能的分步方法后宣告该问题没有答案
2.2 直观的解释
下面我通过java注释的形式展示利用回溯法的思想,解决八皇后问题的第一次搜索:
其中,-代表这个位置可以放棋子,*代表该位置不能放棋子,而+代表这个位置已经放下棋子了。我用这三个符号代表一个
8
×
8
8 \times 8
8×8的棋盘,每个位置都可以用坐标表示。
/*
* x为横坐标,y为纵坐标 (0,0) (2,1) (4,2) (1,3)
* 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
* 0 - - - - - - - - + * * * * * * * + * * * * * * * + * * * * * * * + * * * * * * *
* 1 - - - - - - - - * * - - - - - - * * + * * * * * * * + * * * * * * * + * * * * *
* 2 - - - - - - - - * - * - - - - - * * * * - - - - * * * * + * * * * * * * + * * *
* 3 - - - - - - - - * - - * - - - - * - * * * - - - * - * * * * - - * + * * * * * *
* 4 - - - - - - - - * - - - * - - - * - * - * * - - * - * - * * * - * * * - * * - -
* 5 - - - - - - - - * - - - - * - - * - * - - * * - * * * - * * * * * * * * * * * -
* 6 - - - - - - - - * - - - - - * - * - * - - - * * * - * - * - * * * * * - * - * *
* 7 - - - - - - - -(7,7) , * - - - - - - * , * - * - - - - * , * - * - * - - * ,* * * - * * - *,
* (3,4) (7,5)
* 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
* 0 + * * * * * * * + * * * * * * *
* 1 * * + * * * * * * * + * * * * *
* 2 * * * * + * * * * * * * + * * *
* 3 * + * * * * * * * + * * * * * *
* 4 * * * + * * * * * * * + * * * *
* 5 * * * * * * * - * * * * * * * +
* 6 * * * * * * * * * * * * * * * *
* 7 * * * * * * * * , * * * * * * * *
*/
我自己总结的回溯法的搜索策略就是:从左到右不回头,从上往下走,找不到就从下往上回头往右找,这样扫荡一遍,就不会遗漏棋盘上的任何一种可能出现的位置。回溯法这种算法思想用代码实现需要结合递归的操作。
就上面的例子来说,显然最后一个皇后的落子为 (7,5) ,由于此时无子可落(棋盘上没有-了)且皇后数不足八个,要进入的下一层递归直接找到出口。所以我们需要回溯,通过return;
首先回到(7,5)的帧栈,执行pollLast语句,取出 (7,5) 在链表中的位置。这里要注意一下,如果(7,5)的右边还有符号-,也就是可以放置棋子的地方,比方说(7,6)。此时这个位置的棋子就要被put进链表中(用for循环遍历该行)。接着,虚拟棋盘应该变为:
/*
* 取出(7,5)
* 0 1 2 3 4 5 6 7
* 0 + * * * * * * *
* 1 * * + * * * * *
* 2 * * * * + * * *
* 3 * + * * * * * *
* 4 * * * + * * * *
* 5 * * * * * * * -
* 6 * * * * * * * *
* 7 * * * * * * * *
*/
接上文,也就是上图的第6个棋盘;同理,继续执行回退帧栈的操作,取出**(3,4)**,虚拟棋盘变为上图第5个,通过for循环在第四行继续按列扫荡,我们找到另外一个空位(6,4),以此类推。
3、解决八皇后问题
3.1 代码
我在代码中做出了非常详细的解释,这也是我一个字一个字打出来的切身体会。
回溯法的巧妙之处在于,我们不用先定义
8
×
8
8\times8
8×8的棋盘,而是通过for循环对每一列的每一行或者每一行的每一列进行判断(每个皇后的位置一定是每一行有且只有一个,并且是每一列有且只有一个)。这里我统一对棋盘的每一行的每一列进行遍历。也就是说,这个棋盘是虚拟的,我们通过逐行搜索来记录符合位置规范的落子。所以我们要记录下我们所摆放的棋子路径。(更具体的解释详见下面的代码与解释)
我定义了EightQueen类实现八皇后的回溯法,还包含了一个静态的Location类。下面每个函数的功能,我都做了注释。[这个EightQueen类是独立的,与动态展示部分没有直接关系]
package EightQueen;
import java.util.LinkedList;
/**八皇后问题**/
public class EightQueen{
private static final int SIZE = 8; //皇后的个数,此处设为8,表示8个皇后
private static int count = 0; //记录摆放的方式数
public static void main(String[] args) {
LinkedList<Location> list = new LinkedList<Location>();
eightQueen(list, 0, 0); //从棋盘的第0行第0列开始
System.out.println("八皇后共有 " + count + "种摆放方式");
}
/**
* 定义位置的类,用于表示皇后的摆放位置
**/
static class Location {
int x ; //对应棋盘的列
int y ; //对应棋盘的行
Location(int x, int y){
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
/**
* 主要函数,使用回溯法。
**/
private static void eightQueen(LinkedList<Location> list, int x, int y) {
if(list.size() == SIZE){ //当list元素个数为8时,表示8个皇后都摆放完毕
printLocation(list); //打印皇后摆放方式
return ;//当八个皇后都摆放完毕,我们使用return可以返回之前的栈空间执行剩下的语句
}
for(int i = x ; i < SIZE ; i++){
Location loc = new Location(i, y);
if(isLegalLoc(list, loc)){
list.offer(loc); //将第y行的皇后摆放好
eightQueen(list, 0, y+1); //开始摆放y+1行的皇后,同样从第0列开始摆放
//这里我们用到栈的数据结构来实现回溯法,递归到底部然后回溯到保存之前信息的栈空间,然后执行pollLast的语句
list.pollLast(); //假如总共排放的皇后不能达到8个的话,都要将其撤回,再试探其它的摆法。这就是回溯法。
}//pollLast方法:Retrieves and removes the last element of this list,or returns null if this list is empty.
}
//假如有一行的每一列都放不下,那么for循环结束后自动返回之前的栈空间;递归中每一层,也就是每次进入一个方法体后我们都会创建一个新的栈空间
/*这个程序的回溯法巧妙在,每一行中可能有n列是符合条件的,但是如果最终的皇后数不到8的话,我们需要回溯,执行pollLast语句,然后继续之前的for循环,
*寻找该行中其余复合放置条件的皇后,然后依次类推,一旦我们完成了八个皇后的放置的时候,此时一定是在第八行,我们继续执行到for循环结束,然后回到上一个递归调用方法
*的帧栈继续执行for循环;直到回到(1,0)从头开始执行我们的函数,最终我们的虚拟棋盘会变为空
*/
}
/**
* 判断位置为loc的皇后是否合法
**/
private static boolean isLegalLoc(LinkedList<Location> list, Location loc) {
for(Location each : list){
if(loc.x == each.x || loc.y == each.y) //判断是否在同一行或同一列
return false;
else if (Math.abs(loc.x - each.x) == Math.abs(loc.y - each.y)) //判断是否在同一对角线或反对角线上
return false;
}
return true;
}
/**
* 打印皇后摆放方式
* @param list
*/
private static void printLocation(LinkedList<Location> list) {
for(Location each : list){
System.out.print(each.toString() + "\t");
}
System.out.println();
count ++;
}
}
3.2 控制台运行结果
4、动态展示部分的Java GUI 代码
以下代码关键的地方我都做了注释,比较需要注意的就是Listener类。
4.1 主界面——JFrame类
MainFrame类(继承自JFrame)调用Jpanel类。
package EightQueen;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class MainFrame extends JFrame {
int windowWidth;
int windowHeight;
int n=0;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MainFrame frame = new MainFrame();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the frame.
*/
public MainFrame() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(600, 200, 1200, 649);
this.setTitle("八皇后问题的动态表示");
windowWidth = this.getWidth(); // 获得JFrame宽
windowHeight = this.getHeight(); // 获得JFrame高
Jpanel contentPane = new Jpanel();
add(contentPane);
this.setVisible(true);
}
}
4.2 面板——JPanel类
Jpanel类(继承JPanel类)调用Listenr类。
package EightQueen;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
import java.awt.Font;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public class Jpanel extends JPanel {
/**
* Create the panel.
*/
int w = 600;
int h = 600;
static int x[] = new int[8];
static int y[] = new int[8];
JScrollPane scrollPane;
JTextArea textArea;
private ImageIcon icon = new ImageIcon("src/EightQueen/queen.png");
public Jpanel() {
setSize(1158,600);
setLayout(null);
JLabel lblNewLabel = new JLabel("\u516B\u7687\u540E\u95EE\u9898\u52A8\u6001\u5C55\u793A");
lblNewLabel.setBounds(826, 13, 190, 52);
lblNewLabel.setFont(new Font("宋体", Font.PLAIN, 20));
add(lblNewLabel);
JButton btnNewButton_1 = new JButton("动态展示");
btnNewButton_1.setBounds(826, 558, 168, 29);
btnNewButton_1.setFont(new Font("宋体", Font.PLAIN, 18));
add(btnNewButton_1);
btnNewButton_1.addMouseListener(new Listener(this));
Font x = new Font("Serif",0,20);
textArea = new JTextArea();
textArea.setFont(x);
scrollPane = new JScrollPane(textArea);
scrollPane.setBounds(676, 52, 482, 493);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setViewportView(textArea);
textArea.setLineWrap(true);
add(scrollPane);
}
@Override
public void paint(Graphics g) {
try {
super.paintComponent(g);
super.paint(g);
// 设置线条颜色(RED为红色)
g.setColor(Color.black);
//绘制水平8个,垂直8个方格。 同时内部64个方格填写数字。
for (int i = 1; i <= 8; i++) {
// 绘制第i条水平线
g.drawLine(0, (w / 8) * i, w, (w / 8) * i);
// 绘制第i条竖直线
g.drawLine((h / 8) * i, 0 , (h / 8) * i, h );
//记录进入数组
x[i-1] = (w / 8) * (i-1)+14;
y[i-1] = (h / 8) * (i-1)+14;
//icon.paintIcon(this, paint, Jpanel.x[i-1], Jpanel.y[i-1]);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3 鼠标监听器——MouseListener类
Listener类(继承MouseListener类),实现回溯算法解决八皇后问题,并监听鼠标点击事件。在Jpanel类中通过btnNewButton_1.addMouseListener(new Listener(this));
与该类联系。
package EightQueen;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.LinkedList;
import javax.swing.ImageIcon;
import javax.swing.JButton;
public class Listener implements MouseListener {
private Jpanel Panel;//
int height;
private Graphics g;
private static ImageIcon icon = new ImageIcon("src/EightQueen/queen.png");
private Location temp;
private static final int SIZE = 8; // 皇后的个数,此处设为8,表示8个皇后
private static int count = 0; // 记录摆放的方式数
static LinkedList<Location> list = new LinkedList<Location>();
public Listener(Jpanel P) {
// TODO Auto-generated constructor stub
Panel = P;
}
public Listener() {
}
static class Location {
int x; // 对应棋盘的列
int y; // 对应棋盘的行
Location(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
/**
* 主要函数,用回溯法。
*/
private void eightQueen(LinkedList<Location> list, int x, int y) throws Exception {
if (list.size() == SIZE) { // 当list元素个数为8时,表示8个皇后都摆放完毕
printLocation(list); // 打印皇后摆放方式
return;// 当八个皇后都摆放完毕,我们使用return可以返回之前的栈空间执行剩下的语句
}
for (int i = x; i < SIZE; i++) {
Location loc = new Location(i, y);
if (isLegalLoc(loc)) {
list.offer(loc); // 将第y行的皇后摆放好
icon.paintIcon(Panel, g, Jpanel.x[loc.x], Jpanel.y[loc.y]);
Thread.sleep(15);// 直接抛出异常
eightQueen(list, 0, y + 1); // 开始摆放y+1行的皇后,同样从第0列开始摆放
// 这里我们用到栈的数据结构来实现回溯法,递归到底部然后回溯到保存之前信息的栈空间,然后执行pollLast的语句
temp = list.pollLast(); // 假如总共排放的皇后不能达到8个的话,都要将其撤回,再试探其它的摆法。这就是回溯法
g.clearRect(Jpanel.x[temp.x], Jpanel.y[temp.y], icon.getIconWidth(), icon.getIconHeight());
Thread.sleep(15);
}
}
}
/**
* 判断位置为loc的皇后是否合法
*/
private boolean isLegalLoc(Location loc) {
for (Location each : list) {
if (loc.x == each.x || loc.y == each.y) // 判断是否在同一行或同一列
return false;
else if (Math.abs(loc.x - each.x) == Math.abs(loc.y - each.y)) // 判断是否在同一对角线或反对角线上
return false;
}
return true;
}
private void printLocation(LinkedList<Location> list) throws InterruptedException {
count++;
StringBuilder str = new StringBuilder("路径" + count + ":");
for (Location each : list) {
System.out.print(each.toString() + "\t");
str.append(each.toString());
}
Panel.textArea.append(str.toString() + "\r\n");
//Panel.textArea.setCaretPosition(Panel.textArea.getText().length());
Panel.textArea.paintImmediately(Panel.textArea.getX(), Panel.textArea.getY(), Panel.textArea.getWidth(),
Panel.textArea.getHeight());
Panel.textArea.setCaretPosition(Panel.textArea.getDocument().getLength());
Panel.invalidate();
System.out.println();
}
@Override
public void mouseClicked(MouseEvent e) {
// TODO Auto-generated method stub
JButton btn = (JButton) (e.getSource());
if (btn.getActionCommand().trim().equals("动态展示")) {
try {
g = Panel.getGraphics();
eightQueen(list, 0, 0);
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
}
4.4 queen.png
![](https://i-blog.csdnimg.cn/blog_migrate/e8aa1b2a8bd9ab1804dbc341320bfbb4.png)
4.5 代码结构
![](https://i-blog.csdnimg.cn/blog_migrate/d6fb0f55d2c92e14da2f2a21a2aeff2d.png)
5、GUI部分代码解释
5.1 搜索算法模型[再强调一次]
在八皇后问题中我用到栈的数据结构来实现回溯法,递归到底部然后回溯到保存之前信息的栈空间,然后执行pollLast的语句,取出棋子。假如有一行的每一列都放不下符合规范的棋子,那么for循环结束后就会自动返回之前的栈空间;递归中每一层,也就是每次进入一个方法体后我都会创建一个新的栈空间。
这个程序的回溯法巧妙在:每一行中可能有n列是符合条件的,但是如果最终的皇后数不到8的话,我们需要回溯,执行pollLast语句,然后继续之前栈空间的for循环,然后依次类推。一旦我们完成了八个皇后的放置的时候,此时一定是在第八行,我们继续执行for循环直到结束,然后回到上一个递归调用方法的帧栈继续执行for循环;直到棋子的所有摆放情况都已经遍历完了,最终我们的虚拟棋盘会变为空。
5.2 函数功能
5.2.1 初始化绘制8×8网格的函数
这个函数调用了Graphics类,根据我在原界面的设计的长宽,画出8条等间距的竖线和8条等间距的横线。
5.2.2 回溯法主要实现函数eightQueen
这个函数的初始化输入是一个初始化的空链表;x=0,y=0分别代表了棋盘的第一个位置。这个函数用到了递归,利用一个for循环对每一行遍历。一旦在该行落子,马上进入下一个递归并在下一行的第0列开始继续搜索合适的落子点并准备进行绘制。一旦从这个函数上一个状态的堆栈退出来,马上取出不合适的落子并在棋盘上清除。
5.2.3 判断落子点是否合法函数isLegalLoc
这个函数的输入是一个Location类,该类是Listener类的内部类。通过x,y判断该落子点和存在链表中的“棋子”是否在行、列或者斜线上有冲突。
如果有冲突返回false,反之返回true。
![](https://i-blog.csdnimg.cn/blog_migrate/3287ff35fb8bfb7f7438bbe9c7269167.png)
5.2.4 鼠标监听函数mouseCliscked(MouseEvent e)
这个函数调用了MouseListener接口,当点击“动态展示”按钮的时候调用方法5.2.2。
5.2.5 打印落子路径的函数printLocation
在这个函数中我对textArea进行了操作,利用append添加每一条路径。我利用textArea.setCaretPosition(Panel.textArea.getDocument().getLength())函数让光标始终定位在textArea的最后一行。并使用paintImmediately函数动态地刷新文本域。
![](https://i-blog.csdnimg.cn/blog_migrate/419d35210bc333ce5ff842eae751916f.png)
6、动态展示结果
6.1 八皇后问题运行结束界面
由于八皇后动态运行结束,所以左边的棋盘为空,右边的文本域展示的是回溯法搜索得到的全部92条路径。
6.2 八皇后问题运行中的界面
此时左边的棋盘打印出了六个棋子,此时回溯法在寻找合适的路径6。
此时左边的棋盘打印出了四个棋子,此时在寻找合适的路径13。
6.3 整体走一遍吧!
在Listener类中可以通过修改eightQueen函数中的Thread.sleep(15);
而修改皇后图标显示的速度。
![](https://i-blog.csdnimg.cn/blog_migrate/e45e0c23bb18266e3ede5abf2d0e8210.gif)
7、存在的小问题
- 代码跑起来后,GUI面板右侧文本域打印出的路径超过17个之后,文本框滚动条没办法往下滑动。运行完,输出92条路径之后才能滑动。
- 没有暂停按钮。
以上问题影响不大,博主随缘更新。
8、创新点
- 回溯法解决八皇后问题的过程中,并不需要记录整棵“搜索树”,而只需记录从初始状态到当前状态的一条搜索路径,是“线性链状”的,其最大优点是占用空间少。
- 利用WindowBuider进行八皇后问题的GUI动态演示。利用鼠标监听器MouseListener监听按钮的动作,并用Graphics类进行绘制GUI界面。
- 在进行八皇后问题求解的时候,我使用了链表进行操作。构造了Location类对链表进行操作,在该类中有棋子的x和y坐标的属性,并重写了toString(该方法在testArea中打印一个合适的被放置的棋子的具体位置)。当有合适棋子放置的时候调用offer方法,要移除棋子的时候调用pollLast方法。
- 使用ImageIcon 加载棋子 的图片,并调用该类的paintIcon方法绘制图片,调用Graphics类的clearRect方法清除图片。
9、结语
本篇博文的教程应该是八皇后问题的集大成之作了,我抽空将大二95分的人工智能导论的报告之一整理成了博文。有问题欢迎评论,我会虚心接受。希望大家能多多点赞、关注、收藏,你的支持是对我持续创作的帮助!
严正申明:转载请联系博主,不允许盗用!