寻找“好排列” 问题解析 | 豆包MarsCode AI刷题

96 阅读5分钟

问题描述:寻找“好排列”

小R正在研究一种特殊的排列,称为“好排列”。一个排列被称为“好排列”,当且仅当所有相邻的两个数的乘积均为偶数。现在给定一个正整数 n,我们需要计算长度为 n 的“好排列”的数量,并将结果对 10^9 + 7 取模后输出。

为了更好地理解这个问题,我们将一步步解析其思路和解决方案。

问题分析

  1. “好排列”的条件

    一个排列被称为“好排列”,当且仅当其中所有相邻的两个数的乘积均为偶数。

    两个数的乘积为偶数的条件是:至少有一个数是偶数。因此,我们需要确保在排列中,所有相邻数对中至少有一个数是偶数。这意味着不能让两个奇数相邻。

  2. 奇数与偶数的数量

    给定长度为 n 的排列,数字范围是从 1n

    在这个范围内,奇数的数量为 k = ceil(n / 2)(ceil(n)表示对n向上取整),可以通过 k = (n + 1) / 2 计算得到。

    偶数的数量为 m = floor((n + 1)/2) (floor(n)表示对n向下取整),即 m = n / 2

    比如如果 n = 5,那么奇数就是 3(1, 3, 5),偶数就是 2(2, 4)

  3. 排列的约束

    我们需要将 k 个奇数和 m 个偶数排列成一个“好排列”,且不能让两个奇数相邻。 为了做到这一点,我们不能让两个奇数挨在一起,因为两个奇数相乘会得到奇数。

    偶数起到了“隔开”奇数的作用。想象一下,我们可以把偶数放好,然后把奇数插入到偶数之间的空隙中。

    例如,有 m 个偶数,它们可以形成 m + 1 个空隙(一个在每两个偶数之间,还有一个在最前面和最后面),我们要把奇数放入这些空隙中。

    如果空隙的数量 m + 1 小于奇数的数量 k,那我们就无法把奇数放好。因此,必须满足 m + 1 >= k 这个条件,才能有可能形成一个“好排列”。

计算排列数量

  1. 组合数计算

    如果槽位数满足 m + 1 >= k,那么我们可以将 k 个奇数放入 m + 1 个槽位中的不同位置,这可以用组合数来计算,即 C(m + 1, k)

  2. 排列奇数和偶数

    在确定了奇数的位置之后,剩下的就是如何排列奇数和偶数。

    奇数可以在其槽位中以 k! 种方式排列,偶数可以在剩余位置以 m! 种方式排列。

  3. 总的排列数

    因此,总的“好排列”数量为:C(m + 1, k) * k! * m!

  4. 结果取模

    由于结果可能非常大,我们需要对 10^9 + 7 取模,以防止数值溢出。

代码实现详解

下面是实现这一逻辑的完整 C++ 代码,其中包括对组合数、阶乘和逆元的计算。

#include <iostream>
#include <vector>

const int MOD = 1000000007;

// 快速幂计算 x^y % mod
long long power_mod(long long x, long long y, long long mod) {
    long long res = 1;
    x %= mod;
    while (y > 0) {
        if (y & 1) {
            res = res * x % mod;
        }
        x = x * x % mod;
        y >>= 1;
    }
    return res;
}

// 预计算阶乘和阶乘的逆元
void precompute_factorials(int max_n, std::vector<long long> &fact, std::vector<long long> &inv_fact) {
    fact.resize(max_n + 1, 1);
    for (int i = 1; i <= max_n; i++) {
        fact[i] = fact[i - 1] * i % MOD;
    }
    inv_fact.resize(max_n + 1, 1);
    inv_fact[max_n] = power_mod(fact[max_n], MOD - 2, MOD);
    for (int i = max_n - 1; i >= 0; i--) {
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
    }
}

// 计算组合数 C(n, k) % MOD
long long comb(int n, int k, const std::vector<long long> &fact, const std::vector<long long> &inv_fact) {
    if (k < 0 || k > n) {
        return 0;
    }
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

int solution(int n) {
    // 计算奇数和偶数的数量
    int k = (n + 1) / 2; // 奇数的数量
    int m = n / 2;       // 偶数的数量

    if (m + 1 < k) {
        return 0;
    }

    // 预计算阶乘和逆元
    std::vector<long long> fact, inv_fact;
    precompute_factorials(n, fact, inv_fact);

    // 计算组合数 C(m + 1, k)
    long long c = comb(m + 1, k, fact, inv_fact);

    // 计算 k! 和 m!
    long long fk = fact[k];
    long long fm = fact[m];

    // 计算最终结果
    long long result = c * fk % MOD;
    result = result * fm % MOD;

    return (int)result;
}

代码详解

  1. 快速幂计算 (power_mod)

    该函数用于高效计算 ,通过二进制指数的方法来减少计算量。

  2. 预计算阶乘和逆元 (precompute_factorials)

    使用动态规划的方法预计算阶乘和逆元。

    逆元的计算使用了费马小定理,因为 MOD 是一个质数,利用公式:

  3. 组合数计算 (comb)

    计算组合数 C(n, k),即从 n 个元素中选 k 个的方式数。

  4. 解决方案 (solution)

    首先计算奇数和偶数的数量,并判断是否可以形成“好排列”。

    如果可以,则计算组合数,接着计算奇数和偶数的排列数量,最后得到总的“好排列”数量。

总结

这个问题的核心在于理解如何在排列中确保所有相邻的两个数的乘积为偶数。通过观察排列中奇数和偶数的分布规律,我们可以确定不能让两个奇数相邻,这样才能满足“好排列”的条件。为了解决这个问题,我们将偶数视为“隔板”,并将奇数插入到这些隔板中间的槽位中。这种方法将原本看似复杂的问题转化为一个组合数学问题。

在代码实现中,我们首先计算奇数和偶数的数量,判断是否满足“好排列”的条件。接下来,我们使用组合数学的方法计算出奇数在偶数之间的分布方式,再计算奇数和偶数各自的排列数量。为了保证计算的高效性和防止数值溢出,我们使用了快速幂方法来计算模逆元,并预先计算了阶乘和逆元,以便在后续的组合数和排列数计算中快速查找。