什么是回溯法?
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
回溯算法设计过程
1. 确定问题的解空间
2. 确定结点的扩展规则
3. 搜索解空间
回溯法例子
经典八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
-
简单的四皇后问题
四皇后与八皇后解法相同,只是放置的旗子比较少,用这个来讲解,容易理解。
上图为四皇后遍历循环解法的全图。
1. 第一行为一个棋盘
2. 第二行为 放置第一个皇后,一共可以在第一行的四个位置。
3. 第三行为 放置了第一个皇后之后,放置第二个皇后的位置,也可以放置在四个位置。(第一行的每个放置的位置,第三行都有四个对应的分支,篇幅所限,没有画出。)
4. 所以第三行有4X4=16种方法,一次类推,第四行(也就是放置第三个皇后之后)有 16X4=64中方法,第五行(第四个皇后)有64X4=256中方法。
现在来去掉不符合解法的方法:首先看上图第三行,放置第二个皇后的时候,明显第三行图片的第一、第二、第三小图都不符合要求,所以他们之下的所有解法都应该排除。
总结: 循环放置皇后,在第二个皇后不符合的情况下,(如上图第三行 第一小图)也会继续放置其他皇后,这样浪费了资源,而回溯法就是在出现这种情况的时候,不在继续往下放置,而是切换到第二个小图。(以此类推,直到发现第四个小图符合要求,才会继续放置第三个皇后)
四皇后回溯法的步骤如下图所示:
上图为 四皇后解法的代码执行顺序。可以在下边的代码理解之后,在对照着来理解。
八皇后的代码步骤:
- 从第0行0列开始,检查第0行能否放置一个皇后
- 如果可以,那么放置一个皇后 (也就是0行0列置为1)
- 接下来放置第1行的皇后,依次类推,直到放置到第8的时候,说明找到一个解
- 如果没有找到一个解,那么就将上一步放置的皇后置为0(也就是第0行0列置为0)
- 然后开始0行1列,重复以上步骤(也就是0行开始,循环把皇后放置到0行的每一列,然后看能否找到一个解)
Talk is cheap, show me the code.
-
创建对象,初始化棋盘。
/** * @author colter * 2018/3/25 */ public class QueenSolution { //模拟一个8X8的棋盘,0代表没有放置,1代表放置了一个皇后 private int[][] board = new int[8][8]; //解法的数量 private int total = 0; } -
增加放置第K个皇后的方法
/** * 放置皇后的时候从第0行开始,依次放置一个 * 如果放置成功,那么继续放置下一行。(0-7) * 当要放置k=8的时候说明已经全部放置完 * 毕了,找到了一个对应的解 * * @param k 放置第几个皇后,K从0开始 */ //放置第K个皇后 public void putQueen(int k) { int max = board.length; //放置第8个,说明棋盘已经放置完毕了,输出结果。 if (k >= max) { //找到一个解,打印出来。 total++; //打印解 System.out.println(String.format("=============%s===============", total)); for (int i = 0; i < max; i++) { System.out.println(Arrays.toString(board[i])); } System.out.println("============================="); } else { /** * A: * 1. 从第0行,0列开始放置一个皇后 * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行 * 3. 如果全部顺利,那么直到找到一个解 * 4. 然后0行1列放置一个皇后,找到一个解。以此类推 * * B: * 1. 从第0行,0列开始放置一个皇后 * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行 * 3. 如果下一列没有可以放置的位置,那么将刚才放置的位置 置为0 * (也就是皇后不能放在这里,此时会回溯到上一层循环,重新放置) * 例子: * k = 6 的时候,假如board[6][0]放置了一个皇后(k=6,i=0), * 那么在接下来执行putQueen(6+1) * 假如遍历board[7]之后发现没有位置能够放置一个皇后, * 那么会执行board[k][i] = 0 (k=6,i=0) * 此时第6行第0列的皇后被拿走了,此时i自增为1 * 然后执行check(k,i) (k=6,i=1)如果能放置的话,重复上边的动作, * 如果不能放置的话继续自增i,放置到第6行的下一列。 */ for (int i = 0; i < max; i++) { if (check(k, i)) { board[k][i] = 1; putQueen(k + 1); board[k][i] = 0; } } } } -
增加检查是否满足的方法
/** * 皇后放置的时候是从上到下每一行放置的,所以不用检查改行以及之后的行 * 所以只用检查列以及左上右上对角线 * * @param row 检查的对应行 * @param col 检查的对应列 * @return 返回改点是否满足可以放置一个皇后 */ private boolean check(int row, int col) { //检查列是否有皇后 for (int i = 0; i < row; i++) { if (board[i][col] == 1) { return false; } } //检查左上对角线是否有皇后 for (int m = row - 1, n = col - 1; m >= 0 && n >= 0; m--, n--) { if (board[m][n] == 1) { return false; } } //检查右上对角线是否有皇后 for (int m = row - 1, n = col + 1; m >= 0 && n < board[0].length; m--, n++) { if (board[m][n] == 1) { return false; } } return true; }
整个解法已经完毕了,整个文章只是为了讲解回溯法以及其应用,所以并不会用到很复杂的代码,以及代码结构的不合理之处,还请见谅。
以下为完整代码(可直接运行):
import java.util.Arrays;
/**
* @author colter
* 2018/3/25
*/
public class QueenSolution {
//修改棋盘的大小,可模拟其他皇后类似问题
//模拟一个8X8的棋盘,0代表没有放置,1代表放置了一个皇后
private int[][] board = new int[8][8];
//解法的数量
private int total = 0;
/**
* 放置皇后的时候从第0行开始,依次放置一个
* 如果放置成功,那么继续放置下一行。(0-7)
* 当要放置k=8的时候说明已经全部放置完
* 毕了,找到了一个对应的解
*
* @param k 放置第几个皇后,K从0开始
*/
//放置第K个皇后
public void putQueen(int k) {
int max = board.length;
//放置第8个,说明棋盘已经放置完毕了,输出结果。
if (k >= max) {
//找到一个解,打印出来。
total++;
System.out.println(String.format("=============%s===============", total));
for (int i = 0; i < max; i++) {
System.out.println(Arrays.toString(board[i]));
}
System.out.println("=============================");
} else {
/**
* A:
* 1. 从第0行,0列开始放置一个皇后
* 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
* 3. 如果全部顺利,那么直到找到一个解
* 4. 然后0行1列放置一个皇后,找到一个解。以此类推
*
* B:
* 1. 从第0行,0列开始放置一个皇后
* 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
* 3. 如果下一列没有可以放置的位置,那么将刚才放置的位置 置为0
* (也就是皇后不能放在这里,此时会回溯到上一层循环,重新放置)
* 例子:
* k = 6 的时候,假如board[6][0]放置了一个皇后(k=6,i=0),
* 那么在接下来执行putQueen(6+1)
* 假如遍历board[7]之后发现没有位置能够放置一个皇后,
* 那么会执行board[k][i] = 0 (k=6,i=0)
* 此时第6行第0列的皇后被拿走了,此时i自增为1
* 然后执行check(k,i) (k=6,i=1)如果能放置的话,重复上边的动作,
* 如果不能放置的话继续自增i,放置到第6行的下一列。
*/
for (int i = 0; i < max; i++) {
if (check(k, i)) {
board[k][i] = 1;
putQueen(k + 1);
board[k][i] = 0;
}
}
}
}
/**
* 皇后放置的时候是从上到下每一行放置的,所以不用检查改行以及之后的行
* 所以只用检查列以及左上右上对角线
*
* @param row 检查的对应行
* @param col 检查的对应列
* @return 返回改点是否满足可以放置一个皇后
*/
private boolean check(int row, int col) {
//检查列是否有皇后
for (int i = 0; i < row; i++) {
if (board[i][col] == 1) {
return false;
}
}
//检查左上对角线是否有皇后
for (int m = row - 1, n = col - 1; m >= 0 && n >= 0; m--, n--) {
if (board[m][n] == 1) {
return false;
}
}
//检查右上对角线是否有皇后
for (int m = row - 1, n = col + 1; m >= 0 && n < board[0].length; m--, n++) {
if (board[m][n] == 1) {
return false;
}
}
return true;
}
public static void main(String[] args) {
QueenSolution solution = new QueenSolution();
solution.putQueen(0);
}
}