递归函数不再调用他自己而是返回上一层调用,这种现象称为回溯。
当把问题分成若干个步骤并进行递归求解时,如果当前步骤没有合法选择,则函数将返回上一级调用。正是这个原因,递归枚举算法也经常被称为回溯法。
之前介绍了递归构造和简单枚举,简单枚举思路简单程序也简单,缺点是无法减小枚举量--必须生成(generate)所有可能的解,然后一一检查(test)。在递归构造中,回溯将生成和检查的过程有机结合起来,从而减少了不必要的枚举量。就是尝试枚举出所有的,但是走不通的就不走了,减少了许多枚举。
在回溯种如果具有辅助的全局变量,一定要记得修改回来,不要影响后来的递归。
回溯与dfs,之前在学习数据结构时,dfs就是用回溯的思想实现的。而从解答树的角度来看,回溯法正是按照深度优先遍历的顺序遍历解答树。
在回溯法中,应该避免不必要的判断,比如八皇后问题中,只需要判断新皇后和之前的皇后是否冲突而不用判断之前皇后之间的冲突。
多“剪枝”,往往需要记录下当前的最优解,通过某些“预测”剪掉一些枝,进行优化。
经典问题:八皇后问题
1 //回溯,八皇后问题,在8*8的格子上放八个皇后,使她们不能成行成列或者成对角线 2 public static void search(int cur,int[]c) { 3 //每一行必有一个皇后,所以需要确定每一行对应的每一列 4 //c[i]=j表示第i行,第j列上有皇后 5 if(cur==8) { 6 System.out.println(1); 7 return; 8 } 9 for(int i=0;i<8;i++) { 10 int flag=1; 11 for(int j=0;j<cur;j++) { 12 if(c[j]==i||cur-j==i-c[j]||cur-j==c[j]-i) { 13 flag=0;break; 14 } 15 } 16 if(flag==1) { 17 c[cur]=i;search(cur+1,c); 18 } 19 } 20 }
1 public static void search(int cur,int[]c) { 2 //每一行必有一个皇后,所以需要确定每一行对应的每一列 3 //c[i]=j表示第i行,第j列上有皇后 4 if(cur==8) { 5 System.out.println(1); 6 return; 7 } 8 for(int i=0;i<8;i++) { 9 int flag=1; 10 c[cur]=i; 11 for(int j=0;j<cur;j++) { 12 if(c[j]==c[cur]||cur-j==c[cur]-c[j]||cur-j==c[j]-c[cur]) { 13 flag=0;break; 14 } 15 } 16 if(flag==1) { 17 search(cur+1,c); 18 } 19 } 20 }
这两种写法,效果是一样的,但是下面这种是书上给的,更贴合“回溯”这一概念,就是这一步已经走了但是不满足条件然后回溯。上面的更像是判断这个满不满足条件然后进行更新。
可以进一步优化,用一个全局变量存放列,对角线的元素,避免了内层循环。
1 public static void search1(int cur,int[][]x,int[]c) { 2 //每一行必有一个皇后,所以需要确定每一行对应的每一列 3 //x[0][i]=j表示第i行,第j列上有皇后 x[1][i-j]表示在对角线Ⅰ上, 4 // x[2][i+j]表示在对角线Ⅱ上 5 if(cur==8) { 6 System.out.println(1); 7 return; 8 } 9 for(int i=0;i<8;i++) { 10 if(x[0][i]==0&&x[1][i-cur+8]==0&&x[2][i+cur]==0) { 11 c[cur]=i; 12 x[0][i]=1;x[1][i-cur+8]=1;x[2][i+cur]=1;//修改全局变量 13 search1(cur+1,x,c); 14 x[0][i]=0;x[1][i-cur+8]=0;x[2][i+cur]=0;//将全局变量改回来 15 } 16 } 17 }