回溯算法采用的是试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其他的可能的分步解答再次尝试寻找问题的答案。
回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
1 找到一个可能存在的正确的答案;
2 在尝试了所有可能的分步方法后宣告该问题没有答案
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
52. N皇后 II
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回 n 皇后不同的解决方案的数量。
示例:
输入: 4
输出: 2
解释: 4 皇后问题存在如下两个不同的解法。
[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解法1:回溯
该题的解法是基于获得所有N皇后不同解决方案的基础上得来的,改变返回值即可以返回所有不同的解决方案。
/**
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
*/
class Solution {
List<List<String>> res = new ArrayList();
public int totalNQueens(int n) {
//先构造一个棋盘:默认全为0,第i行j列放置棋子后置为1,最后根据值输出res的长度.
int[][] queen = new int[n][n];
backtrack(queen, 0, n);//从第0行开始放置棋子
return res.size();
}
private void backtrack(int[][] queen, int row, int n) {
//当0到n-1行全部放置完成后,输出结果
if(row == n) {
List<String> list = new ArrayList();
for(int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for(int j = 0; j < n; j++) {
if(queen[i][j] == 0) {
sb.append(".");
} else {
sb.append("Q");
}
}
list.add(sb.toString());
}
res.add(list);
return;
}
//验证第row行第col列是否合法,如果合法放置棋子并开始下一行
for(int col = 0; col < n; col++) {
if(isQueen(queen, row, col)) {
queen[row][col] = 1;//放置棋子
backtrack(queen, row + 1, n);//开始放置下一行
queen[row][col] = 0;//还原棋盘,验证下一个位置
}
}
}
private Boolean isQueen(int[][] queen, int row, int col) {
//1 纵向验证:只需要遍历上方已经有值的部分
for(int i = 0; i < row; i++) {
if(queen[i][col] == 1) return false;
}
int newrow = 0;
//2 正对角线:只需要遍历右上方已经有值的部分
for(int i = col+1; i < queen.length; i++) {
newrow = row + col - i;
if(newrow < 0) break;
if(queen[newrow][i] == 1) return false;
}
//3 负对角线:只需要遍历左上方已经有值的部分
for(int i = col-1; i >= 0; i--) {
newrow = row - col + i;
if(newrow < 0) break;
if(queen[newrow][i] == 1) return false;
}
return true;
}
}
终极解法:位运算
class Solution {
private static int size;
private static int count;
//在八皇后问题中,问题的关键是找出该行可放皇后的格子
public static int totalNQueens(int n) {
count = 0;
// 1 起始阶段可以填皇后的位置(值为1的位置不可以填皇后),八皇后size值后8位全是1
size = (1 << n) - 1;
solve(0, 0, 0);
return count;
}
// col 来记录所有上方行已放置的皇后导致当前行格子不可用的列集合,所在列如果放了皇后,则当前行格子对应的位置为 1,否则为 0,
// pie(撇,左斜线) 记录所有已放置的皇后左斜方向导致当前行格子不可用的集合,
// na(捺,右斜线) 表示所有已放置的皇后右斜方向导致当前行不可用的集合
public static void solve(int col, int pie, int na) {
if (col == size) {
count++;
return ;
}
// 2 得到当前行能放皇后的所有位置,所有位值为 1 的格子可放置皇后
// (1) (row | pie | na): 进行或运算得到目前行不能放皇后的位置,再取反即可得到所有能放皇后的位置值为1
// (2) 与size进行与运算是因为int是32位的,而我们只需要最低的n位
// (3) 而size是低n为全是1,其他为全是0的,进行与运算后正好可以得到所需要的最低的n位的值
int pos = size & (~(col | pie | na));
// 3 遍历当前行所有能放皇后的位置
while (pos != 0) {
//4 每次从当前行可用的格子中取出最右边位为 1 的格子放置皇后,pos & (-pos)意思就是取到最低位的1
//-p的二进制位:p的二进制位按位取反,末位加一
int p = pos & (-pos);
//5 在pos里去掉p,相当于在当前pos里面的p的位置放上皇后
pos -= p;
//6 在当前行确定填皇后的格子是p后,继续下探确定下一行的可用格子
// (pie | p) << 1 : 撇:往左下边移一位正好是撇
// (na | p) >> 1:捺:往右下边移一位正好是捺
solve(col | p, (pie | p) << 1, (na | p) >> 1);
// 不需要回溯col,pie,na的state:因为这三个值时int类型,在当前函数中,并没有改变这三个变量的值,只是在函数中复制了一份与p进行或运算在进行递归,所以不需要revert这三个值的state
}
}
}
无注释版(代码是不是很简洁):
class Solution {
private int size;
private int count;
public int totalNQueens(int n) {
count = 0;
size = (1 << n) - 1;
dfs(0,0,0);
return count;
}
public void dfs(int col, int pie, int na) {
if (col == size) {
count++;
return ;
}
int pos = size & (~(col | pie | na));
while (pos != 0) {
int p = pos & (-pos);
pos -= p;
dfs(col | p, (pie | p) << 1, (na | p) >> 1);
}
}
}