如何利用栈解决八皇后问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/young2415/article/details/85243430

追本溯源,什么是八皇后问题?

什么是八皇后问题?八皇后问题(Eight queens puzzle)起源于国际象棋,在国际象棋中,皇后的势力范围是它所在的行、列以及对角线,八皇后问题就是如何在一个 8×8 的棋盘上放置八个皇后,使得这八个皇后不能互相攻击(怎么听起来像是后宫戏啊)。换句话说就是,任何一个皇后都不会与其他皇后在同一行、同一列或者同一条对角线上。

八皇后问题是一个古老而又著名的问题,提出这个问题的并不是一个皇帝,而是由国际象棋棋手马克思·贝瑟尔于1848年提出。该问题一公布,立刻引起了数学家们的兴趣。1850年,弗朗兹·诺克公布了该问题的第一个解,并首次将八皇后问题扩展至 N 皇后问题,即在 N×N 的棋盘上放置 N 个皇后,使其不能相互攻击。

从那以后,包括高斯在内的许多数学家都研究过八皇后问题及其推广版本 N 皇后问题。1874年,S.冈德尔提出了一个通过行列式来求解的方法,这个方法后来又被J.W.L.格莱舍加以改进。1972年,Dijkstra 以这个问题为例说明来说明他提出的结构性编程的能力。他发表了一篇关于深度优先回溯算法的非常详细的描述。

而本文正是基于大神 Dijkstra 的回溯算法来向读者朋友们介绍 N 皇后问题的解法。

你的第一反应?

当你看完问题描述时,你的第一反应是什么?反正我首先想到的就是暴力求解法。所谓暴力求解,就是不管三七二十一,怼就完了。你不是在 8×8 的棋盘上放置八个皇后吗?行,那我就把所有的放置方案穷举出来,然后一一考查,把不符合要求的方案踢掉,那么剩下的不就是问题的解了吗?思路简单清晰,看似没毛病,很黄。。。咳咳,不对,很暴力。

但是仔细分析一下,我们就会发现,在 8×8 的棋盘上放置八个皇后,总共有 4, 426, 165, 368(也就是 C 64 8 C^{8}_{64} )种放置方案。暂且不考虑如何验证每个方案的正确与否,单是遍历这 44 亿个方案,恐怕就要等很长时间。所以这种暴力求解法虽然逻辑上说得通,但是在实践中是没有可行性的。

可能有人会说了,何不改进一下?我们不需要八个皇后在棋盘上的所有排列组合,既然皇后之间不能相互攻击,那么每行只能放一个皇后,这样的话,候选方案的数量就减少至 16, 777, 216(即 8 8 8^8 )。再进一步优化,不难发现,棋盘的每一列也只能放一个皇后,所以候选方案的数量又进一步减少至 40, 320(即 8!)。

现在看起来似乎经过优化的暴力求解有了一定的可行性,它变得和蔼可亲了,不再那么面目狰狞了。但是,别着急,事情还没完。

主角登场

在前面优化后所得到的 40, 320 个候选方案中,依然存在大量的冗余。比如说,看下面这张图,蓝色方块表示已经放好的皇后,在我们放第 6 个皇后时,会发现无论放到哪里它都会被攻击。在这种情况下,后续第 7 个、第 8 个皇后的试探都必将是徒劳的。然而,在这 4 万多个候选方案中,就有大量的方案做了这种无用功。所以说,虽然我们已经将候选方案从 44 亿优化到了 4 万,但是仍然具有很大的改进空间。

正确的做法是,当我们发现第 k(1 <= k <= N) 个皇后摆放在任何位置都会造成冲突时,应该回溯到第 k-1 个皇后,改变第 k-1 个皇后的位置,再继续向下试探。这就是所谓的试探回溯法。

举个例子

简单起见,我们将问题规模减小到 N=4,即四皇后问题,看一下如何用试探回溯法解决四皇后问题。

首先试探第一行皇后,如图 (a) 所示将其暂置于第 1 列,同时列号入栈。接下来再试探第二行皇后,如图 (b) 所示在排除前两列后,将其暂置于第 3 列,同时列号入栈。然而此后试探第三行皇后时,如图 © 所示发现所有列均有冲突。于是回溯到第二行,并如图 (d) 所示将第二行皇后调整到第 4 列,同时更新栈顶列号。后续各步原理相同,直至图 (l) 所有列均放置上皇后,得到一个全局解。

同理,这个方法也适用于八皇后乃至 N ( N >= 4 ) 皇后问题。而如果采用该方法来解决八皇后问题,试探的次数将减少至 13664 次。注意,这是试探的次数,每放置一个皇后就算一次试探,而我们之前讨论的暴力求解法,是将候选方案优化到 40320 个,而要放置 8 个皇后才构成一个候选方案。算法的威力可见一斑。

