【八皇后问题】递归回溯法【原创】

八皇后问题

八皇后问题是一个古老的问题,于1848年由一位国际象棋棋手提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,如何求解?八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当 n = 1 或 n ≥ 4 时问题有解。
《【八皇后问题】递归回溯法【原创】》

问题分析

  • 满足上述条件的八个皇后,必然是每行一个,每列一个
  • 棋盘上任意一行、任意一列、任意一条斜线上都不能有两个皇后

解决方法

使用递归回溯来解决

所谓递归回溯,本质上是一种枚举法。这种方法从棋盘的第一行开始尝试摆放第一个皇后,摆放成功后,递归一层,再遵循规则在棋盘第二行来摆放第二个皇后。如果当前位置无法摆放,则向右移动一格再次尝试,如果摆放成功,则继续递归一层,摆放第三个皇后……

如果某一层看遍了所有格子,都无法成功摆放,则回溯到上一个皇后,让上一个皇后右移一格,再进行递归。如果八个皇后都摆放完毕且符合规则,那么就得到了其中一种正确的解法,保存起来,继续下一种解法的寻找。

解决八皇后问题,可以分为两个层面:
* 找出第一种正确摆放方式,也就是深度优先遍历
* 找出全部的正确摆放方式,也就是广度优先遍历

输出格式

类似下面的格式结果,可以看做是一个棋盘,0表示没有放置皇后,1表示放置皇后

1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0
0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 0

代码

具体的代码为:

<?php $obj = new EightQueen(8); // 输出所有棋盘的格子 $obj->printOut(); /** * 八皇后问题 */ class EightQueen { // 棋盘格子的范围/皇后的数量 private $MAX_NUM; // 二维数组作为棋盘,二维数组的第一个维度代表横座标,第二个维度代表纵座标,并且从0开始。比如chessBoard[3][4]代表的是棋盘第四行第五列格子的状态 private $ChessBoard; // 所有的正确棋牌解法 private $result = []; public function __construct($max_num) { // 初始化棋盘的格子范围/皇后的数量 $this->MAX_NUM = $max_num; // 小于3x3的棋盘是无解的 if ($max_num >= 4) { // 初始化棋盘,所有的格子(MAX_NUM x MAX_NUM)都为0,表示格子未放置 $this->ChessBoard = array_fill(0, $this->MAX_NUM, array_fill(0, $this->MAX_NUM, 0)); // 从第一层开始递归摆放皇后 $this->settleQueen(); } } /** * 检查落点是否符合规则(未放置棋子即符合规则) * @param $x int 横座标 * @param $y int 纵座标 * @return bool */ private function check($x, $y) { // 从第一层开始检查,从上到下进行每一层检查 for ($i = 0; $i < $y; $i++) { // 纵向检查,对每一层的$x位置进行检查,如果每一层的$x位置已经有放置棋子的话则返回false,比如检查(5,3)这个格子的话,则这里是检查(5,0)(5,1)(5,2)这三个点 if ($this->ChessBoard[$x][$i] == 1) { return false; } // 检测左侧斜向,比如检查(5,3)这个格子的话,则这里是检查(4,2)(3,1)(2,0) if ($x - 1 - $i >= 0 && $this->ChessBoard[$x - 1 - $i][$y - 1 - $i] == 1) { return false; } // 检测右侧斜向比如检查(5,3)这个格子的话,则这里是检查(6,2)(7,1) if ($x + 1 + $i < $this->MAX_NUM && $this->ChessBoard[$x + 1 + $i][$y - 1 - $i] == 1) { return false; } } return true; } /** * 从$y层(纵层)开始往下每一层递归摆放皇后,一旦找到一种解法就保存到result中去,然后继续找下一种解法 * @param $y int 纵座标 */ private function settleQueen($y = 0) { // 行数超过棋盘的范围,说明已经找到一种解法了,保存到result里面去 if ($y == $this->MAX_NUM) { // 保存正确的棋牌解法 $this->result[] = $this->ChessBoard; } // 遍历当前行,从左到右逐一格子进行验证 for ($i = 0; $i < $this->MAX_NUM; $i++) { // 为当前行的每个格子清零,以免在回溯的时候出现脏数据 for ($x = 0; $x < $this->MAX_NUM; $x++) { $this->ChessBoard[$x][$y] = 0; } // 检查是否符合规则(未放置棋子即符合规则),如果符合,更改元素值并进一步递归 if ($this->check($i, $y)) { $this->ChessBoard[$i][$y] = 1; // 递归下层 $this->settleQueen($y + 1); } } } /** * 输出所有正确棋盘解法 */ public function printOut() { // 小于3x3的棋盘是无解的 if ($this->MAX_NUM < 4) { echo '小于3x3的棋盘是无解的'; } else { echo '一共有' . count($this->result) . '种解法:<br />'; foreach ($this->result as $k=>$v) { echo "<br />输出第" . ++$k . "个结果 :<br />"; for ($i = 0; $i < $this->MAX_NUM; $i++) { for ($j = 0; $j < $this->MAX_NUM; $j++) { echo $v[$i][$j] . "&nbsp;&nbsp;&nbsp;"; } echo "<br />"; } } } } }

运行:

一共有92种解法:

输出第1个结果:
1   0   0   0   0   0   0   0   
0   0   0   0   1   0   0   0   
0   0   0   0   0   0   0   1   
0   0   0   0   0   1   0   0   
0   0   1   0   0   0   0   0   
0   0   0   0   0   0   1   0   
0   1   0   0   0   0   0   0   
0   0   0   1   0   0   0   0   

