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

八皇后问题

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

问题分析

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

解决方法

使用递归回溯来解决

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

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

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

输出格式

类似下面的格式结果,可以看做是一个棋盘,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

代码

具体的代码为:



$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) . '种解法:
'
; foreach ($this->result as $k=>$v) { echo "
输出第"
. ++$k . "个结果 :
"
; for ($i = 0; $i < $this->MAX_NUM; $i++) { for ($j = 0; $j < $this->MAX_NUM; $j++) { echo $v[$i][$j] . "   "; } echo "
"
; } } } } }

运行:

一共有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为求绝对值的函数

最终的代码为:



$obj = new EightQueen(8);
// 获取所有解法的皇后位置
$result = $obj->getResult();
// 输出所有解法的棋盘格子
PrintChessBoard($result);
/**
 * 按照0和1来输出棋盘的格式
 * @param $array
 */
function PrintChessBoard($array)
{
      
    $k = 0;
    echo '一共有' . count($array) . '种解法:

'
; foreach ($array as $v) { echo "输出第" . ++$k . "个结果:
"
; foreach ($v as $row) { for ($i = 0; $i < count($v); $i++) { if ($row == $i) { echo "1   "; } else { echo "0   "; } } echo "
"
; } echo "
"
; } } /** * 八皇后问题 */ 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; } }

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



$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

可见优化还是有效果的

你可能感兴趣的:(PHP,数据结构与算法,八皇后,PHP,算法)