题目:
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例:
输入: 4 输出: [ [".Q..", // 解法 1 "...Q", "Q...", "..Q."], ["..Q.", // 解法 2 "Q...", "...Q", ".Q.."] ] 解释: 4 皇后问题存在两个不同的解法。
思路:在看到这类题目的时候,首先想到的就是:图搜索。这里我使用的就是图搜索,使用回溯算法进行解答。而在图搜索中选择深度优先搜索的难点主要在于以下几点:
1、结束时,记录返回结果
2、抉择之后对应状态的改变
3、还原现场
其中对于困难点1,我们可以通过建立全局变量List> 对象来进行填充,而dfs()的返回值可以置为void。
对于困难点2是有一点麻烦的。我们可以通过对列进行搜索,每次dfs的时候将列+1(idx + 1),但是对于行,和斜线,我们需要专门考虑,为此我们可以再设立几个全局的数组来记录对应的行和斜线的状态。
困难点3,这个点是容易忽视掉的,类似于深搜和递归等,在改调用的方法结束的时候记得要还原现场,将状态还原,避免影响之后的操作·。
以下是伪代码:
List> ans = new ArrayList>();
int[] path = new int[n];
void dfs(int idx,int n)// idx当前遍历的列,n表示棋盘的边长
if(idx >= n) // 边界条件,当搜索到边界的时候,也就是改输出结果的时候
// 记录结果 并结束
return ;
for(i...n)//对于每一列,皇后所在的行都是未知的、
if(状态是否符合搜索条件) //剪枝判断
path[idx] = i; //表示该列所对应的行
//记录状态
dfs(idx+1,n);
//还原状态
public List> solveNQueens(int n){
dfs(0,n);
return ans
}
观上面的伪代码,可以知道主要难点在于剪枝判断和记录结果。其实记录结果还是很简单的,只需要遍历整个棋盘,记录该列所对应的行并标记为Q就行。所以,对于整个算法的难点就落在了剪枝操作和记录状态上面了。因为剪枝操作其实就是判断对应的状态,所以所有的难点也就是记录状态。
记录状态的难点:由于皇后可以走八个方向,四条直线。因此可以归结为一下四种情况
1、行
2、列
3、正斜线
4、反斜线
由于我们的搜索是以列展开的,因此列这条直线可以直接省去。而行的考虑还是比较简单的,我们只需要在记录状态的时候标记改行已经被占用就可以了。可以定义一个全局变量rows[i] = true,来表示改行是否被占用,记得之后要还原现场。
对于正斜线和反斜线,我们就要仔细考虑了。我们可以参照行进行操作。由于这个状态的记录主要是表示那些未知被皇后占用了。所以我们可以将斜线也类似于行一样标记出来。对于一个n*n的棋盘来说正斜线和反斜线的数量都是 2n-1。而我们想要将被占用的斜线表示出来,可以才用数学中的思维来进行解决,构件坐标轴如下:
虽然丑了点,不过可以看= =。我们将正斜线作为x+y=k固定值,那么对于不同的k值,即可以表示为不同的正斜线。那么对于正斜线的全局变量slash数组的下标可以记为slash[x+y],即slash[idx + i] = true;而反斜线的方程是-x+y = k。同理可以记为backlash[idx-i] = true;不过idx - i可能是负数,所以我们要将其设置为正数,不然下标就出错了,因此可以定为backlash[idx-i+n-i] = true;记录完状态之后,再调用dfs之后需要还原现场。
由以上的思考方式最后可以得到最后的代码为以下:
List> ans = new ArrayList>();
int[] row;
boolean[] line;
boolean[] slash;
boolean[] backlash;
/**
* 对列进行深度优先搜索
* @param idx
* @param n
*/
void dfs(int idx,int n){
//结束条件 记录结果
if(idx >= n){
List tmp = new ArrayList();
for(int i = 0 ; i < n ; i++){
String str = "";
for(int j = 0 ; j < n ; j++){
if(row[i] == j){
str +="Q";
}else{
str +=".";
}
}
tmp.add(str);
}
ans.add(tmp);
return ;
}
for(int i = 0 ; i < n ; i++){
if(!line[i] && !slash[idx + i] && !backlash[idx-i+n-1]){
row[idx] = i;
line[i] = true;
slash[i+idx] = true;
backlash[idx-i+n-1] = true;
dfs(idx + 1 , n);
line[i] = false; // 还原现场
slash[i+idx] = false;
backlash[idx-i+n-1] = false;
}
}
}
public List> solveNQueens(int n) {
row = new int[n];
line = new boolean[n];
slash = new boolean[2*n];
backlash = new boolean[2*n];
dfs(0,n);
return ans;
}
这道题目还是挺有意思的,主要是很有做的意义。在刚刚拿到题目的时候感觉考虑的点有很多,但是在一步一步解析之后,再看这道题目,就会发现并没有多难。而且这道题目很具有代表性,可以说是图的深搜的代表了。笔者目前也是慢慢在学习图的相关算法,解题思路比较简陋,代码还有很多可以优化的点。欢迎大家在阅读完之后,如果有什么想法和意见的话,能够及时告诉我,我好及时修正。