八皇后问题是一个古老的问题,于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
具体的代码为:
$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
可见优化还是有效果的