很久没聊算法啦!此次咱们来聊聊n皇后问题。n 皇后问题,研究的是如何将 n 个皇后放置在 n×n 的棋盘上,而且使皇后彼此之间不能相互攻击。好多同窗对这样的问题都比较慌张,以为规则多烧脑抗拒,祈祷面试中不要遇到,别急,咱们今天就来尝试把这其中的逻辑给说道说道。面试
一个皇后能够向水平、垂直以及向斜对角方向移动,若是一个皇后出如今另外一个皇后的同一行,同一列或者斜对角,那它就能够被这个皇后攻击。算法
那咱们直觉里的一个比较粗暴的办法,就是列举出在棋盘上全部的状况,而后咱们判断每一个组合是否是符合咱们的条件。可是实际上咱们不须要尝试全部的组合,咱们知道当咱们在某一列上放置了一个皇后以后,其它的皇后就不能放在这一列了,在它的同一个水平线上跟四个斜对角也放不了。这样咱们能够最先发现“此路不通”。数组
当咱们发现一条路行不通时,咱们赶忙回到前面一步,而后尝试另一个不一样的选择,咱们称之为回溯
。安全
针对n皇后问题咱们把这个思路再展开一下:markdown
是否是还以为有点抽象?函数
那咱们拿其中一个场景来具体说说:优化
如今是否是以为眼睛会了?🤔,接下来咱们可让手来试试了。spa
首先咱们须要一个方法来判断某一个位置能不能放皇后,这样若是一个位置会被棋盘上已有的皇后攻击的话,咱们能够直接跳过这个位置:code
// 这个方法用来判断咱们接下来要放置的皇后在不在某个已经放置的皇后的水平方向、垂直方向或者斜对角,
// 若是都不,那咱们找到了一个合适的位置来放一个新皇后了
static boolean isValidPosition(int proposedRow, int proposedCol, List<Integer> solution) {
//对当前棋盘上的全部皇后,咱们都要作判断
for (int oldRow = 0; oldRow < proposedRow; ++oldRow) {
int oldCol = solution.get(oldRow);
int diagonalOffset = proposedRow - oldRow;
if (oldCol == proposedCol ||
oldCol == proposedCol - diagonalOffset ||
oldCol == proposedCol + diagonalOffset) {
return false;
}
}
return true;
}
复制代码
有了这个方法以后,咱们就须要实现逐行搜索以及在全部路不通时回到前一行的搜索里面来,继续寻找其它可能性。每一行的搜索方式都一致,因此这边适合使用递归来实现咱们的逻辑:orm
static void solveNQueensRec(int n, List<Integer> solution, int row, List<List<Integer>> results) {
// 最后一行也找到了一个合适的位置,咱们成功找到了一种解决方案
if (row == n) {
results.add(new ArrayList<Integer>(solution));
return;
}
// 从每一行的第一列开始尝试
for (int i = 0; i < n; ++i) {
// 对于走到最后一列还没都没有找到合适的点的状况, 当前递归结束,调用栈回到上一层的递归流程,会回去执行前面一行里剩余的状况
if (isValidPosition(row, i, solution)) {
solution.set(row, i);
solveNQueensRec(n, solution, row + 1, results);
}
}
}
复制代码
当有条路走不通时,调用栈里当前递归就执行结束了,它会回到上一个递归的调用逻辑里,也就实现了咱们的回溯
。咱们的目的很简单,这一行走到最后没路走了,就继续回到前一行继续日后走,直到全部的路都尝试过。
最后执行一下咱们的递归函数就好啦:
static int solveNQueens(int n, List<List<Integer>> results) {
List<Integer> solution = new ArrayList<Integer>(n);
for (int i = 0; i < n; ++i) {
solution.add(-1);
}
solveNQueensRec(n, solution, 0, results);
return results.size();
}
复制代码
这边至关于从N×N的数组里,选出N个数,时间复杂度是O(n^n)
,而空间复杂度是O(n!)
。
上面咱们搜索的过程当中,一行一行上升去寻找合适的位置,而后在某个条件下又回到前一行,有点像栈的入栈出栈操做,其实咱们也是能够用栈来实现整个回溯过程的。咱们在某一行里找到一个合适的位置时就把它的列push
到栈中,回溯到前一行时再把它pop
出来。
// 这边栈里咱们只存放列的值
static int solveNQueens(int n, List<List<Integer>> results) {
List<Integer> solution = new ArrayList<Integer>(n);
Stack<Integer> solStack = new Stack<Integer>();
for (int i = 0; i < n; ++i) {
solution.add(-1);
}
int row = 0;
int col = 0;
while(row < n){
while(col < n){
// 当前能够放置皇后,继续下一行的寻找
if(isValidPosition(row, col, solution)){
solStack.push(col);
solution.set(row, col);
row++;
col = 0;
break;
}
// 当前位置不行,尝试在下一列放置皇后
col++;
}
// 找到了当前行的最后一列
if(col == n){
// 说明前面一行还没到最后一列,执行pop操做回到前一行寻找过程
if(!solStack.empty()){
col = solStack.peek() + 1;
solStack.pop();
row--;
}
else{
// 只有第一行也走到了最后一列,而且全部路径都尝试过了,此时因为上面if里的逻辑栈空了,说明咱们的寻找过程该结束了
break;
}
}
if(row == n){
// 咱们找到一种符合条件的摆放位置
results.add(new ArrayList<Integer>(solution));
// 回溯到前一行
row--;
col = solStack.peek() + 1;
solStack.pop();
}
}
return results.size();
}
复制代码
时间复杂度是O(n^n)
,空间复杂度是O(n!)
。这边整个逻辑仍是比较直接的,咱们依旧须要isValidPosition
这个辅助方法来判断某个位置能不能放置皇后,而后对每一个位置逐一判断,用栈来配合寻找过程当中的回溯操做,核心思想仍是不变的。
咱们两种办法里都把全部的符合规则的摆放记录下来了,若是咱们只须要最后求得有多少种可能性,那咱们其实能够把数组换成一个变量来计数,这样咱们的空间复杂度能够优化成O(n)
。
好啦,相信你们这会儿对回溯算法
有了一个感性的认识,也能明白回溯
只是咱们面对问题时常规的思路,并非什么高大上的概念,咱们不用去畏惧它~