代码

要实现这样一个试探回溯算法,栈是必不可少的,因为要用栈来记录已经放置到棋盘上的皇后的状态,便于回溯。其次,皇后是组成棋局和最终解的基本单元,因此需要实现对应的 Queen 类。具体代码如下:

class Queen {
public:
	int x, y; //皇后在棋盘上的位置坐标
	Queen(int xx = 0, int yy = 0) : x(xx), y(yy) {};
	bool operator==(Queen const& q) const { //重载判等操作符,以检测不同皇后之间可能的冲突
		return (x == q.x) //行冲突,因为限制每行只能放一个皇后,所以这一情况并不会发生,可省略
			|| (y == q.y) //列冲突
			|| (x + y == q.x + q.y) //沿正对角线冲突
			|| (x - y == q.x - q.y); //沿反对角线冲突
	}
	bool operator!= (Queen const& q) const { //重载不等操作符
		return !(*this == q);
	}
};

有了皇后类以后,就可以写核心代码了:

void placeQueens ( int N ) { //N皇后算法(迭代版):采用试探/回溯的策略,借助栈记录查找的结果
   Stack<Queen> solu; //存放(部分)解的栈
   Queen q ( 0, 0 ); //从原点位置出发
   Vector<Queen> solu_vector;

   do { //反复试探、回溯
      if ( N <= solu.size() || N <= q.y ) { //若已出界,则
         q = solu.pop(); q.y++; //回溯一行,并继续试探下一列
      } else { //否则,试探下一行
         while ( ( q.y < N ) && ( 0 <= solu.find ( q ) ) ) //通过与已有皇后的比对
            /*DSA*///while ((q.y < N) && (solu.find(q))) //(若基于List实现Stack,则find()返回值的语义有所不同)
            { q.y++; nCheck++; } //尝试找到可摆放下一皇后的列
         if ( N > q.y ) { //若存在可摆放的列,则
            solu.push ( q ); //摆上当前皇后,并
			if (N <= solu.size()) {
				nSolu++; //若部分解已成为全局解,则通过全局变量nSolu计数
				displaySolution(solu.toVector()); //打印结果
			}
            q.x++; q.y = 0; //转入下一行,从第0列开始,试探下一皇后
         }
      }/*DSA*/
   } while ( ( 0 < q.x ) || ( q.y < N ) ); //所有分支均已或穷举或剪枝之后,算法结束
}

还能改进吗?

其实在理解了试探回溯法的原理之后,八皇后问题乃至 N 皇后问题还可以有更精简的解决方法。甚至不用借助于栈,只用一个数组就可以搞定。代码如下:

#pragma once
#include "display.h"

int nSolu = 0; //解的总数
int nCheck = 0; //尝试的总次数

/*判断第 k 个皇后是否与之前的 k-1 个皇后冲突(既可以互相攻击)
  如果与任一皇后冲突,则返回 true,否则返回false*/
bool conflict(int k, int* solu) {
	for (int i = 0; i < k; i++) {
		if (solu[k] == solu[i] || (k + solu[k]) == (i + solu[i]) || (k - solu[k]) == (i - solu[i])) {
			return true;
		}
	}
	return false;
}

/*利用纯数组解决 N 皇后问题*/
void placeQueensWithPureArray(unsigned int N) {
	int* solu = new int[N];
	int size = 0;
	solu[0] = 0; //从原点位置出发
	do { //反复试探、回溯
		if (N <= size || N <= solu[size]) { //若以出界
			size--;	solu[size]++; //回溯一行,并继续试探下一列
		}else { //否则,试探下一行
			while (solu[size] < N && conflict(size, solu)) { //通过与已有皇后的比对
				solu[size]++; //尝试找到可摆放下一皇后的列
				nCheck++;
			}
			if (solu[size] < N) { //若存在可摆放的列
				size++; //当前解的规模加 1
				if (N <= size) { //若解的规模已构成全局解
					nSolu++; //全局解的数量加 1
					displaySolution(solu, N); //打印全局解
				}
				solu[size] = 0; //转入下一行,从第0列开始,试探下一皇后
			}
		}
	} while (0 < size || solu[size] < N);
}

本文完整代码已上传至 GitHub,可点击这里获取。

参考资料:
《数据结构(C++语言版)》,邓俊辉 编著,清华大学出版社,ISBN 978-7-302-33064-6
维基百科 - Eight queens puzzle

欢迎关注我的微信公众号,扫描下方二维码或微信搜索:AProgrammer,就可以找到我,我会持续为你分享 IT 技术。

猜你喜欢

转载自blog.csdn.net/young2415/article/details/85243430
今日推荐