算法-回溯-N皇后
1 题目概述
1.1 题目出处
https://leetcode-cn.com/problems/n-queens/
1.2 题目描述
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],
["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释: 4 皇后问题存在两个不同的解法。
提示:
皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一或七步,可进可退。(引用自 百度百科 - 皇后 )
2 低效的回溯法
2.1 思路
这段转自组合总和-解题思路
回溯法的解体框架是什么呢,解决一个回溯问题,实际上就是一个决策树的遍历过程。一般来说,我们需要解决三个问题:
- 路径:也就是已经做出的选择。
- 选择列表:也就是你当前可以做的选择。
- 结束条件:也就是到达决策树底层,无法再做选择的条件。
其中最关键的点就是:在递归之前做选择,在递归之后撤销选择。
LinkedList result = new LinkedList();
public void backtrack(路径,选择列表){
if(满足结束条件){
result.add(结果);
}
for(选择:选择列表){
做出选择;
backtrack(路径,选择列表);
撤销选择;
}
}
这里我们每次回溯递归时,就遍历该行的所有列。且每次需要判断该行该列位置放入皇后是否会攻击到之前已经放好的皇后,如果冲突则不选,如果不冲突才选并继续回溯递归。
2.2 代码
class Solution {
private List<List<String>> resultList = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
if(n == 0){
return resultList;
}
// 使用TreeMap row为key,key为行皇后放置的列位置,保证按row排序
backtrack(n, new TreeMap<Integer,Integer>(), 0);
return resultList;
}
private void backtrack(int n, TreeMap<Integer,Integer> recordMap, int row){
if(row == n){
// 结束条件
List<String> tmpResult = new ArrayList<>();
Iterator<Map.Entry<Integer,Integer>> it = recordMap.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer,Integer> entry = it.next();
int tmpCol = entry.getValue();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++){
if(i == tmpCol){
sb.append("Q");
}else{
sb.append(".");
}
}
tmpResult.add(sb.toString());
}
resultList.add(tmpResult);
}
for(int i = 0; i < n; i++){
if(isValid(n, recordMap, row, i)){
recordMap.put(row,i);
backtrack(n, recordMap, row + 1);
recordMap.remove(row);
}
}
}
private boolean isValid(int n, TreeMap<Integer,Integer> recordMap, int row, int col){
// 判断该元素所在列
if(recordMap.containsValue(col)){
return false;
}
// 判断元素所在的左上
for(int i = row - 1,j = col-1; i >=0 && j >=0; i--,j--){
if(null != recordMap.get(i) && recordMap.get(i) == j){
return false;
}
}
// 判断元素所在的右上
for(int i = row - 1,j = col+1; i >=0 && j < n; i--,j++){
if(null != recordMap.get(i) && recordMap.get(i) == j){
return false;
}
}
return true;
}
}
2.3 时间复杂度
O(N^3)
每次回溯需要遍历N列 * 每列判断该位置Valid需要判断N*3 * 一共N行
2.4 空间复杂度
O(N)
- 全部共用一个大小为N的TreeMap
3 优化后的回溯法
3.1 思路
前面的回溯法中,每次判断该位置放置皇后都需要三次遍历来判断(3 * O(N)),十分低效,有没有更高效的方法呢?
我们可以想到用数组,他取某一位是就是O(1)的。
用数组保存列很简单,只需要一个长度为N的Bitmap,只要某列放了皇后就将该列位置置为1即可。
但是斜着的方向怎么表示呢?
通过观察可以发现:
- 从右上方往左下45度划的直线上的点的行坐标减列坐标的差相同,比如(1,0) -> (2,1) -> (3,2)差都是1,所以可以用行坐标减列坐标的差来表示所在的从左上方往右下45度划的直线。那么就可以用一个BitSet来所有该类斜线,只要一个斜线放了皇后就把表示该斜线的位标记为1。
- 从左上方往右下45度划的直线上的点的行坐标加列坐标的和相同,比如(2,0) -> (1,1) -> (0,2)和都是2,所以可以用行坐标加列坐标的和来表示所在的从右上方往左下45度划的直线。那么就可以用一个BitSet来所有该类斜线,只要一个斜线放了皇后就把表示该斜线的位标记为1。
3.2 代码
class Solution {
private List<List<String>> resultList = new ArrayList<>();
// 只要一个列放了皇后,该列位置就标记为1
private BitSet colBitset;
// 从右上方往左下45度的斜线(从左上方往右下45度划的直线上的点的行坐标减列坐标的差相同,范围为-n到n,等差为1)
private BitSet leftBitset;
// 从左上方往右下45度的斜线(从右上方往左下45度划的直线上的点的行坐标加列坐标的和相同,范围为0->2n,等差为1)
private BitSet rightBitset;
// 使用TreeMap row为key,key为行皇后放置的列位置,保证按row排序
private TreeMap<Integer,Integer> recordMap;
public List<List<String>> solveNQueens(int n) {
if(n == 0){
return resultList;
}
colBitset = new BitSet(n);
leftBitset = new BitSet(n + 3);
rightBitset = new BitSet(n + 3);
recordMap = new TreeMap<>();
backtrack(n, 0);
return resultList;
}
private void backtrack(int n, int row){
if(row == n){
// 结束条件
List<String> tmpResult = new ArrayList<>();
Iterator<Map.Entry<Integer,Integer>> it = recordMap.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer,Integer> entry = it.next();
int tmpCol = entry.getValue();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++){
if(i == tmpCol){
sb.append("Q");
}else{
sb.append(".");
}
}
tmpResult.add(sb.toString());
}
resultList.add(tmpResult);
}
for(int i = 0; i < n; i++){
if(isValid(row, i, n)){
add(row, i, n);
backtrack(n, row + 1);
remove(row, i, n);
}
}
}
private void add(int row, int col, int n){
recordMap.put(row, col);
colBitset.set(col, true);
leftBitset.set(row - col + n, true);
rightBitset.set(row + col, true);
}
private void remove(int row, int col, int n){
recordMap.remove(row);
colBitset.set(col, false);
leftBitset.set(row - col + n, false);
rightBitset.set(row + col, false);
}
private boolean isValid(int row, int col, int n){
// 1. 判断列
if(colBitset.get(col)){
return false;
}
// 2. 判断右上到左下的斜线
if(leftBitset.get(row - col + n)){
return false;
}
// 3. 判断左上方往右下的斜线
if(rightBitset.get(row + col)){
return false;
}
return true;
}
}
3.3 时间复杂度
O(N^2)
每次回溯需要遍历N列 * 每列判断该位置Valid需要判断1 * 一共N行
3.4 空间复杂度
O(N)
- 全部共用一个大小为N的TreeMap