[SDOI2016] 排列计数 题解

注意,本文章代码由人类编写但是文章由AI书写

原文地址

题目大意

求有多少种 1n 的排列 a,满足恰好有 m 个位置 i 使得 a_i = i。答案对 10^9 + 7 取模。

解题思路

我们需要找出恰好 m 个位置固定的排列数。这个问题可以分为两部分:

  1. 选择固定点:从 n 个位置中选择 m 个作为固定点,组合数为 C(n, m)
  2. 错位排列:剩下的 n - m 个位置必须都不在原来的位置上,称为错位排列,记作 D(n - m)

总方案数为两者的乘积,即 C(n, m) \times D(n - m) \mod (10^9 + 7)

组合数计算

组合数 C(n, m) 可以通过预处理阶乘和阶乘的逆元来快速计算。公式为:

C(n, m) = \frac{n!}{m! \times (n - m)!} \mod p

其中 p = 10^9 + 7

错位排列数

错位排列数 D(k) 满足递推关系:

  • D(0) = 1(空排列视为一个错排)
  • D(1) = 0
  • k \geq 2 时,D(k) = (k - 1) \times (D(k - 1) + D(k - 2)) \mod p

预处理

  1. 阶乘数组:预处理 fact[i] = i! \mod p
  2. 阶乘逆元数组:利用费马小定理预处理 invfact[i] = (i!)^{-1} \mod p
  3. 错位排列数组:预处理 der[i] 表示 D(i)

代码实现

预处理阶乘、逆元和错位排列数组后,每个测试用例只需 O(1) 时间计算答案。

边界情况

  • m > n 时,答案为 0
  • n = m 时,答案为 1(因为 D(0) = 1)。

示例代码

#include <iostream>
using namespace std;

typedef long long ll;
const ll maxn = 1e6, p = 1e9 + 7;
ll fact[maxn + 5], invfact[maxn + 5], der[maxn + 5];

ll qpow(ll b, ll e) {
    ll res = 1;
    while (e) {
        if (e & 1) res = res * b % p;
        b = b * b % p;
        e >>= 1;
    }
    return res;
}

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

    // 预处理阶乘
    fact[0] = 1;
    for (ll i = 1; i <= maxn; ++i)
        fact[i] = fact[i - 1] * i % p;

    // 预处理阶乘逆元
    invfact[maxn] = qpow(fact[maxn], p - 2);
    for (ll i = maxn - 1; i >= 0; --i)
        invfact[i] = invfact[i + 1] * (i + 1) % p;

    // 预处理错位排列
    der[0] = 1;
    der[1] = 0;
    for (ll i = 2; i <= maxn; ++i)
        der[i] = (i - 1) * (der[i - 1] + der[i - 2]) % p;

    int T;
    cin >> T;
    while (T--) {
        ll n, m;
        cin >> n >> m;
        if (m > n) {
            cout << "0\n";
            continue;
        }
        ll C = fact[n] * invfact[m] % p * invfact[n - m] % p;
        cout << der[n - m] * C % p << '\n';
    }
}

复杂度分析

  • 预处理O(n),每个数组的预处理均为线性复杂度。
  • 查询:每次查询 O(1),总复杂度 O(T)

该算法能够高效处理 n \leq 10^6T \leq 5 \times 10^5 的情况。