问题描述
Description
在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。你的任务是,对于给定的N,求出有多少种合法的放置方法。
Input
共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
Output
共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。
Sample Input
1
8
5
0
Sample Output
1
92
10
本题大致可以用两种解法来解决。
一、DFS(回溯法)
即暴力,时间复杂度较高,遍历图的所有情况统计所有符合条件的方案,本题的解空间是一个m叉树。
(1)自己瞎做的办法,憨憨解法请跳过
#include <bits/stdc++.h>
using namespace std;
int mp[15][15];
int vis[15]; // 列是否可放
int cnt = 0;
int dir[4][2] = {-1, -1, -1, 1, 1, -1, 1, 1}; // 主副对角线四个方向
bool check(int r, int c, int n) { // 检查是否符合对角线不相互攻击
int a=r, b=c;
for (int i = 0; i < 4; ++i) {
a=r, b=c;
while (a >=0 && b >= 0 && a < n && b < n) {
if(mp[a][b]) {
return true; // 相互攻击返回true
}
a += dir[i][0];
b += dir[i][1];
}
}
return false;
}
void dfs(int step, int n) { // step行
if (step == n) {
cnt++;
return;
}
for (int i = 0; i < n; ++i) {
if(!vis[i] && !mp[step][i] && !check(step, i, n)) {
vis[i] = 1;
mp[step][i] = 1;
dfs(step+1, n);
vis[i] = 0;
mp[step][i] = 0;
}
}
}
int main() {
int n;
while(~scanf("%d", &n)) {
if (!n) break;
memset(vis, 0, sizeof vis);
cnt = 0;
dfs(0, n);
cout << cnt << endl;
}
return 0;
}
(2)常见的做法
首先,每一行只可摆放一个皇后,且每一行都会有一个皇后,所以只需查看列的情况,进行摆放皇后即可;
用ans数组存储结果,ans[i] = x表示第i行第x列放置了皇后;而判断是否对角线相互攻击,只需判断前面的行已放置的皇后坐标与当前要放的皇后坐标斜率绝对值是否为1就可以了。
#include <bits/stdc++.h>
using namespace std;
int ans[15]; // 列是否可放
int cnt = 0;
bool check(int n) { // 检查是否符合条件
for (int i = 0; i < n; ++i) {
if ((ans[i] == ans[n]) || (abs(ans[n]-ans[i])==abs(n-i))) // 后半部分判断对角线
return false;
}
return true;
}
void dfs(int step, int n) { // step表示行
if (step == n) {
cnt++;
return;
}
for (int i = 0; i < n; ++i) { // 遍历列
ans[step] = i;
if(check(step)) {
dfs(step+1, n);
}
}
}
int main() {
int n;
while(~scanf("%d", &n)) {
if (!n) break;
memset(ans, 0, sizeof ans);
cnt = 0;
dfs(0, n);
cout << cnt << endl;
}
return 0;
}
二、状态压缩
状态压缩是指用用数的每一个二进制位存储图的信息,以快速、方便地解决一些问题。本题和回溯法解题时一样,也只对列进行操作。
看了大佬的文章学得此法:
https://blog.csdn.net/txl199106/article/details/55505785
- 概念解释:
1.Max = (1<<n)-1
此时Max到第n位每一位都为1,表示所有的列全部不可放。状态压缩中,通常用(1 << n) - 1
(就是N个1)来表示最大状态。
2.Col, MainDiag, ContDiag
第x位等于1就表示 第x列因列、主对角线、副对角线互相攻击而不可放置。
3.EmptyCol = Max & ~(Col|MainDiag|ContDiag)
Max & ~(Col|MainDiag|ContDiag)
就表示各种方式都不会相互攻击,也就是可以放置的列的状态;
EmptyCol就表示可以放置皇后的列的状态,第x位等于1就表示第x列可以放置皇后,且循环的调条件就是EmptyCol不为0,即没有可以放置皇后的列。
4.current = EmptyCol & ((~EmptyCol)+1)
表示二进制数的最低位的1,也就是当前行摆放的皇后可以摆放的最后一列(证明略),用于取可以摆放皇后的列来摆放新的皇后。
5.EmptyCol &= ~current
用于清除EmptyCol的最后一个1,表示更新EmptyCol的状态(因为4.中取了一列)。
6.参数传递(更新状态)
(1) Col:Col | current
设current中第x位为1,则Col中第x位也应该为1,其他位不变。Col | current
即为将当前摆放的列在Col中更新。
(2)MainDiag:(current|MainDiag) >> 1
MainDiag中的1表示因为之前摆放的皇后的主对角线斜向攻击而使得当前行摆放的皇后不能位于的列。由于攻击是沿主对角线的,故而当前行影响的是第X列的话,那下一行影响的就是第X+1列,所以将列的状态总体右移。
(3)ContDiag与(2)中类似。 - 代码:
#include <bits/stdc++.h>
using namespace std;
int cnt = 0;
int n, Max;
void dfs(int Col, int MainDiag, int ContDiag) {
if(Col == Max) { // 终止条件
cnt++;
return;
}
int EmptyCol = Max & ~(Col|MainDiag|ContDiag);
while (EmptyCol) { // 每次while循环代表一行
int step = EmptyCol & ((~EmptyCol)+1); // 取一个可以放置的列
EmptyCol &= ~step; // 清除刚刚放置的列
dfs(step|Col, (step|MainDiag) >> 1, (step|ContDiag) << 1);
}
}
int main() {
while(~scanf("%d", &n)) {
if (!n) break;
cnt = 0; Max = (1<<n)-1;
dfs(0, 0, 0);
cout << cnt << endl;
}
return 0;
}