这是一篇写给刚入门的同志的题解 无意冒犯大佬
题目描述
在一个nXn的国际象棋棋盘上放置n(n<=12)个皇后,使它们不能互相攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。试求出所有放置方法。
输入 输出
输入一个数n(n<=12) 输出所有的排列方案总数。
样例输入 样例输出
4 2
引入
搜索与回溯的理解
搜索?回溯?这是个什么东西?相信不少新手都问过这个问题
简单来说 搜索就是暴力枚举
具体来说 搜索是一种将所有情况一一判断,寻找满条件的答案
回溯又是什么呢
回溯通常用于遍历 这用于解决可由一种状态产生多种状态的题目
产生于搜索,回馈于搜索,搜索的高级产物,搜索的精髓所在(还有后面要学到的剪枝和记忆化)
常用于解决棋盘问题(如过河卒问题,素数环问题),放置问题(如n皇后问题,素数棋盘问题),取放问题(如01背包问题弱化版,机器分配问题)等
回溯的特性
回溯的遍历过程可近似看成一颗树,本质是递归
#include<cstdio>
using namespace std;
int f(int x)
{
int s=0;
for(int i=1;i<=x;i++)s+=f(i);
return s;
}
int main()
{
printf("%d",f(4));
return 0;
}
这是一种回溯搜索 也叫递归
遍历过程如下图
大脑模拟一遍遍历过程,可以发现,回溯搜索是优先向下遍历深度,再遍历同层的其他状态
由于回溯搜索的这种特性 我们总称回溯搜索为深度优先搜索,简称深搜或dfs(deep first search)
时间复杂度通常较大,请谨慎使用
解题
思考过程
因为上面刚讲
因为不会打状压
因为这一题的每一层放置状态需要由之前放的点的状态得出,所以我们优先考虑深搜暴力
怎么搜
利用dfs枚举皇后所在位置
最后检查方法是否可行
计算时间复杂度
对于这种做法
每层有n种放法,共有n层,由乘法原理可得算法时间复杂度为O(n^n)
计算机每秒能处理约10^8次数据
很明显,12^12>10^8
所以该方法不可行
重新思考
我们发现,在遍历过程中,有很多一开始就不合法的状态
例如这种
还有这种
利用一些简单的组合数学,我们可以计算出这种一开始就不合法的状态占的比重是非常大的
发现,若一个状态的上一层状态不合法,那么这个状态也一定是不合法的
所以我们需要想办法让不合法状态不能继续产生新的状态
那么我们在遍历时判断该状态是否合法,就可以排除掉由不合法状态产生的不合法状态
计算新方法的时间复杂度
因为放完第x层后,第x+1层最多有(n-x)种状态,所以总状态数约为(n!)
12!<(10^8),所以想法可行
代码实现
首码
判断合法状态需要有之前的皇后方法状态,所以我们遍历时需要记录行列主副对角线
我们是顺层遍历的,所以不需要记录列
行非常好记录
主副对角线怎么记录呢
列一个表观察一下
不难发现
在同一条正对角线中,横坐标与纵坐标之差都相等
在同一条副对角线中,横坐标与纵坐标之和都相等
所以得出,已知对角线上一点横坐标可得出纵坐标
得出代码
bool a[20][20],b[20][20],c[20][20];
void dfs(int t)
{
if(t>n)
{
s++;
return;
}
for(int i=1;i<=n;i++)
{
if(!a[t][i]&&!b[t][i]&&!c[t][i])
{
for(int j=1;j<=n;j++)a[j][t+i-j]=b[j][t-i+j]=c[j][i]=1;
dfs(t+1);
for(int j=1;j<=n;j++)a[j][t+i-j]=b[j][t-i+j]=c[j][i]=0;
}
}
}
负向越界
你会惊喜地发现,wa掉啦,还有可能是re
理性分析下为什么
数组大小开小了?我开的够了啊?
? ! (1,1)好像没有副对角线!
这就造成了负向越界
那肯定会有人说了 那我加个绝对值不就行了?
似乎确实可行
但是这样的话,一条副对角线就会拐弯了,会将部分原本合法的状态剔除
所以这种情况需要和零进行比较取大者
那我们同理可得,主对角线也需要与n取小者
#define max(a,b) a>=b?a:b
#define min(a,b) a>=b?b:a
bool a[20][20],b[20][20],c[20][20];
void dfs(int t)
{
if(t>n)
{
s++;
return;
}
for(int i=1;i<=n;i++)
{
if(!a[t][i]&&!b[t][i]&&!c[t][i])
{
for(int j=1;j<=n;j++)a[j][max(0,t+i-j)]=b[j][min(n,t-i+j)]=c[j][i]=1;
dfs(t+1);
for(int j=1;j<=n;j++)a[j][max(0,t+i-j)]=b[j][min(n,t-i+j)]=c[j][i]=0;
}
}
}
空间,时间优化
自信一发
TLE了 bukaixin
在这里发现,我们的行是已知的,遍历到哪里都一样
所以每一个标记的行都是可以省略的,记录的两个循环就可以省略了
那记录后面的 j 怎么办?
干脆不要了 遍历到哪就拿它当 j ,毕竟j本身就是枚举行的
那么这样,副对角线也不会负向越界了,那就可以把max,min删掉了
但是这样一来 主对角线就会负向越界了
那可能又会有人不长记性
那可能又会有人说了 那我套个abs绝对值一下不就行了?
但是,绝对值等于同一个值的数有两个
那么同理 标记值等于一个数的主对角线也有两条,这会剔除一些原本合法的状态
那既然我们不能用这种方式防止负向越界了
那怎么办?
把min拿回来?
但是取0以后又会使所有越界的合法状态消失
所以这种办法不行
那我们就只能利用加法强行赋个正值了
对于主对角线的标记值,最小值当然是在(n,1)的位置,标记值=1-n
让它加上一个n就一定为正了
想到这里,你就得出正解了
正解代码贴出来了
已在多个oj上ac
code
#include<cstdio>
using namespace std;
int n,s;
bool a[14],b[500],c[500];//a是列 b,c是对角线
void dfs(int t)
{
if(t>n)//前n行都放完了
{
s++;//合法状态数+1
return;
}
for(int i=1;i<=n;i++)
{
if(!a[i]&&!b[t-i+n]&&!c[t+i])//判断能否放置皇后
{
a[i]=1,b[t-i+n]=1,c[t+i]=1;//放置后记录 所在列 对角线
dfs(t+1);//前往下一行
a[i]=0,b[t-i+n]=0,c[t+i]=0;//回溯
}
}
}
int main()
{
scanf("%d",&n);
dfs(1);//从第一行开始枚举
printf("%d",sum);
return 0;
}
点个赞呗 😉