题解 | #小红的好排列#

小红的好排列

https://www.nowcoder.com/practice/67bf8c0b7fc64f3bb9cc21bf6dbbd14f

1. 问题分析

1.1 问题抽象

题目要求统计满足特定条件的排列数量:长度为 的排列 中,恰好有 个下标 满足 是 3 的倍数。

因为 3 是一个质数,乘积 被 3 整除的充要条件是: 是 3 的倍数 或者 是 3 的倍数。

我们可以根据“是否为 3 的倍数”这一属性,将数字 分为两类集合:

  1. 倍数集 (Div3):包含所有能被 3 整除的数。
    • 集合大小设为
  2. 非倍数集 (NonDiv3):包含所有不能被 3 整除的数。
    • 集合大小设为

1.2 排列位置与值的匹配逻辑

在排列构建过程中,我们需要考虑“下标 的属性”和“填入值 的属性”之间的匹配关系:

  • 如果下标 (共有 个这样的位置): 无论填入什么值 ,乘积 必然包含因子 3。因此,这 个位置必然贡献 个满足条件的计数。

  • 如果下标 (共有 个这样的位置): 由于下标本身不含因子 3,为了使乘积成为 3 的倍数,填入的值 必须属于 集合。

1.3 目标确定

题目要求总共有 个满足条件的位置。 已知来自 下标的贡献是固定的 个。 因此,我们需要从 下标的位置中,额外凑出 个满足条件的位置,其中:

如果计算出的 ,说明即使没有任何 下标贡献,总数也超过了 ,此时无解(但根据 ,这种情况不会发生,因为 )。

2. 算法:组合计数

我们需要计算满足上述结构的排列总数。问题转化为两部分的填充任务。

  1. 在 NonDiv3 下标中构造 个“好”位置

    • 选位置:从 下标中选出 个位置。
      • 方案数:
    • 选值并排列:这 个位置必须填入 的值。我们从 个可用的 值中选出 个,并排列放入这 个位置。
      • 方案数:
  2. 填充剩余的 NonDiv3 下标

    • 剩余的 下标数量为
    • 为了不增加满足条件的计数,这些位置严禁填入剩余的 值,必须填入 值。
    • 我们有 值可用,从中选出 个并在这些位置排列。
    • 方案数:
  3. 填充 Div3 下标

    • 此时,剩余的空位是 下标位置。
    • 剩余的待填数字总数也是 个(包括剩余的 值和剩余的 值)。
    • 由于这些位置的下标已经是 3 的倍数,无论填什么都满足条件(已经被计入基础贡献 中),因此只需将剩余数字任意全排列。
    • 方案数:

最终公式

根据乘法原理,总方案数为各步骤方案数的乘积:

展开组合数和排列数公式:

注意:如果 (需要的 值比拥有的多)或者 ,答案为 0。

3. 复杂度分析

3.1 时间复杂度

  • 预处理:我们需要计算阶乘及其模逆元(用于组合数计算)。范围是
    • 计算阶乘数组 frac[]
    • 计算逆元数组 inv[]:利用费马小定理 + 线性递推或快速幂,整体预处理可以是
  • 单次查询:公式计算仅涉及常数次乘法和取模操作。
    • 计算耗时:
  • 总时间复杂度

3.2 空间复杂度

  • 如果不使用线性求逆元,仅存储阶乘表,需要 空间。
  • 如果存储阶乘和逆元表,需要 空间。

4. 代码实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

static constexpr ll MOD = 1e9 + 7;

struct Combinatorics {
    std::vector<ll> fact;
    std::vector<ll> invFact;

    Combinatorics(int n) {
        fact.resize(n + 1);
        invFact.resize(n + 1);

        fact[0] = 1;
        for (int i = 1; i <= n; ++i) {
            fact[i] = (fact[i - 1] * i) % MOD;
        }

        // 使用费马小定理计算 n! 的逆元
        invFact[n] = power(fact[n], MOD - 2);
        for (int i = n - 1; i >= 0; --i) {
            invFact[i] = (invFact[i + 1] * (i + 1)) % MOD;
        }
    }

    // 快速幂
    ll power(ll base, ll exp) const {
        ll res = 1;
        base %= MOD;
        while (exp > 0) {
            if (exp % 2 == 1) res = (res * base) % MOD;
            base = (base * base) % MOD;
            exp /= 2;
        }
        return res;
    }

    // 查询组合数 C(n, k)
    ll nCr(int n, int k) const {
        if (k < 0 || k > n) return 0;
        return fact[n] * invFact[k] % MOD * invFact[n - k] % MOD;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n;
    cin >> n;
    Combinatorics comb(n);
    ll k = n / 3;
    ll m = n - k;
    ll x = n / 2 - k;
    if (x < 0 || x > k || x > m) {
        cout << 0 << endl;
        return 0;
    }
    ll ans = 1LL;
    // 在 NonDiv3 下标中构造 x 个“好”位置
    ans = ans * comb.nCr(m, x) % MOD;
    ans = (ans * comb.fact[k] % MOD) * comb.invFact[k - x] % MOD;
    // 填充剩余的 NonDiv3 下标
    ans = (ans * comb.fact[m] % MOD) * comb.invFact[x] % MOD;
    // 填充 Div3 下标
    ans = ans * comb.fact[k] % MOD;

    cout << ans << endl;
}
#如果你有一天可以担任公司的CEO,你会做哪三件事?#
全部评论

相关推荐

01-28 16:12
中南大学 Java
几年前还没有chatgpt的时候,刷题真的是很痛苦。刷不出来只能看题解,题解有几个问题:第一个是每次看的写题解的人都不一样,很难有一个统一的思路;第二个也是最重要的是,题解只提供了作者自己的思路,但是没有办法告诉你你的思路哪里错了。其实很少有错误的思路,我只是需要被引导到正确的思路上面去。所以传统题解学习起来非常困难,每次做不出来难受,找题解更难受。但是现在chatgpt能做很多!它可以这样帮助你&nbsp;-1.&nbsp;可以直接按照你喜欢的语言生成各种解法的题解和分析复杂度。2.&nbsp;把题和你写的代码都发给它,它可以告诉你&nbsp;你的思路到底哪里有问题。有时候我发现我和题解非常接近,只是有一点点🤏想错了。只要改这一点点就是最优解。信心倍增。3.&nbsp;如果遇到不懂的题解可以一行一行询问为什么要这样写,chatgpt不会嫌你烦。有时候我觉得自己的range写错了,其实那样写也没错,只是chat老师的题解有一点优化,这个它都会讲清楚。4.&nbsp;它可以帮你找可以用同类型解法来做的题。然后它可以保持解法思路不变,用一个思路爽刷一个类型的题。如果题目之间思路又有变化,它会告诉你只有哪里变了,其他的地方还是老思路。5.&nbsp;它也可以直接帮你总结模板,易错点。经过chat老师的指导,我最大的改变是敢刷题了。之前刷题需要先找某一个人写的算法题repo,然后跟着某一个人他的思路刷他给的几个题。如果想写别的题,套用思路失败了,没有他的题解,也不知道到底哪里错了;看别人的题解,思路又乱了。这个问题在二分查找和dp类型的题里面特别常见。但是现在有chat老师,他会针对我的代码告诉我我哪里想错了,应该怎么做;还按照我写代码的习惯帮我总结了一套属于我的刷题模板。每天写题全是正反馈!
牛客981:不刷才是爽
AI时代的工作 VS 传...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务