题目链接:https://leetcode-cn.com/problems/n-queens/
题目描述:n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
基于回溯的思想,一个较为粗略的程序如下:solve 函数中,每次选择一个合适的位置(flag 为 false)来设置为 Q,然后更新当前棋盘上已有皇后的活动区域(设置为 true),然后继续下一个位置的查找,直到 Q 的个数 num 等于 n,即表示找到了一种正确的结果,保存并返回。 其中 flag 数组表示棋盘上已有的 Q 的活动区域。
List<List<String>> ans=new LinkedList<>();
public List<List<String>> solveNQueens(int n) {
char[][] tmp=new char[n][n];
boolean[][] flag=new boolean[n][n];//用来标记棋盘上已有皇后的占领区域。
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
tmp[i][j]='.';
solve(tmp,n,flag,0);
return ans;
}
public void solve(char[][] tmp,int n,boolean[][] flag,int num){
if(num==n){ //成功时保存,且程序返回
save(ans,tmp,n);
return;
}
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
if(flag[i][j]==false){
tmp[i][j]='Q';
setDomin(flag,n,i,j,true);
solve(tmp,n,flag,num+1);
setDomin(flag,n,i,j,false);//需要采用别的方法恢复flag到上一个状态。它和 setDomin(true)不是可逆的。
tmp[i][j]='.';
}
}
}
public void setDomin(boolean[][] flag,int n,int i,int j,boolean status){
for(int k=0;k<n;k++){//水平 竖直 方向
flag[i][k]=status;
flag[k][j]=status;
}
for(int k=1;k<n;k++){
if(i+k<n&&j+k<n)//右下
flag[i+k][j+k]=status;
if(i-k>=0&&j-k>=0)//左上
flag[i-k][j-k]=status;
if(i-k>=0&&j+k<n)//左下
flag[i-k][j+k]=status;
if(i+k<n&&j-k>=0)//右上
flag[i+k][j-k]=status;
}
}
public void save(List<List<String>> ans,char[][] tmp,int n){ //将 char[][] 转化为 List 并保存
List<String> t=new LinkedList<>();
for(int i=0;i<n;i++)
t.add(new String(tmp[i]));
ans.add(t);
}
上面代码在回溯时出现问题,因为它不能恢复 flag 到上一个状态,所以需要修改,拟采用的解决方法为重新绘制除之间的所有 Q 的活动区域,而不包括当前的 Q ,当然,这需要保存之前的 Q 坐标,修改后的算法如下:
1,增加变量 chosen 记录了已经选取的 Q 位置,方便我们重新设置 flag 数组(在将 flag 数组回退到上一个状态时)。2,修改变量 ans 的类型为 Set ,因为在执行的时候发现,虽然此算法的正确性没有问题,但是需要优化(包括剪枝),在 n 为 4 ,使用 List 存储答案的情况下,得到的最终结果有 48 个, 而每 24 个一摸一样,所以目前先建立了一个 Set 来去掉重复的元素,保证可以得到正确的结果。
Set<List<String>> ans=new HashSet<>();
public List<List<String>> solveNQueens(int n) {
char[][] tmp=new char[n][n];
boolean[][] flag=new boolean[n][n];//用来标记棋盘上已有皇后的占领区域。
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
tmp[i][j]='.';
LinkedList<int[]> chosen=new LinkedList<>();//用来保存已有的皇后坐标。
solve(tmp,n,flag,0,chosen);
List<List<String>> re=new LinkedList<>();
re.addAll(ans);
return re;
}
public void solve(char[][] tmp,int n,boolean[][] flag,int num,LinkedList<int[]> chosen){
if(num==n){ //成功时保存,且程序返回
save(ans,tmp,n);
return;
}
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
if(flag[i][j]==false){
tmp[i][j]='Q';
chosen.addLast(new int[]{i,j});
setDomin(flag,n,i,j,true);
solve(tmp,n,flag,num+1,chosen);
chosen.removeLast();//删除当前 Q 的位置
cancel(flag,n,chosen);// 重新设置 flag 数组到上一个状态。
tmp[i][j]='.';
}
}
}
public void cancel(boolean[][] flag,int n,LinkedList<int[]> chosen){
for(int i=0;i<n;i++)
Arrays.fill(flag[i],false);
for(int[] t:chosen)
setDomin(flag,n,t[0],t[1],true);
}
public void setDomin(boolean[][] flag,int n,int i,int j,boolean status){
for(int k=0;k<n;k++){//水平 竖直 方向
flag[i][k]=status;
flag[k][j]=status;
}
for(int k=1;k<n;k++){
if(i+k<n&&j+k<n)//右下
flag[i+k][j+k]=status;
if(i-k>=0&&j-k>=0)//左上
flag[i-k][j-k]=status;
if(i-k>=0&&j+k<n)//左下
flag[i-k][j+k]=status;
if(i+k<n&&j-k>=0)//右上
flag[i+k][j-k]=status;
}
}
public void save(Set<List<String>> ans,char[][] tmp,int n){
List<String> t=new LinkedList<>();
for(int i=0;i<n;i++)
t.add(new String(tmp[i]));
ans.add(t);
}
上面算法属于暴力枚举,其时间复杂度很高。对它进行优化如下:
参考对应官方题解,由于上述暴力枚举的时间复杂度非常高,需要利用限制条件加以优化。
需要说明的是,下面算法仍然和之前的算法类似,大体的回溯框架没有变化。唯一变化的是如何判断标记棋盘上已有皇后的占领区域,或者说是下一个皇后可以放置的位置区域。它采用了 3 个集合来降低之前算法进行标记的时间复杂度。
将之前的二维数组表示皇后位置转化为只用一个数组 queens 来表示,比如对于 queens[0]=1 ,表示在第0行第1列放置一个皇后。一旦 queens 数组被填满元素,那么我们就得到了一个放置结果。
当在 queens 数组放置新元素时(即在下一行填充新的皇后位置时):因为肯定是不同行,所以需要关心的是新皇后位置和已有的皇后位置是否在相同列;和已有的皇后位置是否在同一条左上右下斜线上;和已有的皇后位置是否在同一条右上左下斜线上。而 3 个集合的功能就是用来分别判断这三种情况的。
columns 数组保存已有皇后的列下标(判断 columns 中是否已经包含了新位置的列下标)
diagonals1 数组保存已有皇后的左上右下斜线位置,比如位置(1,2),由于经过(1,2)的此斜线上所有点(x,y)满足" 1-2 ==
x-y “关系,那么这一条斜线就可以用 -1(1-2) 来表示,即保存 -1 到diagonals1 中(判断 diagonals1 是否已经包含的新位置所在左上右下斜线,即新位置的行下标减去列下标的值)
diagonals2 数组保存已有皇后的右上左下斜线位置,比如位置(1,2),由于经过(1,2)的此斜线上所有点(x,y)满足” 1+2 ==
x+y "关系,那么这一条斜线就可以用 3(1+2) 来表示,即保存 3 到diagonals2 中(判断 diagonals2 是否已经包含的新位置所在左上右下斜线,即新位置的行下标加上列下标的值)
如下面算法所示:(每行代码的功能已经注明,可参考理解)
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions=new ArrayList<>();
int[] queens=new int[n]; //保存每一行中成功放置皇后的下标。
Arrays.fill(queens,-1);
Set<Integer> columns=new HashSet<>();//记录每一列是否可以放置新的皇后
Set<Integer> diagonals1=new HashSet<>();//记录左上右下斜线上是否可以放置新的皇后
Set<Integer> diagonals2=new HashSet<>();//记录右上左下斜线上是否可以放置新的皇后
backtrack(solutions,queens,n,0,columns,diagonals1,diagonals2); // row 表示已经放置了多少行的皇后
return solutions;
}
public void backtrack(List<List<String>> solutions,int[] queens,int n,int row,Set<Integer> columns,
Set<Integer> diagonals1,Set<Integer> diagonals2){
if(row==n){
List<String> board=generateBoard(queens,n);
solutions.add(board);
}
else{
for(int i=0;i<n;i++){
//上述三个判断语句如果有任意一个为 true,即表示当前位置(row,i)不能被放置新的皇后,所以需要跳过。
//并且使用 Set 来存储,可以顺带着去重。比如columns中不同的两行但是列下标相同,就没必要存储了;diagonals1中同一条斜线上的元素也没必要存储了...
if(columns.contains(i))
continue;
int diagonal1=row-i;//左上右下一条斜线上元素的行下标与列下标之差相等。
if(diagonals1.contains(diagonal1))
continue;
int diagonal2=row+i;//右上左下一条斜线上元素的行下标与列下标之和相等。
if(diagonals2.contains(diagonal2))
continue;
queens[row]=i; //此时在位置(row,i)上找到了一个可以放置新皇后的位置
columns.add(i);//将该列下标加入columns。
diagonals1.add(diagonal1);//将 (row,i) 表示的对应方向的斜线加入 diagonals1
diagonals2.add(diagonal2);//将 (row,i) 表示的对应方向的斜线加入 diagonals2
backtrack(solutions,queens,n,row+1,columns,diagonals1,diagonals2); //回溯判断每一种放置的可能性。
queens[row]=-1; //恢复到在此位置放置新皇后之前的状态。
columns.remove(i);
diagonals1.remove(diagonal1);
diagonals2.remove(diagonal2);
}
}
}
public List<String> generateBoard(int[] queens,int n){ //将已经找到符合条件的 queens 数组转换为标准的返回值(List类型)
List<String> board=new ArrayList<>();
for(int i=0;i<n;i++){
char[] row=new char[n];
Arrays.fill(row,'.');
row[queens[i]]='Q';
board.add(new String(row));
}
return board;
}
目前来看,这道题的整体思路和代码框架是很容易实现的,关键的问题在于如何描述新皇后的可选位置区域这块,并尽可能地降低时间复杂度。