Skip to content

Inversions of all permutations - 题解

标签与难度

标签: 组合数学, q-analog, q-阶乘, 生成函数, 数论, 模运算, 快速幂 难度: 2100

题目大意喵~

你好呀,指挥官!艾米小姐姐又给B先生出了道难题呢,我们一起来帮帮他吧,喵~

题目是这样哒:给定一个长度为 nn 的数组 aa 和一个底数 bb。这个数组 aa 里面可能会有重复的数字哦。

我们要考虑数组 aa 的所有不同的排列。对于每一种排列,我们都计算出它的逆序对数量,记作 tt。然后呢,对于每一种排列,我们都计算一个值 btb^t

最后,我们需要把所有不同排列算出来的 btb^t 全部加起来,得到一个总和。因为这个总和可能会非常非常大,所以结果需要对 10000000071000000007 取模。

简单来说,就是要计算这个式子:

{ri}是 a 的一个不同排列bt({ri})(mod1000000007)\sum_{\{r_i\} \text{是 } a \text{ 的一个不同排列}} b^{t(\{r_i\})} \pmod{1000000007}

解题思路分析

这道题看起来好吓人呀,要枚举所有排列,那不是要累死我了喵!(>ω<) でも大丈夫 (Don't worry),这种题目肯定有巧妙的数学方法!让我来给你分析一下吧~

第一步:从最简单的情况开始思考

我们先假设数组 aa 里的 nn 个数字都是互不相同的,比如说就是 {1, 2, ..., n}。 这时候,我们要计算的就是:

σSnbinv(σ)\sum_{\sigma \in S_n} b^{\text{inv}(\sigma)}

其中 SnS_n{1, 2, ..., n} 的所有排列的集合,inv(σ)\text{inv}(\sigma) 是排列 σ\sigma 的逆序对数。

这个和式其实在组合数学里是一个非常经典的存在哦!它被称为 q-阶乘 (q-factorial) 的一个展开形式,只不过这里的 qq 换成了我们题目里的 bb

为了理解它,我们先来定义一个叫做 b-模拟 (b-analog) 的概念。数字 kk 的 b-模拟,我们记作 [k]b[k]_b,定义是:

[k]b=1+b+b2++bk1[k]_b = 1 + b + b^2 + \dots + b^{k-1}

b1b \neq 1 时,它可以用等比数列求和公式写成 [k]b=bk1b1[k]_b = \frac{b^k - 1}{b - 1}。 当 b=1b = 1 时,[k]1=1+1++1[k]_1 = 1+1+\dots+1 (kk个1),所以 [k]1=k[k]_1 = k

b-阶乘 (b-factorial),记作 [n]b![n]_b!,就是把这些 b-模拟乘起来:

[n]b!=[1]b[2]b[n]b[n]_b! = [1]_b \cdot [2]_b \cdot \dots \cdot [n]_b

一个非常神奇的定理(MacMahon's Master Theorem 的一个特例)告诉我们:

σSnbinv(σ)=[n]b!\sum_{\sigma \in S_n} b^{\text{inv}(\sigma)} = [n]_b!

举个栗子看看,喵~ 当 n=3n=3, 数组是 {1, 2, 3} 时:

  • 排列 {1, 2, 3} 有 0 个逆序对, 贡献 b0b^0
  • 排列 {1, 3, 2} 有 1 个逆序对, 贡献 b1b^1
  • 排列 {2, 1, 3} 有 1 个逆序对, 贡献 b1b^1
  • 排列 {2, 3, 1} 有 2 个逆序对, 贡献 b2b^2
  • 排列 {3, 1, 2} 有 2 个逆序对, 贡献 b2b^2
  • 排列 {3, 2, 1} 有 3 个逆序对, 贡献 b3b^3 总和是 b0+2b1+2b2+b3=(1+b)(1+b+b2)b^0 + 2b^1 + 2b^2 + b^3 = (1+b)(1+b+b^2)

而根据我们的公式: [3]b!=[1]b[2]b[3]b=(1)(1+b)(1+b+b2)[3]_b! = [1]_b \cdot [2]_b \cdot [3]_b = (1) \cdot (1+b) \cdot (1+b+b^2)。 看吧,完全一样!是不是很神奇,喵!

第二步:处理有重复元素的情况

好啦,现在回到原题,如果数组 aa 里有重复的数字该怎么办呢?比如 a={1,1,2}a = \{1, 1, 2\}

这种情况是 b-阶乘的推广,叫做 b-多项式系数 (b-multinomial coefficient)。 假设数组 aa 中,数字 v1v_1 出现了 c1c_1 次,数字 v2v_2 出现了 c2c_2 次,...,数字 vmv_m 出现了 cmc_m 次,其中 c1+c2++cm=nc_1 + c_2 + \dots + c_m = n

那么我们要求的总和就是:

(nc1,c2,,cm)b=[n]b![c1]b![c2]b![cm]b!\binom{n}{c_1, c_2, \dots, c_m}_b = \frac{[n]_b!}{[c_1]_b! \cdot [c_2]_b! \cdot \dots \cdot [c_m]_b!}

这个公式是不是也很有规律呀?我们可以验证一下:

  • 如果所有元素都不同,那么所有的 cic_i 都等于 1。因为 [1]b!=[1]b=1[1]_b! = [1]_b = 1,分母就变成了 1,公式退化为 [n]b![n]_b!,和我们第一步的结论一致。
  • 如果 b=1b=1,我们知道 [k]1=k[k]_1 = k,所以 [k]1!=k![k]_1! = k!。公式就变成了 n!c1!c2!cm!\frac{n!}{c_1! \cdot c_2! \cdot \dots \cdot c_m!}。这正好是普通多重集排列的计数公式!这也说明我们的公式非常可靠,喵~

第三步:制定计算策略

现在我们有了制胜法宝(公式),就可以制定作战计划啦!

  1. 统计频率: 首先,我们要遍历一遍输入的数组 aa,用一个哈希表(std::map)或者数组来统计每个数字出现的次数 cic_i

  2. 预计算 b-阶乘: 我们的公式里需要用到 [k]b![k]_b! 这种东西。为了不每次都重新算,我们可以预先计算出从 [0]b![0]_b![n]b![n]_b! 的所有值,存进一个数组里。

    • [0]b!=1[0]_b! = 1
    • 我们可以递推地计算 [k]b![k]_b!。首先需要 [k]b=1+b++bk1[k]_b = 1 + b + \dots + b^{k-1}
    • 我们可以维护一个前缀和 b_sum_prefixbb 的幂 b_power
    • b_sum_prefix_k = [k]_b
    • q_factorial[k] = q_factorial[k-1] * b_sum_prefix_k
    • 这个方法对于 b=1b=1b1b \neq 1 都适用,非常优雅!
  3. 计算最终结果:

    • 分子就是我们预计算好的 q_factorial[n]
    • 分母是所有 q_factorial[c_i] 的乘积。
    • 最终答案就是 分子 * (分母的模逆元)。分母的模逆元可以用快速幂来求,即 power(denominator, MOD - 2)

这样一来,问题就迎刃而解啦!我们把一个看似复杂的问题,转化成了一步步清晰的计算,这就是数学的魅力呀,喵~

代码实现

这是我根据上面的思路,精心为你准备的代码哦~ 注释写得很详细,希望能帮到你!

cpp
#include <iostream>
#include <vector>
#include <map>

// 定义一个我喜欢的模数~
const int MOD = 1000000007;
const int MAXN = 100005;

// 用于存储 b-阶乘 的数组
long long q_factorial[MAXN];

// 快速幂函数,用来求 a^p (mod MOD),也是求模逆元的基础哦
long long power(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

// 模逆元函数
long long modInverse(long long n) {
    return power(n, MOD - 2);
}

// 预计算 b-阶乘 [k]_b! for k from 0 to n
void precompute_q_factorials(int n, int b) {
    q_factorial[0] = 1;

    // b=1 是个特殊情况,[k]_1 = k,所以 [k]_1! = k!
    if (b == 1) {
        for (int i = 1; i <= n; ++i) {
            q_factorial[i] = (q_factorial[i - 1] * i) % MOD;
        }
        return;
    }

    // 对于 b != 1 的一般情况
    // 我们用 b_power 记录 b^i,用 b_sum_prefix 记录 [k]_b = 1 + b + ... + b^(k-1)
    long long b_power = 1;
    long long b_sum_prefix = 0;

    for (int k = 1; k <= n; ++k) {
        // 更新 b_sum_prefix 来得到 [k]_b
        b_sum_prefix = (b_sum_prefix + b_power) % MOD; // b_sum_prefix 现在是 [k]_b
        
        // 更新 b_power 为下一次迭代做准备
        b_power = (b_power * b) % MOD;
        
        // 计算 [k]_b!
        long long q_k = b_sum_prefix; // [k]_b
        q_factorial[k] = (q_factorial[k - 1] * q_k) % MOD;
    }
}


int main() {
    // 加速输入输出,让我跑得更快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n;
    long long b;
    std::cin >> n >> b;

    // 使用 map 来统计每个数字的出现次数,喵~
    std::map<int, int> counts;
    for (int i = 0; i < n; ++i) {
        int val;
        std::cin >> val;
        counts[val]++;
    }

    // 预计算 b-阶乘
    precompute_q_factorials(n, b);

    // 分子是 [n]_b!
    long long numerator = q_factorial[n];

    // 分母是所有 [c_i]_b! 的乘积
    long long denominator = 1;
    for (auto const& [val, count] : counts) {
        denominator = (denominator * q_factorial[count]) % MOD;
    }

    // 最终答案 = 分子 * (分母的模逆元)
    long long answer = (numerator * modInverse(denominator)) % MOD;

    // 如果结果是负数,要转成正数哦
    if (answer < 0) {
        answer += MOD;
    }

    std::cout << answer << std::endl;

    return 0;
}

复杂度分析

  • 时间复杂度: O(N+Mlog(MOD))O(N + M \log(\text{MOD}))

    • 统计频率:如果使用 std::map,需要 O(NlogM)O(N \log M),其中 MM 是不同元素的数量。如果用 std::unordered_map 或值域不大用数组,可以是 O(N)O(N)
    • 预计算 q_factorial:我们进行了一次线性扫描,复杂度是 O(N)O(N)
    • 计算分母:遍历 map 中的 MM 个元素,复杂度是 O(M)O(M)
    • 计算模逆元:快速幂的复杂度是 O(log(MOD))O(\log(\text{MOD}))
    • 综合起来,主要瓶颈在于频率统计和预计算,所以总时间复杂度大约是 O(N)O(N)(假设哈希表操作是 O(1)O(1))。
  • 空间复杂度: O(N+M)O(N + M)

    • q_factorial 数组占用了 O(N)O(N) 的空间。
    • counts 这个 map 最多存储 MM 个不同的元素,占用 O(M)O(M) 的空间。
    • 所以总空间复杂度是 O(N+M)O(N + M)。由于 MNM \le N,也可以说是 O(N)O(N)

知识点总结

这道题真是个小宝库呢,能学到好多东西!

  1. q-模拟 (q-analog): 这是组合数学中一个非常核心的概念,它将许多经典的计数公式(如阶乘、二项式系数)推广到带有参数 qq 的形式。我们这道题就是 q=bq=b 的情况。
  2. q-阶乘与逆序对: 核心知识点是排列的逆序对生成函数与 q-阶乘的关系。σSnqinv(σ)=[n]q!\sum_{\sigma \in S_n} q^{\text{inv}(\sigma)} = [n]_q!
  3. q-多项式系数: 对于多重集排列,这个关系推广为 q-多项式系数。这是解决本题的关键公式。
  4. 模运算: 解决大数问题必备的技巧,包括加、乘、求逆元等。
  5. 快速幂: 高效计算模幂和模逆元的不二之选。
  6. 预计算思想: 当一个值(比如 [k]b![k]_b!)需要被多次使用时,提前把它们都算出来存好,可以大大提高效率。

希望这篇题解能让你对这个问题有更深的理解,喵~ 如果还有不懂的地方,随时可以再来问我哦!加油,指挥官!(^▽^)