八皇后问题,其实早在 [15. N-Queens II](../15. N-Queens II) 中就碰上了。更早还可以追溯到 CoolShell 谜题的第七关。
这个如此有名的问题,在我做完80多道 LeetCode 面试题之后,也基本变成了一道基础题了。
本质就是回溯。回溯需要知道时机,不符合,才需要回溯。那么我想来想去,这道题的考点就在于如何判断不符合。
! ! ! *
! Q ! *
! ! ! *
* * * !
我以四皇后为例,如果 Q 所在位置为 (1,1),那么 !
所表示的位置,即为皇后可以吃掉的位置。所以我最初的思路非常直接暴力。
第一行肯定是随便放 Q 的,那么放一个 Q ,就能得到这么多 !
,到下一行,可选的范围便大大的减小,什么时候放弃呢?当有一行全是 !
的时候放弃。
我现在还相信,按照这种思路做出来一点问题也没。但难点在于遇到不符合 —— 这里就是遇到一行全是 !
的时候,该如何做? 具备可回溯性吗? 我画出来的 !
能恢复成上一步吗? 显然是非常困难的。
可以回忆一下我们遇到的回溯,甚至动态规划问题,我们的解法都会留出退路。回溯就更加明显,所谓试错,是指你错了,还能 Ctrl + Z,试想如果不能回退,你错了就是错了,无路可走了。
那么,如何留出退路,让我们可以回去呢?一个基本的思维模式就是,“以下判上”,怎么讲?这道题我们判断的顺序显然是从第一行开始,直到最后一行,力求将所有皇后放下。
那么,“以下判上”,就是希望在进行第二行的时候,**试着将该行的每一列放一个 Q,看她会不会吃掉 上一行的 Q **. 这样的回退是非常方便的,如果会吃掉,我不放 Q 就是了。
由于我只关注试错时,上面行的情况,所以我重新绘制了一张图:
! ! ! * | * ! * !
* Q * * | ! ! ! *
* * * * | * Q * *
* * * * | * * * *
当我试图在 (1,1) 位置放一个 Q 的时候,!
所在的位置,都是会被这个 Q 吃掉的位置。那么如果第一行的 Q 在这三个 !
所处的位置上,则不能放置!
写一个函数:
bool isValid(int row, int col, const vector<int> &queens) { // 要试的位置 (row, col),以及已经存放了 Q 的位置,用 vector<int> 来存储,坐标为行,值为列。
for (int queen_col=0, r=row-1, lc=col-1, rc=col+1; r>=0; --r, --lc, ++rc) // r 是要检查的行, lc (left col) 是左侧的列, rc (right col) 是右侧的列,这三个标记组成了上面左图中`!`的位置。
{
// queen_col 是要检查的这一行,Q 所在的列。
queen_col = queens[r];
if (queen_col == col || queen_col == lc || queen_col == rc) return false; // 只要 queen_col 与上面推测出 `!` 的所在位置一致,则不符合,返回。
}
return true; // 若所有要检查的行都通过了测试,则证明有效。
}
这个核心的函数有了之后,我们只需要考虑如何将完整的回溯过程描述出来即可。为了更通俗的表述思路,我运用递归 + DFS 的思想来表述回溯过程。
void solveNQueens(int row, vector<int> queens) { // 按行向下,想想为何是深搜?因为我第一行随便放个 Q , 我不把所有行走完,根本不知道这个 Q 放的对不对。这个按行向下,简单讲,类似二叉树的深度遍历。
int size = queens.size(); // 节约时间复杂度,仅计算一次长度,或可以直接传入 n
if (row == size) { // DFS 到底了,正好是尾递归的出栈条件
vector<string> solution(size, string(size, '.')); // 构建输出格式,即解决方案的棋盘
for (int r=0; r<size; ++r) // 按行给 Q
solution[r][queens[r]] = 'Q';
ret.push_back(solution);
} else for (int col=0; col<size; ++col) { // 试错过程
queens[row] = col;
if (isValid(row, col, queens)) solveNQueens(row+1, queens); // 只有没错的,才进入更深的一层,错了就再试下一个地方放 Q
}
}
整个解决方案,不过寥寥几行,但是仗了 DFS 递归的好处。是否理解透彻了呢?这个解法效率很高,差一点就是最高效率,什么?想知道最高效率的解法?用 bitset 吧,请回去看看第 15 题的解法,思路完全一样。