本篇所有代码均正确已通过测试,请放心食用
题目
一个如下的 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
上面的布局可以用序列 2 4 6 1 3 5 来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子,如下:
行号 1 2 3 4 5 6
列号 2 4 6 1 3 5
这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 3 个解。最后一行是解的总个数。
输入格式
一行一个正整数 n,表示棋盘是 n×n 大小的。
输出格式
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
输入输出样例
输入
6
输出
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
说明/提示
【数据范围】
对于 100% 的数据,6≤n≤13。
题目解析
1. 每行每列只能有一个皇后
2. 每条对角线上只能有一个皇后
3. 需要输出前三种摆法以及方案总数,每种摆法由按行从小到大的顺序输出所占列号
需要输出方案总数,且皇后摆放位置有要求,且实验数据范围较小,那么这是一个有条件的排列问题,直接用dfs解决。
分析思路
数据要求:
int n;//规格
bool st[20];//对应列序号是否被选
int total;//解的总数
int a[15];//a[i]记录第i行的皇后在第几列,输出结果时,按顺序访问即可
解析:dfs最经典的数据就是一个布尔类型的数组(根据具体题目定维数),用于标记该元素是否被选,从而生成解空间树的不同分支。
区别于普通的排列dfs问题,此处还用到了一个int型数组a,这是因为最终的要求需要输出前三种方案,无论算法如何,一定需要一个数组存储每行对应占了哪一列,才能方便输出,否则仅仅一个数组st,无法记录每列选择的顺序。
对于列举出的方案,建议读者画出解空间树,dfs()函数中,用for循环横向遍历解空间树的同一层元素,用递归纵向遍历一条分支,触底或者不满足条件时则回溯或剪枝。
那么如何列举或者选择元素呢,这是便用到了bool数组st,以及N皇后的摆放条件。
关键:如何判断皇后摆放的合法性:
- dfs()函数中,传参k表示目前已经摆了k个皇后,即当前所在行为k+1,前k行已经被摆好了。因此可保证一行只有一个皇后。
- bool st[i]数组用于标识第i列是否被选择,被选择为1,未被选则为0,因此可保证一列上只有一个皇后。
- 每条对角线上只有一个皇后:
- 方法一:观察可得列两个元素若在同一条对角线上,那么行数之差=列数之差
- 方法二:设置两个bool类型数组,专门标记每条对角线上是否已被占。列数学算式不难发现:每个表格中每条对角线均可用唯一数字标识:主对角线方向的对角线:行号-列号+n;副对角线上方向:行号+列号。读者可参考以下图片自行验证。
主对角线方向的对角线:
1 | 2 | 3 | 4 | 5 | |
1 | |||||
2 | |||||
3 | |||||
4 | |||||
5 |
副对角线方向上的对角线:
1 | 2 | 3 | 4 | 5 | |
1 | |||||
2 | |||||
3 | |||||
4 | |||||
5 |
两种方法的代码实现分别如下:
代码实现
方法一:
利用对角线上行列之差相等
#include <iostream>
using namespace std;
#include<math.h>
int n;//规格
bool st[20];//对应列序号是否被选
int total;//解的总数
int a[15];//a[i]记录第i行的皇后在第几列,输出结果时,按顺序访问即可
void print()
{
if (total < 3)
{
for (int i = 1; i <= n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
}
bool Ndiagonal(int c,int k)//传入当前列号,前面已摆好的行数
//判断是否成对角线就是和前面所有列号进行对比
{
//如果当前行数与对比目标相差k行,那么列数相差k列的话就是在一个对角线上
for (int i = 1; i <= k; i++)
{
if (abs(c - a[i]) == k + 1 - i)
return false;
}
return true;
}
void dfs(int k)//目前已经摆了k个皇后,即当前所在行为k+1,前k行已经被摆好了
{
if (k == n)
{
print();
total++;
return;
}
//for循环遍历列,每层递归遍历行
for (int i = 1; i <= n; i++)
{
//关键:如何判断皇后摆放的合法性:目前只需要判断是否成对角线即可
if (!st[i] && Ndiagonal(i,k))
{
st[i] = 1;
a[k + 1] = i;
dfs(k + 1);
st[i] = 0;
}
}
}
int main()
{
cin >> n;
dfs(0);
cout << total;
}
运行结果:
方法二:
用布尔数组标记每条对角线上的皇后摆放情况,此代码为博主在洛谷上看了其他大牛的题解后自己复现了一遍,发现用数学关系标识不同的对角线的方式十分巧妙,于是写入本篇博客供大家一起学习。
#include <iostream>
using namespace std;
int n;//棋盘规模
//数组范围要尽量大一点!!!
bool c[15],d1[100],d2[100];//c标记列有无被选中,d1标记该主对角线方向的对角线是否被占用,d2标记该副对角线方向的对角线
int a[15];//a[i]记录第i行占用的列数,用于后续打印
int total;
void print()
{
if (total < 3)
{
for (int i = 1; i <= n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
total++;
}
void queen(int r)//当前所在第r行,即表示行数,又可以计数作为终结条件
{
if (r > n)
{
print();
return;
}
for (int i = 1; i <= n; i++)
{
if (!c[i] && !d1[r - i + n] && !d2[i + r])//如果当前位置没被选择,且不与已选择的元素发生冲突
{
c[i] = 1;
d1[r - i + n] = 1;
d2[i + r] = 1;
a[r] = i;
queen(r + 1);
c[i] = 0;
d1[r - i + n] = 0;
d2[i + r] = 0;
}
}
}
int main()
{
cin >> n;
queen(1);
cout << total;
}
注意!数组范围要尽量大一点,起初提交未能全部AC,下意识觉得是数组范围问题,果真如此。对于大小未知的数组的范围大家还是尽量写大一点。
运行结果:
相应的解释已经写在代码注释中,除此以外,对于两种方法的dfs()中的for循环里这一段代码我还想额外解释一下,尤其是对于初学者:(两种方式都大同小异,此处以方法一为例,方法二类似)
for (int i = 1; i <= n; i++)
{
//关键:如何判断皇后摆放的合法性:目前只需要判断是否成对角线即可
if (!st[i] && Ndiagonal(i,k))
{
st[i] = 1;
a[k + 1] = i;
dfs(k + 1);
st[i] = 0;
}
}
调用dfs(k+1)后,回溯的时候为何只复原了st[i]=0,而没有管a[k+1]?
因为:
- a[j]的值在此处并不影响皇后位置的选择,即:不影响判断条件
- a[j]在之后的for循环中会被复写,所以不会影响未来的数据。
所以,在dfs的回溯过程中,只需要将会影响本次操作的元素复原即可,类似的还有求和操作等(是需要复原的)。
后续会继续更新dfs类型题目的相关题解与知识点技巧总结,欢迎评论区留言与我共同讨论!