八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。
上图就是八皇后问题的其中一种解法,八皇后问题采用回溯算法来解决,如果读者不能很好的理解递归的话,可能理回溯算法很吃力,回溯算法的核心思想类似于深度优先搜索,如果当前结点可行,就搜索当前结点的一个子结点,反之,就回溯,搜索当前结点的子结点。递归所生成的树是一棵深度优先搜索树,叶子结点是那些不合法的结点(换句话说,就是无法向下递归的结点)和答案结点。
我们来模拟一遍4皇后问题,便于读者理解。
对于第一行: 对于第二行: 对于第三行: 对于第四行:
(1,1) a (2,1) e (3,1) i (4,1) m
(1,2) b (2,2) f (3,2) j (4,2) n
(1,3) c (2,3) g (3,3) k (4,3) o
(1,4) d (2,4) h (3,4) l (4,4) p
一开始我们先放第一行的第一个位置(1,1)执行a,
然后放第二行,因为放第一列(2,1)会和(1,1)列冲突,放第二列(2,2)会和(1,1)对角线冲突,所有只能将皇后放在(2,3) 执行g
接下来放第3行,放第一列(3,1)和(1,1)列冲突,放第二个列(3,2)会和(2,3)对角线冲突,放第三列(3,3)会和(2,3)列冲突,放第四列(3,4)和(2,3)冲突,也就是说第三行无论怎么放皇后都会冲突,所以上一个皇后放的位置不合法,我们就回溯,上一个皇后放的位置是(2,3),现在我们拿掉(2,3)那个位置的皇后,放到(2,4)执行h。
因为回溯,所以我们现在又要放第三行的皇后,前面只放了(1,1)和(2,4),现在放第三行的皇后,放第一列(3,1)和(1,1)列冲突,所以我们现在将皇后放在第二列(3,2)执行j
现在放第4行,前面放了(1,1)、(2,4)、(3,2)。放在第一列(4,1)会和(1,1)列冲突,放第二列(4,2)和(3,2)列冲突,放第三列(4,3)会和(3,2)对角线冲突,放第4列会和(2,4)列冲突。所以第4行没有合法的位置,也就是说上一个放的皇后位置不对,我们就回溯,拿走上一个皇后(3,2)。
还是放第三行,前面只放了(1,1)、(2,4)。放第三列(3,3)会和(1,1)对角线冲突,放第四列(3,4)和(2,4)列冲突,也就是说上一次放的皇后不对,回溯,拿走上一个皇后(2,4),此时第二行已经没有位置合法了,所以再回溯,拿走上上一个皇后(1,1),所以把皇后放在第一行第一列,后面无论怎么放都不合法,所以我们考虑将皇后放在(1,2),执行b。
接下来放第二行的皇后,前面放了(1,2),放第一列(2,1)和(1,2)对角线冲突,放第二列(2,2)和(1,2)列冲突,放第三列(2,3)和(1,2)对角线冲突,所以将皇后放在第4列(2,4),执行h。
接下来放第三行的皇后,前面放了(1,2)和(2,4),放第一列(3,1)合法,执行 i 。
现在放第四行的皇后,前面放了(1,2)、(2,4)、(3,1)。放第一列(4,1)和(3,1)列冲突,放第二列(4,2)和(2,4)对角线冲突,放第三列(4,3)合法,执行o,此时已经放完4行的皇后,所以为一个可行解(1,2)、(2,4)、(3、1)、(4,3)
读者可以模仿上面的过程推出另外一组解(1,3)、(2、1)(3、4)、(4、2)。直到考虑完所有的子结点,才算结束。
因为笔者今天写这篇博客的时候去图书馆没带草稿本,所以后期有时间补上上面模拟过程的图。
下面给出八皇后问题的2种代码
1:第一种代码可能读者看不出来回溯的过程在哪里,其实类似于一棵深度优先搜索树,当前结点不合法时,就不在向下递归,而是回到上一个调用这个函数的那个函数,也就是当前结点的父节点。
#include
using namespace std;
const int maxn=100+7;
int c[maxn];
int n;
int tot;
void search(int cur) //放第cur个皇后
{
if(cur==n){ //如果放了n个就答案+1
tot++;
}
else for(int i=0;i
2:第二种代码很容易看出来回溯过程,第二种代码的运行速度更快,因为设置了一个标志数组vis[3][maxn]。
vis[0][i]:用来判断是否列冲突,这个很好理解,如果前面的皇后放在了第cur行,vis[0][cur]就会被标记。
读者可能需要画两个图来理解下面两个数组,笔者后期补上,其实笔者认为也可以用借助一次函数来理解。
主对角线的一次函数为:y=x+c 所以y-x=c为一个定值,所以我们可以用y-x+n来标记那条对角线,为什么要加n呢,因为y-x可能小于0,超出了数组的下标。
副对角线的一次函数为:y=-x+c 所以y+x=c为一个定值,所以我们用y+x来标记那条对角线。
vis[1][cur+i]:副对角线
vis[2][cur-i+n]: 主对角线
#include
using namespace std;
const int maxn=100+7;
int c[maxn];
int vis[3][maxn];
int n;
int tot;
void NQueen(int cur) //考虑放第cur行的皇后
{
if(cur==n){ //如果已经放了n个皇后 答案+1
tot++;
}
else for(int i=0;i