【第0003页 · 递归】N皇后问题

【前言】本文以及之后的一些题解都会陆续整理到目录中,若想了解全部题解整理,请看这里:

第0003页 · N皇后问题

         今天我们来看一个著名的问题:N皇后问题。在此之前,我们先温习一下递归的思想。当然,温习的方式是看一道题目——全排列。

【全排列】现在给出 1 ~ n 这 n 个数,请问将这 n 个数排成一行共有多少种排法?例如:对于 1 ~ 3,可以有 (1,2,3)、(1,3,2)、(2,1,3)、(2,3,1)、(3,1,2)、(3,2,1) 6种排法。

【解题分析】这里我们用递归的思想。构造一个数组 P 用来存储当前排列的情况。再设一个数组 hash 用来标记哪些数已经放置在了当前的排列中,hash = 0 表示未放入,hash = 1 表示已放入。

        现在按照顺序往 P 数组的第 1 位到第 n 位放置数字。不妨假设当前已经填好了 1 ~ i 位,准备填 i + 1 位,那么就需要枚举 1 ~ n,如果这个数的 hash = 0,那么就可以将其放入第 i + 1 位,将其 hash 标记为 1,然后递归处理第 i + 2 位。注意:在这个递归结束后要将这个数的 hash 重新标记为 0。而递归的边界就是到达第 n + 1 位,此时说明 1 ~ n 位都已经填好了,我们定义一个数 cnt 用来记录排列种数。当到达边界时,cnt++。

【源码展示】

#include <cstdio>
using namespace std;
int cnt = 0, P[100] = {0};
bool hash[100] = {false};

void generate(int number, int n) {
    if (number == n + 1) {
        // 代表 1~n 位都排列好了
        cnt++;
        return;
    }
    // 处理当前这一位
    for (int i = 1; i <= n; i++) {
        if (!hash[i]) {
            P[number] = i;
            hash[i] = true;
            generate(number + 1, n);
            hash[i] = false;
        }
    }
}

int main() {
    int n;
    scanf("%d", &n);
    generate(1, n);
    printf("%d", cnt);
    return 0;
}

        复习完递归的知识后,我们来看一下 N 皇后问题。这里我们采用暴力枚举和回溯优化两种做法来实现这个问题。但注意,暴力枚举不能完成 n = 12及以上的情况,这时我们必须要使用回溯优化的解法。(蓝色为OJ平台的链接)

【N皇后问题】给出一个 n × n 的国际象棋棋盘,你需要在棋盘中摆放 n 个皇后,使得任意两个皇后之间不能互相攻击。具体来说,不能存在两个皇后位于同一行、同一列,或者同一对角线。请问共有多少种摆放方式满足条件。 

IO要求示例

输入描述:

一行,一个整数 n (1 <= n <= 12) 表示棋盘大小

8

输出描述:

一行,一个整数 num 表示摆放种类

92

 【解题分析】对于这个问题,最基本的方法就是枚举所有摆放种类,看看有哪些可行,但显然这不是很好的方法。我们换个角度考虑这个问题。

        如上图所示,对于每一列每一行而言都只能有一枚棋子,并且一定有一枚棋子。那么如果我们按照列从左往右编号,对于行从上往下编号,实际上,在不考虑对角线是否符合要求的情况下,这就是枚举所有全排列的情况罢了。例如:对于上面的左图,我们可以认为棋子为 (24135),右图为 (35124)。

        此时,我们就可以在全排列的代码基础上进行修改,在每次到达递归边界时判断是否符合对角线的要求即可。

【源码展示】

// 最多算到 11, 到 12 就爆掉了!!!所以必须优化!!!
#include <cstdio>
#include <cmath>
using namespace std;
int cnt = 0, P[100] = {0};
bool hash[100] = {false}, flag = true;

void generate(int number, int n) {
    if (number == n + 1) {
        flag = true;
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (abs(i - j) == abs(P[i] - P[j])) flag = false;
            }
        }
        if (flag) cnt++;
        return;
    }
    for (int i = 1; i <= n; i++) {
        if (!hash[i]) {
            P[number] = i;
            hash[i] = true;
            generate(number + 1, n);
            hash[i] = false;
        }
    }
}

int main() {
    int n;
    scanf("%d", &n);
    generate(1, n);
    printf("%d", cnt);
    return 0;
}

        但是,经过实验,上面的方法最多只能算到 11,算到 12 的时候就已经爆炸了,所以我们不得不寻找一个更为优化的解法。

【解题分析】那么我们应该如何优化呢?在上述过程中,我们是直到将一种排列表示出来后才进行判断对角线是否合理。那么我们是否可以在排列过程中就进行这个过程呢?答案是可以的!我们在每次排当前位置上的棋子时,就可以判断该棋子是否会与之前放置的棋子产生对角线上的冲突,如果这个冲突存在,那么我们直接就结束这种排列方式。这样就可以大大减少程序消耗的时间。同时,如果到达了最后一枚棋子,那么也就意味着排列是成立的!

【源码展示】

// 但这也不是最好的方法,经过实验,n = 15 时,时间就爆炸了。不过已经满足了本题的要求了
#include <cstdio>
#include <cmath>
using namespace std;
int cnt = 0, P[100] = {0};
bool hash[100] = {false}, flag = true;

void generate(int number, int n) {
    if (number == n + 1) {
        cnt++; // 能到达这里一定是合理的
        return;
    }
    for (int i = 1; i <= n; i++) {
        if (!hash[i]) {
            flag = true;
            for (int pre = 1; pre < number; pre++) {
                if (abs(P[pre] - i) == abs(pre - number)) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                P[number] = i;
                hash[i] = true;
                generate(number + 1, n);
                hash[i] = false;
            }
        }
    }
}

int main() {
    int n;
    scanf("%d", &n);
    generate(1, n);
    printf("%d", cnt);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值