回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法可以理解为通过选择不同的岔路口寻找目的地,一个岔路口一个岔路口的去尝试找到目的地。如果走错了路,继续返回来找到岔路口的另一条路,直到找到目的地。
回溯法经典题——八皇后:
在国际象棋中,皇后是最强大的一枚棋子,可以吃掉与其在同一行、列和斜线的敌方棋子。比中国象棋里的车强几百倍,比她那没用的老公更是强的飞起(国王只能前后左右斜线走一格)。上图右边高大的棋子即为皇后。
八皇后问题是这样一个问题:将八个皇后摆在一张8*8的国际象棋棋盘上,使每个皇后都无法吃掉别的皇后,一共有多少种摆法?下面以4*4的棋盘为例,解释思路。
步骤1
尝试先放置第一枚皇后,被涂黑的地方是不能放皇后
步骤2
第二行的皇后只能放在第三格或第四格,比方我们放第三格,则:
步骤3
第三行全部锁死了,第三位皇后无论放哪里都难逃被吃掉的厄运。于是在第一个皇后位于1号,第二个皇后位于3号的情况下问题无解。我们只能返回上一步来,给2号皇后换个位置。
步骤4
虽然是能放置第三个皇后,但是第四个皇后又无路可走了。返回上层调用(3号皇后),而3号也别无可去,继续回溯上层调用(2号),2号已然无路可去,继续回溯上层(1号),于是1号皇后改变位置如下,继续回溯。
这就是回溯算法的精髓,根据这个算法,最终能够把四位皇后放在4x4的棋盘里。也能用同样的方法解决了八皇后问题。下面上八皇后问题的代码。
改编自刘汝佳《算法竞赛入门经典》,见过的实现8皇后问题最简洁的代码
void queen(int row){
if(row==n)
total++;
else
for(int col=0;col!=n;col++){
c[row]=col;
if(is_ok(row))
queen(row+1);
}
}
算法是逐行安排皇后的,其参数row为现在正执行到第几行。n是皇后数,在八皇后问题里当然就是8。第2行,如果程序当前能正常执行到第8行,那自然是找到了一种解法,于是八皇后问题解法数加1。
完整版java代码如下,详细解释清参见《回溯法与八皇后问题》
public class EightQueen {
private static int n = 8;
private static int total = 0;
private static int[] c = new int[n];
public static void main(String[] args) {
EightQueen eq = new EightQueen();
System.out.println(total);
eq.queen(0);
System.out.println(total);
}
boolean isOk(int row) {
for (int j = 0; j != row; j++) {
if (c[row] == c[j] || row - c[row] == j - c[j] || row + c[row] == j + c[j]) {
return false;
}
}
return true;
}
void queen(int row) {
if (row == n) {
total++;
} else {
for (int col = 0; col != n; col++) {
c[row] = col;
if (isOk(row)) {
queen(row + 1);
}
}
}
}
}
运行结果:
0
92
回溯法经典题——0-1背包问题:
给定n种物品和一个容量为c的背包,物品i的重量为Wi,其价值为Pi,0-1背包问题是如何选择装入背包的物品(物品不可分割),使得装入背包的物品的价值为最大
求解步骤
1)针对所给问题,定义问题的解空间;
2)确定易于搜索的解空间结构;
3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
问题的解空间
用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少包含问题的一个(最优)解。对于 n=3 时的 0/1 背包问题,可用一棵完全二叉树表示解空间,如图所示:
常用的剪枝函数:用约束函数在扩展结点处剪去不满足约束的子树;用限界函数剪去得不到最优解的子树。
回溯法对解空间做深度优先搜索时,有递归回溯和迭代回溯(非递归)两种方法,但一般情况下用递归方法实现回溯法。
完整版java代码如下,详细解释清参见《回溯法与0-1背包问题》:
public class KnapsackProblem {
static int n, c, bestp;//物品的个数,背包的容量,最大价值
static int[] p = new int[10000], w = new int[10000], x = new int[10000], bestx = new int[10000];//物品的价值,物品的重量,x[i]暂存物品的选中情况,物品的 选中情况
public static void main(String[] args) {
bestp = 0;
KnapsackProblem kp = new KnapsackProblem();
//用例1
c = 10;
n = 10;
w = new int[]{2, 2, 6, 5, 4, 4, 3, 4, 6, 3};
p = new int[]{6, 3, 5, 4, 6, 2, 8, 3, 1, 7};
kp.dfsByBackTraching(0, 0, 0);
System.out.println(bestp);
for (int s = 0; s < 10; s++) {
System.out.print(bestx[s] + " ");
}
System.out.println();
//用例2
c = 5;
n = 4;
w = new int[]{1, 2, 3, 4};
p = new int[]{2, 4, 4, 5};
bestp = 0;
Arrays.fill(x, 0);
Arrays.fill(bestx, 0);
KnapsackProblem kp1 = new KnapsackProblem();
kp1.dfsByBackTraching(0, 0, 0);
System.out.println(bestp);
for (int s = 0; s < 4; s++) {
System.out.print(bestx[s] + " ");
}
}
void dfsByBackTraching(int i, int cp, int cw) { //cw当前包内物品重量,cp当前包内物品价值
int j;
if (i >= n) {//回溯结束
if (cp > bestp) { //是否超过了最大价值
bestp = cp; //更新最大价值
for (i = 0; i < n; i++) {
bestx[i] = x[i]; //得到选中的物品,bestx[i]值为1的元素对应的下表即为选中的物品编号
}
}
} else {
for (j = 0; j <= 1; j++) {
x[i] = j;
if (cw + x[i] * w[i] <= c) { //满足约束,继续向子节点探索
cw += w[i] * x[i];
cp += p[i] * x[i];
dfsByBackTraching(i + 1, cp, cw);
//回溯上一层物体的选择情况
cw -= w[i] * x[i];
cp -= p[i] * x[i];
}
}
}
}
}
运行结果:
24
1 1 0 0 0 0 1 0 0 1
8
0 1 1 0
回溯法经典题——求解数独:
数独(Sūdoku),一种数学游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含1-9,不重复。
数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次。
完整版java代码如下:
public class Sudoku {
static int[][] arr = new int[9][9];
public static void main(String[] args) {
//示例1
/* arr[0][0] = 9;
arr[0][1] = 1;
arr[0][7] = 4;
arr[2][3] = 5;
arr[2][5] = 3;
arr[3][0] = 5;
arr[3][2] = 6;
arr[3][5] = 8;
arr[3][6] = 3;
arr[5][4] = 1;
arr[5][7] = 2;
arr[5][8] = 4;
arr[6][0] = 8;
arr[6][2] = 5;
arr[7][2] = 3;
arr[8][4] = 4;
arr[8][8] = 1;*/
//示例2
arr[0][0] = 5;
arr[0][1] = 3;
arr[0][4] = 7;
arr[1][0] = 6;
arr[1][3] = 1;
arr[1][4] = 9;
arr[1][5] = 5;
arr[2][1] = 9;
arr[2][2] = 8;
arr[2][7] = 6;
arr[3][0] = 8;
arr[3][4] = 6;
arr[3][8] = 3;
arr[4][0] = 4;
arr[4][3] = 8;
arr[4][5] = 3;
arr[4][8] = 1;
arr[5][0] = 7;
arr[5][4] = 2;
arr[5][8] = 6;
arr[6][1] = 6;
arr[6][6] = 2;
arr[6][7] = 8;
arr[7][3] = 4;
arr[7][4] = 1;
arr[7][5] = 9;
arr[7][8] = 5;
arr[8][4] = 8;
arr[8][7] = 7;
arr[8][8] = 9;
Sudoku sd = new Sudoku();
sd.sudokuByBackTracking(0);
}
public void sudokuByBackTracking(int count) {
if (81 == count) {
print();
return;
}
int i = count / 9;
int j = count % 9;
if (arr[i][j] == 0) {
for (int n = 1; n <= 9; n++) {
arr[i][j] = n;
if (IsValid(count)) {
sudokuByBackTracking(count + 1);
}
}
arr[i][j] = 0;
} else {
sudokuByBackTracking(count + 1);
}
}
public boolean IsValid(int count) {
int i = count / 9;
int j = count % 9;
//检测行
for (int iter = 0; iter < 9; iter++) {
if (iter == j)
continue;
if (arr[i][iter] == arr[i][j]) {
return false;
}
}
//检测列
for (int iter = 0; iter < 9; iter++) {
if (iter == i)
continue;
if (arr[iter][j] == arr[i][j]) {
return false;
}
}
//检测小九宫格
for (int p = i / 3 * 3; p < (i / 3 + 1) * 3; p++) {
for (int q = j / 3 * 3; q < (j / 3 + 1) * 3; q++) {
if (p == i && j == q) {
continue;
}
if (arr[p][q] == arr[i][j]) {
return false;
}
}
}
return true;
}
public void print() {
System.out.println("数独的解集为:");
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}
示例1运行结果:
数独的解集为:
9 1 7 6 8 2 5 4 3
3 5 2 1 9 4 7 6 8
6 8 4 5 7 3 1 9 2
5 9 6 4 2 8 3 1 7
4 2 1 7 3 6 9 8 5
7 3 8 9 1 5 6 2 4
8 7 5 2 6 1 4 3 9
1 4 3 8 5 9 2 7 6
2 6 9 3 4 7 8 5 1
示例2运行结果:
数独的解集为:
5 3 4 6 7 8 9 1 2
6 7 2 1 9 5 3 4 8
1 9 8 3 4 2 5 6 7
8 5 9 7 6 1 4 2 3
4 2 6 8 5 3 7 9 1
7 1 3 9 2 4 8 5 6
9 6 1 5 3 7 2 8 4
2 8 7 4 1 9 6 3 5
3 4 5 2 8 6 1 7 9