P1219 [USACO1.5] 八皇后 Checker Challenge
[USACO1.5] 八皇后 Checker Challenge
题目描述
一个如下的 6 × 6 6 \times 6 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
上面的布局可以用序列 2 4 6 1 3 5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5 来描述,第 i i i 个数字表示在第 i i i 行的相应位置有一个棋子,如下:
行号 1 2 3 4 5 6 1\ 2\ 3\ 4\ 5\ 6 1 2 3 4 5 6
列号 2 4 6 1 3 5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5
这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前
3
3
3 个解。最后一行是解的总个数。
输入格式
一行一个正整数 n n n,表示棋盘是 n × n n \times n n×n 大小的。
输出格式
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
样例 #1
样例输入 #1
6
样例输出 #1
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
提示
【数据范围】
对于
100
%
100\%
100% 的数据,
6
≤
n
≤
13
6 \le n \le 13
6≤n≤13。
题目翻译来自NOCOW。
USACO Training Section 1.5
做题要点
- 字典顺序排列
- 6 ≤ n ≤ 13 6 \le n \le 13 6≤n≤13
- 输出前 3 3 3 个解
- 每行、每列有且只有一个棋子,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
- 以上面的序列方法输出(题目给出了具体的答案输出规则)
做题思路
因为每行、每列有且只有一个棋子,那么模拟放棋子的情况。
因为答案是行号按顺序的,所以按行(从第一行放到第
n
n
n行可以满足题目序列方法输出要求)来依次放棋子是合理的。那么顺序为先行后列。
看一个普遍的情况,假设到了第
m
m
m行,行数固定了,那么看该行上每一列(也是从第一列开始到最后一列)的情况。
假设考虑到了第
m
m
m行
k
k
k列,首先应该判断该位置能不能放棋子:
- 如果可以那么将其放下,然后考虑第 m + 1 m+1 m+1行 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
- 如果不可以那么往后走,如果
k
+
1
k+1
k+1不等于
n
n
n的话,考虑第
m
m
m行
k
+
1
k+1
k+1列;否则,考虑第
m
+
1
m+1
m+1行
1
1
1列
重复以上步骤直到走完整个棋盘,然后判断棋盘上是否有 n n n枚棋子,如果有记为其中一种情况,否则不计入。
到这里已经有第一个答案了
由于题目要求字典顺序排列,就要考虑第二个答案和第一个答案之间的关系。
最粗暴(简单但吃速度)的做法为从新来一遍,直到走完整个棋盘,然后判断棋盘上是否有
n
n
n枚棋子且不为第一个答案(已有答案),那么就记为第二个答案,否则不计入。
然后以此类推得出三个答案…直到所有答案或再用其他办法跑出解的总个数。
字典顺序排列其实就是首先看第一个字符比较大小,如果相等比较下一个字符,否则大的为大。
那么按照字典序的思想,应该先改变最后一个字符(变大),也就是最后一行棋子的列数变大。
如果无法改变把最后一个字符变大,那么就把最后一个字符变最小,改变倒数第二个字符。
依次类推。即字典序逐步增大。
那么第一次走完整个棋盘后,拿掉最后一个棋子(也就是最后一行的棋子,假设原本在第
m
m
m行
k
k
k列),然后考虑第第
m
m
m行
k
+
1
k+1
k+1列能不能放棋子,回到上述步骤。
这里就出现了一个新问题和老问题,如果棋盘上没有
n
n
n枚棋子或者该行无法放棋子了,怎么办?
也就说出现了至少有一行无法放下棋子。这说明放该行以前就把该行的所有情况ban掉了(无法放了)。
换句话说以前放的棋子策略是错的。
那么就需要从新放以前的棋子,再加上字典序的思想。首先调整的就是不能放棋子的那行的上一行。
按照这样的调整思路,如果不能放那就往回走,如果放满了自然就是一种情况,并且答案是按照字典序排序的。
将这种思想称为回溯.
接着说说如何判断该位置能不能放棋子。
最直接的操作,行、列、副对角线、主对角线 各一个标记数组。
记为
r
o
w
,
c
o
l
u
m
n
,
S
u
b
_
d
i
a
g
o
n
a
l
,
M
a
i
n
_
d
i
a
g
o
n
a
l
row , column,Sub\_ diagonal, Main\_ diagonal
row,column,Sub_diagonal,Main_diagonal;
其中
r
o
w
i
=
t
r
u
e
row_i = true
rowi=true表示第
i
i
i行可以放,否则不行。
同理,
c
o
l
u
m
n
i
=
t
r
u
e
column_i = true
columni=true表示第
i
i
i列可以放,否则不行。
那么主副对角线需要找到对应关系。
这里直接给出。
同一副对角线上的格子,下标相加相等,例如第一行第二列和第二行第一列,加起来都为3
同一主对角线上的格子,下标相减相等
例如第一行第四列和第三行第六列,
1
−
4
=
3
−
6
=
−
3
1-4 = 3-6 = -3
1−4=3−6=−3
因为会出现负数,在程序中可以加上
n
n
n即可全为正数,对数组访问更方便。
二进制优化
对于判断该位置能不能放棋子的操作可以进行二进制优化。
可以优化空间复杂度
可以看到
6
≤
n
≤
13
6 \le n \le 13
6≤n≤13,也就是说
n
n
n不大,那么开出来的四个标记数组也不大。
如果换成一个数字,其中二进制的每一位都为一个标记,那么空间复杂度从一个数组变成一个数字了。
例如 二进制数字
0001000
0001000
0001000 对应的数组应该是
v
[
4
]
=
t
r
u
e
v[4] = true
v[4]=true其他全为
f
a
l
s
e
false
false类似这种对应关系。
如果要进行
v
[
3
]
=
t
r
u
e
v[3] = true
v[3]=true则对二进制数字对应位 或(|) 上1即可,最终变为
0001100
0001100
0001100
总结思路:
假设考虑到了第 m m m行 k k k列,首先应该判断该位置能不能放棋子:
- 如果可以那么将其放下,如果 m + 1 = = n m+1==n m+1==n记为一种情况(棋盘放满了),将其拿去,继续考虑第 m m m行 k + 1 k+1 k+1列的情况( k + 1 = = n k+1==n k+1==n的话,拿去 m − 1 m-1 m−1行的棋子(记为 a a a列),继续考虑第 m − 1 m-1 m−1行 a + 1 a+1 a+1列);否则,考虑第 m + 1 m+1 m+1行 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
- 如果不可以那么往后走,如果 k + 1 k+1 k+1不等于 n n n的话,考虑第 m m m行 k + 1 k+1 k+1列;否则,进入第三个情况。
- 回到
m
−
1
m-1
m−1行有棋子的那一列(假设为
a
a
a列),将该棋子拿去,如果
a
+
1
a+1
a+1不等于
n
n
n的话,继续考虑第
m
−
1
m-1
m−1行
a
+
1
a+1
a+1列,否则,考虑第
m
−
1
m-1
m−1行
a
a
a列的第三种情况.
重复以上过程直到回跳到了第 0 0 0行(棋盘外面).
时间复杂度分析
首先分析最简单的第一个棋子如果放在第一行
n
n
n种情况,到最后一个棋子肯定只有一种情况了。因为涉及到主对角线和副对角线的剪枝问题,该时间复杂度无法很好推出。
退一步,如果不考虑主对角线和副对角线的情况,那么该时间复杂度应该是
O
(
n
!
)
O(n!)
O(n!)。
在先如果考虑的话,第
k
k
k行可能减少
k
1
k_1
k1个情况
那么时间复杂度
O
(
n
!
−
∑
i
=
1
n
k
i
)
O(n! - \displaystyle\sum_{i=1}^{n} k_i)
O(n!−i=1∑nki)
但粗略分析,时间复杂度随
n
n
n的增大,后续会迅速增大(可能为指数增长或者低于
n
n
n倍高于
n
−
3
n-3
n−3倍增长)
具体跑程序分析可得下图
伪代码
核心代码对应思路
void Solution::dfs(int x){
if(x == n+1){//棋盘被放满
cnt++;//答案情况+1
if(cnt <= 3) {//如果是前三个答案
for (auto i: ans)//输出答案
std::cout << i << ' ';
std::cout << "\n";
//return; //(可选)
}
}
for(int y=1;y<=n;y++){//枚举每一列
if(check(x,y)){//判断该位置能不能放棋子
put_down(x,y);//放棋子,做标记
dfs(x+1);//到下一行
pick_up(x,y);//拿掉棋子,去掉标记
}
}
//y == n+1 一行都放不下了进入第三个情况,回退到上一个dfs(x-1)
}
假设考虑到了第 m m m行 k k k列,首先应该判断该位置能不能放棋子:
- 如果可以那么将其放下,如果 m + 1 = = n m+1==n m+1==n记为一种情况(棋盘放满了),将其拿去,继续考虑第 m m m行 k + 1 k+1 k+1列的情况( k + 1 = = n k+1==n k+1==n的话,拿去 m − 1 m-1 m−1行的棋子(记为 a a a列),继续考虑第 m − 1 m-1 m−1行 a + 1 a+1 a+1列);否则,考虑第 m + 1 m+1 m+1行 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
- 如果不可以那么往后走,如果 k + 1 k+1 k+1不等于 n n n的话,考虑第 m m m行 k + 1 k+1 k+1列;否则,进入第三个情况。
- 回到
m
−
1
m-1
m−1行有棋子的那一列(假设为
a
a
a列),将该棋子拿去,如果
a
+
1
a+1
a+1不等于
n
n
n的话,继续考虑第
m
−
1
m-1
m−1行
a
+
1
a+1
a+1列,否则,考虑第
m
−
1
m-1
m−1行
a
a
a列的第三种情况.
重复以上过程直到回跳到了第 0 0 0行(棋盘外面).
return;是可选的原因为,放满 n n n个棋子后必定无法放 n + 1 n+1 n+1个棋子了。
所以不写return递归也肯定无法继续深入。
注:回溯写return出口是比较好的习惯
完整代码
C
#include <stdio.h>
#define re(i) (1<<(i))
const int N = 1e5;
int cnt , n , ans[20];
long long row,column,Sub_diagonal,Main_diagonal;
void dfs(int x){
if(x == n+1){
cnt++;
if(cnt <= 3)
for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[n==i]);
return ;
}
for(int i=1;i<=n;i++){
//二进制优化
if((row&re(x)) || (column&re(i)) || (Sub_diagonal&re(i+x)) || (Main_diagonal&re(i-x+n)));
else{
row|=re(x);column|=re(i);Sub_diagonal|=re(i+x);Main_diagonal|=re(i-x+n);
ans[x]=i;
dfs(x+1);
row^=re(x);column^=re(i);Sub_diagonal^=re(i+x);Main_diagonal^=re(i-x+n);
}
}
}
void init(){
NULL;
}
int main() {
scanf("%d",&n);
init();
dfs(1);
printf("%d",cnt);
return 0;
}
C++
#include <iostream>
#include <vector>
#include <cstring>
class Solution{
int n,cnt;
bool *row , *column;
bool *Sub_diagonal,*Main_diagonal;
std::vector<int>ans;
void dfs(int x);
void init();
inline bool check(int ,int );
inline void put_down(int,int);
inline void pick_up(int,int);
public:
void solve();
};
int main() {
auto *solution = new Solution();
solution->solve();
return 0;
}
void Solution::dfs(int x){
if(x == n+1){
cnt++;
if(cnt <= 3) {
for (auto i: ans)
std::cout << i << ' ';
std::cout << "\n";
}
}
for(int y=1;y<=n;y++){
if(check(x,y)){
put_down(x,y);
dfs(x+1);
pick_up(x,y);
}
}
}
inline bool Solution::check(int x,int y){
return row[x] && column[y] && Sub_diagonal[x+y] && Main_diagonal[x-y+n];
}
void Solution::init() {
std::cin >> n;
row = new bool[n+1];memset(row,true,n+1);
column = new bool[n+1];memset(column,true,n+1);
Sub_diagonal = new bool[(n<<1)+1];memset(Sub_diagonal,true,(n<<1) + 1);
Main_diagonal = new bool[n<<1];memset(Main_diagonal,true,n<<1);
cnt = 0;
ans.clear();
}
void Solution::solve() {
init();
dfs(1);
std::cout << cnt ;
}
inline void Solution::put_down(int x, int y) {
ans.push_back(y);
row[x] ^= true;
column[y] ^= true;
Sub_diagonal[x+y] ^= true;
Main_diagonal[x-y+n] ^= true;
}
inline void Solution::pick_up(int x, int y) {
ans.pop_back();
row[x] |= true;
column[y] |= true;
Sub_diagonal[x+y] |= true;
Main_diagonal[x-y+n] |= true;
}
Java
import java.util.Scanner;
import java.util.Vector;
public class Main {
static int n,cnt;
static boolean[] row =new boolean[100];
static boolean[] column =new boolean[100];
static boolean[] Sub_diagonal =new boolean[100];
static boolean[] Main_diagonal =new boolean[100];
static Vector<Integer>v = new Vector<>();
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
init();
dfs(1);
System.out.print(cnt);
}
public static void dfs(int x){
if(x == n+1){
cnt++;
if(cnt <= 3){
for(Integer i:v){
System.out.print(i + " ");
}
System.out.println();
}
return ;
}
for(int i=1;i<=n;i++){
if(check(x,i)){
put_down(x,i);
dfs(x+1);
pick_up(x,i);
}
}
}
public static void init(){
cnt = 0;
for(int i=1;i<100;i++){
row[i] = column[i] = Sub_diagonal[i] = Main_diagonal[i] = true;
}
}
public static boolean check(int x,int y){
return row[x] && column[y] && Sub_diagonal[x+y] && Main_diagonal[x-y+n];
}
public static void put_down(int x,int y){
v.add(y);
row[x] ^= true;
column[y] ^= true;
Sub_diagonal[x+y] ^= true;
Main_diagonal[x-y+n] ^= true;
}
public static void pick_up(int x,int y){
v.removeLast();
row[x] ^= true;
column[y] ^= true;
Sub_diagonal[x + y] ^= true;
Main_diagonal[x - y + n] ^= true;
}
}
Python3(不推荐)
因为最后一个点需要打表才能过
n = int(input())
row = [0 for i in range(200)]
column = [0 for i in range(200)]
Sub_diagonal = [0 for i in range(200)]
Main_diagonal = [0 for i in range(200)]
cnt = 0
def printf():
global cnt
for i in range(1,n+1):
print(row[i], end=' ')
print()
def dfs(x):
global cnt
if x == n+1:
cnt=cnt+1
if cnt <= 3:
printf()
return
for y in range(1,n+1):
if column[y] == 0 and Sub_diagonal[x+y] == 0 and Main_diagonal[x-y+n] == 0:
row[x] = y
column[y] = 1
Sub_diagonal[x+y] = 1
Main_diagonal[x-y+n] = 1
dfs(x+1)
column[y] = 0
Sub_diagonal[x+y] = 0
Main_diagonal[x-y+n] = 0
if n==13:
print('1 3 5 2 9 12 10 13 4 6 8 11 7')
print('1 3 5 7 9 11 13 2 4 6 8 10 12')
print('1 3 5 7 12 10 13 6 4 2 8 11 9')
print(73712)
exit(0)
dfs(1)
print(cnt)