【每日一题】【回溯+二进制优化】[USACO1.5] 八皇后 Checker Challenge C\C++\Java\Python3

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 6n13

题目翻译来自NOCOW。

USACO Training Section 1.5

做题要点

  1. 字典顺序排列
  2. 6 ≤ n ≤ 13 6 \le n \le 13 6n13
  3. 输出前 3 3 3 个解
  4. 每行、每列有且只有一个棋子,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
  5. 以上面的序列方法输出(题目给出了具体的答案输出规则)

做题思路

因为每行、每列有且只有一个棋子,那么模拟放棋子的情况。
因为答案是行号按顺序的,所以按行(从第一行放到第 n n n行可以满足题目序列方法输出要求)来依次放棋子是合理的。那么顺序为先行后列。

看一个普遍的情况,假设到了第 m m m行,行数固定了,那么看该行上每一列(也是从第一列开始到最后一列)的情况。
假设考虑到了第 m m m k k k列,首先应该判断该位置能不能放棋子:

  1. 如果可以那么将其放下,然后考虑第 m + 1 m+1 m+1 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
  2. 如果不可以那么往后走,如果 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 14=36=3
因为会出现负数,在程序中可以加上 n n n即可全为正数,对数组访问更方便。

二进制优化

对于判断该位置能不能放棋子的操作可以进行二进制优化。
可以优化空间复杂度
可以看到 6 ≤ n ≤ 13 6 \le n \le 13 6n13,也就是说 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列,首先应该判断该位置能不能放棋子:

  1. 如果可以那么将其放下,如果 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 m1行的棋子(记为 a a a列),继续考虑第 m − 1 m-1 m1 a + 1 a+1 a+1列);否则,考虑第 m + 1 m+1 m+1 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
  2. 如果不可以那么往后走,如果 k + 1 k+1 k+1不等于 n n n的话,考虑第 m m m k + 1 k+1 k+1列;否则,进入第三个情况。
  3. 回到 m − 1 m-1 m1行有棋子的那一列(假设为 a a a列),将该棋子拿去,如果 a + 1 a+1 a+1不等于 n n n的话,继续考虑第 m − 1 m-1 m1 a + 1 a+1 a+1列,否则,考虑第 m − 1 m-1 m1 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=1nki)
但粗略分析,时间复杂度随 n n n的增大,后续会迅速增大(可能为指数增长或者低于 n n n倍高于 n − 3 n-3 n3倍增长)
具体跑程序分析可得下图
在这里插入图片描述

伪代码

在这里插入图片描述

核心代码对应思路

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列,首先应该判断该位置能不能放棋子:

  1. 如果可以那么将其放下,如果 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 m1行的棋子(记为 a a a列),继续考虑第 m − 1 m-1 m1 a + 1 a+1 a+1列);否则,考虑第 m + 1 m+1 m+1 1 1 1列(因为该行已经放了棋子了,所以考虑下一行)。
  2. 如果不可以那么往后走,如果 k + 1 k+1 k+1不等于 n n n的话,考虑第 m m m k + 1 k+1 k+1列;否则,进入第三个情况。
  3. 回到 m − 1 m-1 m1行有棋子的那一列(假设为 a a a列),将该棋子拿去,如果 a + 1 a+1 a+1不等于 n n n的话,继续考虑第 m − 1 m-1 m1 a + 1 a+1 a+1列,否则,考虑第 m − 1 m-1 m1 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)
  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值