经常做算法赛题的朋友们都知道,八皇后问题是一道经典的搜索回溯题。简单来说,皇后问题就是在一个国际象棋棋盘上摆放若干枚皇后,使得这些皇后无法互相攻击,求出方案数,方案等。而摆放的皇后个数,对位置进行限制,以及要求求出的答案提供了很多变题。最近在洛谷上做到了三题皇后问题,八皇后,K皇后和N皇后。下面介绍一下我对这三题的理解。
一、八皇后
题目链接 https://www.luogu.org/problemnew/show/P1219
分析:这是并不是一道最经典的八皇后问题,有少许变形,不过依然可以采用最普通的深搜加回溯。需要注意的是,由于皇后可以攻击同一行,同一列以及同一对角线上的棋子,因此为了缩减时间复杂度,我们必须找到方法判断棋子是否在同行同列同对角线。也就是说,我们必须找到一些哈希函数,使得我们能在O(1)时间判断两个格子是否在同行同列同对角线。
相关哈希函数:
同行同列很容易判断,对于(x,y)和(i,j)两个格子,x==i和y==j即可判断是否同行同列。而对于主对角线(左上角到右下角的对角线),我们可以看出,若x-y==i-j,则两点在同一主对角线。对于副对角线,若x+y==i+j,则两点在同一副对角线。
深搜和回溯:
对于N*N的棋盘,在深搜的时候,我们可以逐行放皇后,遍历每个点,判断是否可以放皇后,若放到了N个,则找到一种方案。下面贴出AC代码
#include
#include
#include
using namespace std;
int N;
int ansnum;
int row[15];
int col[15];
int zd[30];
int fd[30];
int load[15];
set ans;
void dfs(int depth);
bool judge(int x,int y);
int main()
{
cin>>N;
dfs(1);
int cnt=0;
for(auto it=ans.begin();it!=ans.end();it++,cnt++)
{
if(cnt==3) break;
string temp=*it;
for(int i=0;i'9'||temp[i]<'0')
cout<<(int)temp[i]-'a'<<" ";
else
cout<N)
{
ansnum++;
string res="";
for(int i=1;i<=N;i++)
{
if(load[i]>=10)
res+=load[i]+'a';
else
res+=load[i]+'0';
}
ans.insert(res);
return;
}
for(int i=1;i<=N;i++)
{
if(judge(depth,i))
{
load[depth]=i;
row[depth]=1;
col[i]=1;
zd[depth-i+15]=1;
fd[depth+i]=1;
dfs(depth+1);
//回溯
row[depth]=0;
col[i]=0;
zd[depth-i+15]=0;
fd[depth+i]=0;
}
}
}
bool judge(int x,int y)//判断(x,y)是否可以放皇后
{
return (!row[x]&&!col[y]&&!zd[x-y+15]&&!fd[x+y]);
}
二、K皇后
题目链接 https://www.luogu.org/problemnew/show/P2105
分析:这一题棋盘变成了n*m的矩形,皇后个数变成了K个。不过最大的变化是给出了K个皇后的坐标,并且着K个皇后并非不能互相攻击,而且要求的问题是摆放K个皇后后,棋盘上不能被攻击的格子数。这样,这题其实用不到深搜了。那么暴力的做法是什么呢,将这k个皇后的坐标哈希,然后双重循环,判断每个格子是否被攻击,得答案,复杂度为O(nm)。不过n,m的范围为(1,20000),因此这样显然是tle的。但k的范围是(1,500),因此我的做法是,每放置一个皇后,求出它可以攻击的格子数(注意重复),这样复杂度为O(kn),有了不小的优化。
下面贴出AC代码
#include
using namespace std;
int row[20010];
int col[20010];
int zd[40010];
int fd[40010];
int n,m,k;
int inval;
bool judge(int x,int y);
int main()
{
cin>>n>>m>>k;
for(int cnt=0;cnt>x>>y;
//遍历行
for(int i=1;i<=m;i++)
{
if(judge(x,i))
inval++;
}
//遍历列
for(int i=1;i<=n;i++)
{
if(judge(i,y))
inval++;
}
//遍历主对角线
for(int i=x-min(x,y)+1,j=y-min(x,y)+1;i<=n&&j<=m;i++,j++)
{
if(judge(i,j))
inval++;
}
//遍历副对角线
for(int i=x+min(y-1,n-x),j=y-min(y-1,n-x);i>=1&&j<=m;i--,j++)
{
if(judge(i,j))
inval++;
}
//若(x,y)在放置皇后前不能被攻击,则多算了三次,要减去
if(judge(x,y))
inval-=3;
row[x]++;
col[y]++;
zd[x-y+20001]++;
fd[x+y]++;
}
cout<
三、N皇后问题
题目链接 https://www.luogu.org/problemnew/show/P1562
注:本题我并未AC,仅分析一下题解中一位大佬的做法,题解链接 https://www.luogu.org/problemnew/solution/P1562,作者为karma。
分析:
由于这一题和第一题类似,不过添加了一个限制条件,棋盘中一些点不能放皇后,而且数据范围比第一题大1,所以普通的深搜回溯是会tle的。因此我看了karma这位作者的题解,由于他介绍的不太详细,所以我补充一下我的理解。
这一题,题解中使用了状态压缩。
1.dfs中四个参数:now是当前行的状态,ld为副对角线,rd为主对角线,sta[d]为行d的初始状态。
2.四个宏:
xianzhi 得到当前行可以放皇后的位置,由于是0表示可以放,1表示不可以放,所以将四个参数或,为0的位置即为可以放,而在更新状态时需要1为可以放,所以再取反即可。
lowbit 得到当前状态坐标最小的可以放皇后的坐标值,即最右侧1的位置
youzuo 更新主对角线的状态,由于同一对角线上放了一个皇后后其他格子均不能放,而由于是逐行放皇后,当进入下一行时,副对角线与下一行相交的那个格子也不可以放,因为副对角线行坐标加1,列坐标减1,所以要左移1
zuoyou 同理youzuo
其余地方均较容易理解,不再赘述。
以上就是这我对三个皇后问题的理解。总而言之,皇后问题的精髓就是判断是否同行同列同对角线的哈希函数。在理解这个的基础上,再结合相关问题的相关条件,仔细分析思考,相关的变题也就可以做出来了。