一、什么是回溯算法?
我们肯定都玩过迷宫游戏吧,比较复杂的迷宫,肯定是不可能第一遍就直接过了,只能一步一步地进行尝试。当走到一个死胡同时,只能退回到上一个分岔口进行重新选择。
数独游戏也是这样的,对于一个不确定的方格,我们就会先将这个方格可能出现的问题记录下来,一个一个地尝试,直到得到正确解。有着“通用解”称呼
所以,回溯算法就是类似于枚举的算法,将这一步的所以可能性一个一个地进行尝试。上边迷宫中的分岔口和数独中的可能出现多个数字的方格就是“回溯点”
二、有什么优缺点?
- 因为要对每一个点的可能情况都进行枚举测试,所以效率特别低,比如后边下边例子中的“八皇后问题”中,总共要进行15000次左右的运算,虽然对于计算机来说是很快的,但是更加复杂的问题,可能会更多
- 它可是有着“通用解”称呼,基本上大多数的这类问题都可以用此方法解决。
三、经典的“八皇后”问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。
使用Java来解决此问题主要有以下步骤:
- 设计一个数据模型来解决此问题,
private int result[] = new int[8];
首先想到的就是一个二维数组,但是我在学习的时候,看到老师用了一个长度为8的一维数组就解决了,思想如下:
- 使用int[8]来表示一个正确解,也就是说,这个数组中的8个数组分别表示第一行、第二行、第三行......直到第八行中正确的棋子的位置,比如其中一个解如下图,它所表示的结果就是:{1,5,8,6,3,7,2,4}
- 找到“回溯点”
- 第一种回溯情况:
这个问题的解决方案就是从每一行的每一个点开始摆放棋子,如果这个棋子可以话这里,就继续放置下一行。如果不能放置在这里,就往后移,直到这一行的8个位置全都不能的话,就往上回溯一行,将上一行的棋子往右移一行,再继续放置这一行,如果上一行全部放了个遍还是不行,就再往上回溯一行,就这样继续。。。 - 第二种回溯情况:
因为要找到所有的问题,第二个回溯情况就是当一种摆法摆放完成后。从最后一行进行回溯,比如上边的解中{1,5,8,6,3,7,2,4},当最后一个4摆放完成后,就将最后一个放在5这个位置上,不行的话就放6,7,8,如果全都不行,就将上一行中的2放在3位置上,再从第八行的1,2,3...这样放
- 写出这个判断这个回溯点的条件方法
/**
* 判断这一行中的棋子摆放的是否可用
* 主要就是判断从第一行到第n-1行的这几个皇后会不会和第n行的皇后相互攻击
* 因为之前的n-1行之间之前就已经判断了的,如果它们之前不满足条件就不会到第n行的
*
* @param n 传入的这一行
* @return
*/
private boolean check(int n) {
timeCount++;
for (int i = 0; i < n; i++) {
/*
第一个条件就是判断它们是不是在同一行
这里主要想一个如何判断它们是不是在同一斜行上,
因为我们的模型就是使用数组的角标来表示它当前在第i+1行,用元素表示它在这一行的第i+1个位置上
*/
if (result[i] == result[n] || Math.abs(n - i) == Math.abs(result[n] - result[i])) {
return false;
}
}
return true;
}
4.开始写放置棋子的方法
/**
* 最关键的问题,使用递归来进行回溯算法
* 我们的思路就是一行一行的放置
*
* @param row 在第几行摆放棋子
*/
public void calResult(int row) {
if (row > 7) {
//此时就说明第8行已经摆完了,且是正确的
sum++;
printResult();
return;
}
//这个for循环就是在这一行从第1个开始放,一直放到第max个,
for (int i = 0; i < max; i++) {
result[row] = i;
if (check(row)) {
//说明这一行检验是通过的
calResult(row+1);
}
//如果这个位置不满足,就会自动执行下一次循环,也就是i++
//如果这一行全部试完之后,还是不合格,就会自动放到回溯到上一行去
}
}
最后的代码如下:
package cn.lyn4ever.structure.excerise;
public class EightQueenDemo {
/**
* 用来表示摆放皇后的结果,
* 数组的下标表示第i+1行的棋子,值表示棋子放在这一行的第i+1个位置
* 如:
* result[0]=0 表示在第一行的第一个位置上有一个棋子
*/
private int max = 8;
private int result[] = new int[max];
private int sum = 0;//用来记录总结果数
private int timeCount = 0;//用来记录所有操作的次数
/**
* 判断这一行中的棋子摆放的是否可用
* 主要就是判断从第一行到第n-1行的这几个皇后会不会和第n行的皇后相互攻击
* 因为之前的n-1行之间之前就已经判断了的,如果它们之前不满足条件就不会到第n行的
*
* @param n 传入的这一行
* @return
*/
private boolean check(int n) {
timeCount++;
for (int i = 0; i < n; i++) {
/*
第一个条件就是判断它们是不是在同一行
这里主要想一个如何判断它们是不是在同一斜行上,
因为我们的模型就是使用数组的角标来表示它当前在第i+1行,用元素表示它在这一行的第i+1个位置上
*/
if (result[i] == result[n] || Math.abs(n - i) == Math.abs(result[n] - result[i])) {
return false;
}
}
return true;
}
/**
* 打印这个结果数组
*/
private void printResult() {
for (int i = 0, length = result.length; i < length; i++) {
System.out.print(result[i] + " ");
}
System.out.println();
}
/**
* 最关键的问题,使用递归来进行回溯算法
* 我们的思路就是一行一行的放置
*
* @param row 在第几行摆放棋子
*/
public void calResult(int row) {
if (row > 7) {
//此时就说明第8行已经摆完了,且是正确的
sum++;
printResult();
return;
}
//这个for循环就是在这一行从第1个开始放,一直放到第max个,
for (int i = 0; i < max; i++) {
result[row] = i;
if (check(row)) {
//说明这一行检验是通过的
calResult(row+1);
}
//如果这个位置不满足,就会自动执行下一次循环,也就是i++
//如果这一行会部试完之后,还是不合格,就会自动放到回溯到上一行去
}
}
public static void main(String[] args) {
EightQueenDemo eightQueenDemo = new EightQueenDemo();
eightQueenDemo.calResult(0);
System.out.printf("八皇后问题的解法一共有%d", eightQueenDemo.sum);
System.out.printf("一共进行了%d次运算", eightQueenDemo.timeCount);
}
}
最后的运行结果如下: