2020/10/29N皇后
本周是跟着B站教学视频学习回溯+剪枝。
八皇后问题
八皇后问题(英文:Eight queens),是由国际西洋棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。
问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
先放代码
public class Queen {
public static void main(String[] args) {
new Queen().placeQueens(8);
}
int[] cols;
int ways = 0;
void placeQueens(int n){
if(n < 1)
return;
cols = new int[n];
place(0);
System.out.println(n+"皇后一定有"+ways+"种摆法");
}
void place(int row){
if(row == cols.length){
ways++;
show();
return;
}
for(int col = 0; col<cols.length;col++){
if(isVaild(row,col)){
cols[row] = col;
place(row+1);//执行不下去时则会回溯,重点在于对递归的一个理解
}
}
}
boolean isVaild(int row, int col){
for(int i = 0;i < row;i++){//这里用了一个for循环,每次需要取出每一行的cols来对比是否已经放置皇后
if(cols[i] == col)
return false;
if(row -i == Math.abs(col-cols[i]))//这里涉及到了初中数学的k= 1 || -1来判定斜线
return false;
}
return true;
}
void show(){
for(int row = 0; row < cols.length;row++){
for(int col = 0; col < cols.length;col++){
if(cols[row] == col){
System.out.print("1 ");
}else {
System.out.print("0 ");
}
}
System.out.println();
}
System.out.println("---------------------------");
}
}
先来简单讲讲对这个回溯的理解,其实就是对递归的一个应用,对于递归最容易理解的应该还是一颗二叉树,想象一下迷宫里,一个人走到岔路口,有两个位置可以走,走左边的人和走右边的人将产生两种不同的结果,并且不断地需要去做出选择,这一次是选择走左边还是走右边,在没有走到尽头(死胡同)时,这个人将一直走下去。
这里呢使用了cols数组来存放每一行皇后的摆放位置,再接着调用自身去计算下一行的皇后位置,如果在当前for循环中,isVaild()函数的判断结果都是false,那么就会接着回溯上一行。
但不知道大家会不会有和我同样的想法呀,就是这个cols数组装的东西,不会被反复覆盖掉最后根本就没法正常实现记录功能嘛,打印出来才发觉自己糊涂了,明显也就是对于每一row,都将重新复制(覆盖掉),而这个递归的执行顺序时线性的,也就是选择走左边的人将走到尽头(死胡同)时,它将自己传送到上一次做出决策的地方,也就是上一个路口的另外一个方向。
看下图应该对程序的执行顺序更为直观,重点关注place(1),place(2),place(1)这几个变动时发生了什么。
也就是在这个for循环中,在第一个if判断语句成立时,它进入下一个place(),当place()走投无路,它会回到原本的for循环中其他成立的isValid()下继续进行其他的递归。
优化一:对剪枝进行优化
查看原本的代码,发现isVaild()的判断是用了一个for循环来进行,于是这一个块可以以空间换时间,节约至O(1)的复杂度。
但是效率虽然提高,但是会发现,我们没有办法去追踪这个八皇后是被摆在了什么位置,因为cols每次都被覆盖掉,解决方案是增加一个数组去存储(但这里就偷懒不写啦)。
public class Queen2 {
public static void main(String[] args) {
new Queen2().placeQueens(8);
}
//int[] cols;
int ways = 0;
boolean[] cols;
boolean[] leftTop;//左上角到右下角的斜线
boolean[] rightTop;//右上角到左下角的斜线
void placeQueens(int n){
if(n < 1)
return;
cols = new boolean[n];
leftTop = new boolean[(n<<1) -1];//2*n n左移一位
rightTop = new boolean[leftTop.length];//此时已经不用再去做一次计算,尽量优化
place(0);
System.out.println(n+"皇后一定有"+ways+"种摆法");
}
void place(int row){
if(row == cols.length){
ways++;
show();
return;
}
for(int col = 0; col<cols.length;col++){
if(cols[col])
continue;
int ltIndex =row-col+cols.length-1;
int rtIndex = row+col;
if(leftTop[ltIndex])
continue;
if(rightTop[rtIndex])
continue;
cols[col] = true;
leftTop[ltIndex] = true;
rightTop[rtIndex] = true;
// cols[row] = col;
place(row+1);
cols[col] = false;//还原代码
leftTop[ltIndex] = false;
rightTop[rtIndex] = false;
}
}
// boolean isVaild(int row, int col){
//// for(int i = 0;i < row;i++){
//// if(cols[i] == col)
//// return false;
//// if(row -i == Math.abs(col-cols[i]))
//// return false;
////
//// }
// return true;
//
// }
void show(){
// for(int row = 0; row < cols.length;row++){
// for(int col = 0; col < cols.length;col++){
// if(cols[row] == col){
// System.out.print("1 ");
// }else {
// System.out.print("0 ");
// }
// }
// System.out.println();
// }
// System.out.println("---------------------------");
}
}
优化二:用位运算降低空间复杂度
这一点的思路在于布尔数组存的是true/fasle,true-》1,false-》0,在八皇后的情形下,一个数组可以替换成一个字节,如00100111 即byte cols;
,两个字节则是使用short rightTop
来替代
public class Queen3 {
public static void main(String[] args) {
new Queen3().placeQueens();
}
int ways = 0;
byte cols;
short leftTop;//左上角到右下角的斜线
short rightTop;//右上角到左下角的斜线
void placeQueens() {
place(0);
System.out.println(8 + "皇后一定有" + ways + "种摆法");
}
void place(int row) {
if (row == 8) {
ways++;
return;
}
for (int col = 0; col < 8; col++) {
int cv = 1 << col;
if ((cols & cv) != 0)
continue;
int ltIndex = 1 << (row - col + 7);
int rtIndex = 1 << (row + col);
if ((ltIndex & leftTop) != 0)
continue;
if ((rtIndex & rightTop) != 0)
continue;
//cols = (byte) (cols |(1<<col));
cols |= (1 << col);
leftTop |= ltIndex;
rightTop |= rtIndex;
place(row + 1);
cols &= ~cv;
leftTop &= ~ltIndex;
rightTop &= ~rtIndex;
}
}
}
这里涉及的位运算技巧还是挺多的,我就先承认自己没掌握好啦