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


    八皇后问题

    八皇后问题是一个古老的问题,于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

    可见优化还是有效果的

  • 相关阅读:
    Linux系统常见的压缩与打包
    java 语言规范 java language specifications
    java 枚举
    github邮箱验证技巧
    关于 python
    博客园 编程基础 精华
    fiddler
    一个牛人写的博客
    使用xmarks同步 chrome ie firefox safari书签
    linux 中的 tar 解压
  • 原文地址:https://www.cnblogs.com/linewman/p/9918097.html
Copyright © 2020-2023  润新知