输出第2个结果:
1   0   0   0   0   0   0   0   
0   0   0   0   0   1   0   0   
0   0   0   0   0   0   0   1   
0   0   1   0   0   0   0   0   
0   0   0   0   0   0   1   0   
0   0   0   1   0   0   0   0   
0   1   0   0   0   0   0   0   
0   0   0   0   1   0   0   0   
......

优化

由于我是用三维数组result来存储所有的棋盘,用二维数组ChessBoard来存储单个正确的棋盘,未放置皇后的用0表示,放置皇后的用1表示,一方面造成空间的浪费,一方面在循环的时候可能会影响性能,打印result可以发现这个数组很大:

Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => 1
                    [1] => 0
                    [2] => 0
                    [3] => 0
                    [4] => 0
                    [5] => 0
                    [6] => 0
                    [7] => 0
                )

            [1] => Array
                (
                    [0] => 0
                    [1] => 0
                    [2] => 0
                    [3] => 0
                    [4] => 0
                    [5] => 0
                    [6] => 1
                    [7] => 0
                )

            [2] => Array
                (
                    [0] => 0
                    [1] => 0
                    [2] => 0
                    [3] => 0
                    [4] => 1
                    [5] => 0
                    [6] => 0
                    [7] => 0
                )
......

其实0是没必要存储的,只需要存储每一行皇后的位置即可,比如上面的代码中第三个数组表示皇后在第一行第一列,第二行的皇后位置是在第七列,第三行的皇后在第五列,就可以用下面的来表示:

Array
(
    [0] => Array
        (
            [0] => 0
            [1] => 6
            [2] => 4
            ....
        )
...

也就是result按照皇后的位置来存储,这样的话就可以将为二维数组即可,另外,棋盘的初始化也不需要了。

另外由于改成存储皇后的位置了,所以判断棋盘落点是否符合规则的方法需要更改了,之前的方法里面是分成两步来判断对角线的,分别判断左对角线和右对角线,这里可以统一来判断,经过分析得到,设两个不同的皇后分别在j,k行上,x[j],x[k]分别表示在j,k行的那一列上。那么不在同一对角线的条件可以写为abs((j-k))!=abs(x[j]-x[k]),其中abs为求绝对值的函数

最终的代码为:

<?php $obj = new EightQueen(8); // 获取所有解法的皇后位置 $result = $obj->getResult(); // 输出所有解法的棋盘格子 PrintChessBoard($result); /** * 按照0和1来输出棋盘的格式 * @param $array */ function PrintChessBoard($array) { $k = 0; echo '一共有' . count($array) . '种解法:<br /><br />'; foreach ($array as $v) { echo "输出第" . ++$k . "个结果:<br />"; foreach ($v as $row) { for ($i = 0; $i < count($v); $i++) { if ($row == $i) { echo "1&nbsp;&nbsp;&nbsp;"; } else { echo "0&nbsp;&nbsp;&nbsp;"; } } echo "<br />"; } echo "<br />"; } } /** * 八皇后问题 */ class EightQueen { // 棋盘格子的范围/皇后的数量 private $MAX_NUM; // 二维数组作为棋盘,二维数组的第一个维度代表横座标,第二个维度代表纵座标,并且从0开始。比如chessBoard[3][4]代表的是棋盘第四行第五列格子的状态 private $ChessBoard; // 所有的正确棋牌解法 private $result = []; public function __construct($max_num) { // 初始化棋盘的格子范围/皇后的数量 $this->MAX_NUM = $max_num; // 小于3x3的棋盘是无解的 if ($max_num >= 4) { // 从第一层开始递归摆放皇后 $this->settleQueen(); } } /** * 检查落点是否符合规则(未放置棋子即符合规则) * @param $n int 纵座标即行数 * @return bool */ private function check($n) { // 从第一层开始检查,从上到下进行每一层检查 for ($i = 0; $i < $n; $i++) { // 纵向检查、对角线检查 if ($this->ChessBoard[$i] == $this->ChessBoard[$n] || abs($this->ChessBoard[$i] - $this->ChessBoard[$n]) == ($n - $i)) { return false; } } return true; } /** * 从$y层(纵层)开始往下每一层递归摆放皇后,一旦找到一种解法就保存到result中去,然后继续找下一种解法 * @param $y int 纵座标 */ private function settleQueen($y = 0) { // 行数超过棋盘的范围,说明已经找到一种解法了,保存到result里面去 if ($y == $this->MAX_NUM) { // 保存正确的棋牌解法 $this->result[] = $this->ChessBoard; } // 遍历当前行,从左到右逐一格子进行验证 for ($i = 0; $i < $this->MAX_NUM; $i++) { $this->ChessBoard[$y] = $i; // 检查是否符合规则(未放置棋子即符合规则),如果符合,更改元素值并进一步递归 if ($this->check($y)) { // 递归下层 $this->settleQueen($y + 1); } } } /** * 输出所有正确棋盘解法 */ public function getResult() { return $this->result; } }

加上时间消耗方法来查看所消耗的时间:

<?php $time_start = microtime_float(); $obj = new EightQueen(10); // 获取所有解法的皇后位置 $result = $obj->getResult(); // 输出所有解法的棋盘格子 PrintChessBoard($result); $time_end = microtime_float(); $time = $time_end - $time_start; var_dump($time);

优化前的代码运行十皇后所消耗的时间差不多为:3.24 s

优化后的代码运行十皇后所消耗的时间差不多为:2.39 s

可见优化还是有效果的

点赞