n皇后问题的解决与算法优化
利用学到的算法设计知识,通过布置具有一定难度的设计题目,帮助学生对所学算法知识进行巩固及系统运用,并进一步提高独立的问题分析和算法设计的能力。
设计要求:按算法设计要求完成各阶段文档:
5.算法分析 6.编码实现 7.测试用例设计 8.测试与运行记录 9.课程设计完成结果分析与个人小结 10.参考文献 11.附录(软件配置、个人完成的程序模块和文档清单) 注:此部分在文档可只含一页列出有关文档目录即可,但在交付的个人电子文档中应当含有所有应当完成的具体文档内容。
1. 利用《算法设计与分析》课程中所学到的编程知识和编程技巧对N皇后进行问题分析,选择合适的算法策略解决问题
2. 对N皇后问题进行算法设计并且分析
3. 使用算法对N皇后进行编码实现
4. 测试用例设计、测试与运行记录
在一个N×N的(国际)棋盘上,放置N个棋子(皇后),使得N个棋子中任意2个都不在同一行、同一列以及同一斜线。 问:放置这N个棋子的方法共有多少种?并以此输出这些方案。
n皇后问题可以选择使用回溯法。 本问题首先应该考虑的是皇后位置的限制条件,每一行,每一列,每一斜向右下的线,每一条斜向左下的线都只能放一个皇后。对于nxn的棋盘,可以用四个数组标记每一行、每一列、每一斜向右下的线,每一条斜向左下的线上是否已经有了棋子。其中列一共有 n 个,从左往右编号为 0 至 n-1,同理行也类似,从上往下编号为0到n-1。 斜向右下的线一共有2n-1条,分别从左下到右上方向标号为0到2n-2,同理斜向左下的线也有2n-1条,分别从左上到右下的方向编号为0到2n-2。编号方法如图所示。
对于这n个皇后我们可以编号为0~n-1,我们可以用行来索引,每行放置一个皇后,从第一行开始选择位置放置,直到最后一行。当n个皇后都放好后,开始回溯查找另外一种方案。同时我们需要建立一种从皇后所在行列编号到斜向左下位置和斜向右下位置的映射。我们建立映射,斜向右上方的序号lr=行号+列号,斜向左下方的序号为rl=n-1-行号+列号。放置皇后时需要检查当前选择的位置是否可以放置,以不至于被攻击。
采用的算法模型:回溯法
n皇后问题要求每一个皇后在不同行、不同列、不同斜线,因此我们可以使用不同行、不同列作为显约束,隐约束选择不同斜线。这样选择可以将解空间大大缩小。从根到叶子的路径就是一个解空间树。
n皇后树的深度为n。
(3)搜索解空间
1°约束条件:在第i行第j列放置第i个皇后时不能 `前i-1个皇后在同一斜线。根据行列与斜线的映射关系可知,该位置所在斜线的标号,若标记数组中该位置为false表示此斜线还可以放置皇后,否则该位置不能放。
2°限界条件:该问题不存在放置方案好坏的问题,所以不需要设置限界条件。
3°搜索过程:搜索过程从根开始,以深度优先方式进行搜索,根结点是活结点,并且是当前的扩展结点。在搜索过程中,当前的扩展结点沿纵深方向移向一个新结点, 判断该新结点是否满足隐约束。如果满足,则新结点成为活结点,继续深一层的搜索;如果不满足,则换到该新结点的兄弟结点继续搜索;如果新结点没有兄弟结点。或者其他兄弟结点已经全部搜索完毕,则扩展结点成为死结点,搜索回溯到其父结点处继续进行。搜索过程直到找到问题的根结点变为死结点为止。
时间复杂度:该问题的解空间为一棵树枝数量从n到1递减的树,深度为n,最坏的情况下,解空间树如上图所示。除了最后一层外,有1+n+n*(n-1)+n*(n-1)*(n-2)+……n(n-1)(n-2)(n-3)…2 ≈n!个结点需要扩展,而这些节点分别要扩展n、n-1、n-2、…、1个分支,总的分支个数不会超过n*n!,每个分支都有约束条件,判断约束条件的时间复杂度为O(1)。该算法在最坏情况下耗时为O(n*n!),故时间复杂度为O(n*n!)。
空间复杂度:回溯法的另一个重要特性是在搜索执行的同时产生解空间,该方法需要用一个大小为n的数组记录扩展结点的路径,两个大小为2*n-1数组标记两个斜线方向的某个位置是否已经放置过皇后。所以空间复杂度为O(n)。
1.实现语言:C++
代码:
#include
#include
#include
using namespace std;
vectornd;
int n, num;
bool p;//选择是否输出所有排列方式
vectorl_r, r_l;//标记列,从左到右的斜线,和从右到左的斜线是否已经访问过
//不能放置物品为false 否则为true
inline void init()
{
nd.resize(n);
l_r.assign(2*n-1, true);
r_l.assign(2*n-1, true);
for (int i = 0; i < n; i++)nd[i] = i;
num = 0;
}
inline void mapping(int cur, int& _l_r, int &_r_l)
{
_l_r = cur + nd[cur];
_r_l = (n - 1 - cur) + nd[cur];
}
inline bool judge(int cur)//判断当前位置能否放置皇后
{
int lr, rl;
mapping(cur, lr, rl);
return l_r[lr] && r_l[rl];
}
void dfs(int cur)//排列树优化
{
int i, lr, rl;
if (cur == n)//找到一种放置方法
{
num++;
if (p) {
cout << "---------------------------------------------------" << endl;
cout << "第" << num << "个满足要求的皇后位置:" << endl;
i = 0;
for (auto it : nd)//打印符合要求的位置
{
cout << "(" << i++ << "," << it << ") ";
}
cout << "\n--------------------------------------------------" << endl;
}
}
else
{
for (i = cur; i < n; i++)
{
swap(nd[cur], nd[i]);
if (judge(cur))//判断隐约束是否满足要求
{
mapping(cur, lr, rl);//得到映射关系
//标记
l_r[lr] = false;
r_l[rl] = false;
dfs(cur + 1);
//回退
l_r[lr] = true;
r_l[rl] = true;
}
swap(nd[cur], nd[i]);
}
}
}
int main()
{
//freopen("debug.txt", "r", stdin);
int N;
cout << "请输入测试数据:" ;
cin >> N;
while (N--)
{
cout << "请输入皇后的个数:" ;
cin >> n;
cout << "请选择是否要输出所有排列(’1‘ or ‘0’,‘1’表示输出,‘0’表示不输出):"<> p;
init();
dfs(0);
cout <<"共有"<< num <<"种方案"<< endl;
}
return 0;
}
n皇后问题有一个特点,棋盘是对称分布的。通过对称可以获得另外一种方法,到后面会发现和前面的方法重复了。如下图所示。
因此,我们可以利用对称性使工作量减少一半,即把第一行的皇后放在左边一半的区域,右面一半的皇后可以通过对称性得知其排列方案。但需要注意n为奇数的情况,为了避免重复,当第一行放置在n/2列时,第二行必须放在左面一部分。
此时需要修改void dfs(int cur);函数为void dfs(int cur,int k);其中k表示在第cur行上第0到k-1区域放置皇后。最后得到的方案数目变为num*2。优化后的执行速度大约可提高一倍
优化后的代码为:
#include
#include
#include
using namespace std;
vectornd;
int n, num;
bool p;//选择是否输出所有排列方式
vectorl_r, r_l;//标记列,从左到右的斜线,和从右到左的斜线是否已经访问过
//不能放置物品为false 否则为true
inline void init()
{
nd.resize(n);
l_r.assign(2 * n - 1, true);
r_l.assign(2 * n - 1, true);
for (int i = 0; i < n; i++)nd[i] = i;
num = 0;
}
inline void mapping(int cur, int& _l_r, int &_r_l)
{
_l_r = cur + nd[cur];
_r_l = (n - 1 - cur) + nd[cur];
}
inline bool judge(int cur)//判断当前位置能否放置皇后
{
int lr, rl;
mapping(cur, lr, rl);
return l_r[lr] && r_l[rl];
}
void dfs(int cur,int k)//排列树优化
{
int i, lr, rl;
if (cur == n)//找到一种放置方法
{
num++;
if (p) {
cout << "---------------------------------------------------" << endl;
cout << "第" << num << "个满足要求的皇后位置:" << endl;
i = 0;
for (auto it : nd)//打印符合要求的位置
{
cout << "(" << i++ << "," << it << ") ";
}
cout << "\n--------------------------------------------------" << endl;
cout << "---------------------------------------------------" << endl;
cout << "第" << num+1 << "个满足要求的皇后位置:" << endl;
i = 0;
for (auto it : nd)//打印符合要求的位置
{
cout << "(" << i++ << "," << n-1-it << ") ";
}
cout << "\n--------------------------------------------------" << endl;
}
num++;
}
else
{
for (i = cur; i > N;
while (N--)
{
cout << "请输入皇后的个数:";
cin >> n;
cout << "请选择是否要输出所有排列(’1‘ or ‘0’,‘1’表示输出,‘0’表示不输出):" << endl;
cin >> p;
init();
dfs(0,(n+1)/2);
cout << "共有" << num<< "种方案" << endl;
}
return 0;
}
七、测试用例设计
以下测试用例为n皇后问题的输入与输出(不包括具体的位置,只有多少种可能的情况)
输入
2 3 4 5 6 7 8 9 10 11 12 13 14 15
输出
0 0 2 10 4 40 92 352 724 2680 14200 73712 365596 2279184
经测试该程序满足测试用例。
八、总结:
n皇后问题为经典的算法问题,有很多种解法,在目前的知识范围内可以使用的解法是回溯法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。回溯法属于暴力求解,当问题规模较大时程序运行时间将会很长,因此需要做好优化工作保证程序的执行效率,比如剪枝,缩小解空间,优化判断约束条件的执行效率等……该问题我虽然使用了排列树优化、约束函数优化、对称优化,但当n超过13以后程序的执行时间将会暴涨,这个问题还可以使用二进制优化提高时间和空间效率,由于方法较难理解,目前自己水平有限这里就没有列出。要想在计算机领域深造算法是必备技能,因此在今后的学习中还要加强这方面的学习。