错排问题

https://www.bilibili.com/video/BV1ZwmtYwEsK
第一个视频
https://www.bilibili.com/video/BV1Mw4m1y7xT

错排模板:
https://www.luogu.com.cn/problem/P1595

部分错排问题:
https://www.luogu.com.cn/problem/P4071
部分错排, n个里面选 m 个是正确位置,剩余 n-m 个错排, 所以答案就是

\[C_n^m*D_{n-m} \]

高精度加错排:
https://www.luogu.com.cn/problem/P3182

1. 什么是错排?

错排(Derangement)指的是:
\(n\) 个物品排成一个排列,使得 没有任何一个物品在原来的位置上

例如:

  • \(n = 3\) ,原始序列是 \([1,2,3]\)
    * 所有排列有 \(6\) 种:
    \([1,2,3]\) , \([1,3,2]\) , \([2,1,3]\) , \([2,3,1]\) , \([3,1,2]\) , \([3,2,1]\)
    * 满足错排条件的有: \([2,3,1]\) , \([3,1,2]\) ,共 2 种。

2. 错排数公式

错排数\(D_n\)

有两种常见写法:

递推公式

\[D_n = (n-1)(D_{n-1} + D_{n-2}) \]

  • 初始条件:

    \[D_1 = 0, \quad D_2 = 1 \]

推导思路:

  • 考虑第 \(n\) 个元素,它不能放在位置 \(n\) ,它有 \(n-1\) 种位置可以放。
  • 假设它放到位置 \(k\) ,那么:
    • 如果位置 \(k\) 的原本元素放到位置 \(n\) ,则剩下 \(n-2\) 个数错排,方案数为 \(D_{n-2}\)
    • 如果位置 \(k\) 的原本元素不放到位置 \(n\) ,则剩下 \(n-1\) 个数错排,方案数为 \(D_{n-1}\)
  • 总和得递推公式。

显式公式

错排数还可以用阶乘和容斥原理表示:

\[D_n = n! \sum_{k=0}^n \frac{(-1)^k}{k!} \]

即近似于:

\[D_n = \left\lfloor \frac{n!}{e} + \frac{1}{2} \right\rfloor \]

(四舍五入 \(\frac{n!}{e}\) 就是 \(D_n\)


3. 小数据表

\[\begin{aligned} D_1 &= 0 \\ D_2 &= 1 \\ D_3 &= 2 \\ D_4 &= 9 \\ D_5 &= 44 \\ D_6 &= 265 \\ D_7 &= 1854 \\ D_8 &= 14833 \\ \end{aligned} \]


4. C++ 实现(OI 风格)

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

const int MOD = 1e9 + 7; // 如果题目要求取模

long long derangement(int n) {
    if (n == 1) return 0;
    if (n == 2) return 1;
    vector<long long> D(n + 1, 0);
    D[1] = 0, D[2] = 1;
    for (int i = 3; i <= n; i++) {
        D[i] = ( (i - 1) * (D[i - 1] + D[i - 2]) ) % MOD;
    }
    return D[n];
}

int main() {
    int n;
    cin >> n;
    cout << derangement(n) << endl;
    return 0;
}

✅ 总结:

  • 错排问题的核心公式是 递推式 \(D_n = (n-1)(D_{n-1}+D_{n-2})\)
  • 如果数据很大,可以用递推取模。
  • 如果要精确值,可以用容斥公式 \(D_n = n!\sum_{k=0}^n \frac{(-1)^k}{k!}\)

1. 常见的递推式(标准形式)

\[D_n = (n-1)\big(D_{n-1} + D_{n-2}\big) \]


2. 另一种形式(展开后)

\[D_n = (n-1)D_{n-1} + (n-1)D_{n-2} \]

这就是把括号拆开了,很多资料会直接写成这种形式。


3. 通过组合恒等式得到的另一种递推

利用错排的显式公式

\[D_n = n! \sum_{k=0}^n \frac{(-1)^k}{k!} \]

可以推得:

\[D_n = nD_{n-1} + (-1)^n \]

验证一下:

  • \(n=2\)

    \[D_2 = 2D_1 + (-1)^2 = 0 + 1 = 1 \quad \checkmark \]

  • \(n=3\)

    \[D_3 = 3D_2 + (-1)^3 = 3 \times 1 - 1 = 2 \quad \checkmark \]

这条公式非常简洁,经常用于快速递推。


4. 小结

错排数的递推式常见有三种写法:

  1. 标准形式:

    \[D_n = (n-1)(D_{n-1}+D_{n-2}) \]

  2. 展开形式:

    \[D_n = (n-1)D_{n-1} + (n-1)D_{n-2} \]

  3. 另一种简洁形式:

    \[D_n = nD_{n-1} + (-1)^n \]


一、用容斥式(代数)推导 —— 最直接

我们知道错排的显式公式(由容斥原理得到)是

\[D_n = n!\sum_{k=0}^{n}\frac{(-1)^k}{k!}. \]

同样

\[D_{n-1} = (n-1)!\sum_{k=0}^{n-1}\frac{(-1)^k}{k!}. \]

\(D_{n-1}\) 乘以 \(n\)

\[nD_{n-1}=n!\sum_{k=0}^{n-1}\frac{(-1)^k}{k!}. \]

两式相减:

\[D_n - nD_{n-1} = n!\Big(\sum_{k=0}^{n}\frac{(-1)^k}{k!}-\sum_{k=0}^{n-1}\frac{(-1)^k}{k!}\Big) = n!\cdot\frac{(-1)^n}{n!}=(-1)^n. \]

于是得

\[D_n = nD_{n-1} + (-1)^n. \]

推导完毕,干净利落。


二、用常见递推和归纳证明 —— 更“递推式”风格

已知基础递推:

\[D_n=(n-1)(D_{n-1}+D_{n-2}). \]

我们用数学归纳法证明 \(D_n=nD_{n-1}+(-1)^n\)

  • 基础情形: \(n=1\)\(n=2\) 易验证成立。
    • \(D_1=0=1\cdot D_0+(-1)^1\) (按需可定义 \(D_0=1\)
    • \(D_2=1=2\cdot D_1+(-1)^2\)
  • 归纳步:假设对 \(n-1\)\(n-2\) 都成立,即

    \[D_{n-1}=(n-1)D_{n-2}+(-1)^{n-1}. \]

    那么

    \[\begin{aligned} D_n &= (n-1)(D_{n-1}+D_{n-2})\\ &= (n-1)D_{n-1} + (n-1)D_{n-2}\\ &= nD_{n-1} - D_{n-1} + (n-1)D_{n-2}\\ &= nD_{n-1} -\big(D_{n-1} - (n-1)D_{n-2}\big). \end{aligned} \]

    由归纳假设 \(D_{n-1}-(n-1)D_{n-2}=(-1)^{\,n-1}\) ,所以

    \[D_n = nD_{n-1} - (-1)^{\,n-1} = nD_{n-1} + (-1)^n. \]

    完成归纳。

附:用这个递推快速计算的 C++(OI 风格,带注释)

下面给出两种实现:精确(不取模) 的版本(适合 \(n\) 不大时),以及取模版本(常见于题目要求对 \(M\) 取模,注意处理 \((-1)^n\) 的负数情况)。

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

// 精确版本(n 不大时用)
// D[0] = 1 (按容斥惯例), D[1]=0
ll derange_exact(int n) {
    if (n == 0) return 1; // 方便推导,有时定义 D0 = 1
    if (n == 1) return 0;
    vector<ll> D(n+1);
    D[0] = 1;
    D[1] = 0;
    for (int i = 2; i <= n; ++i) {
        // 使用 D_i = i * D_{i-1} + (-1)^i
        ll sign = (i % 2 == 0) ? 1 : -1;
        D[i] = i * D[i-1] + sign;
    }
    return D[n];
}

// 取模版本(常用于比赛)
// 注意 (-1)^n 在模意义下要转为 (mod-1) 或 1
ll derange_mod(int n, int mod) {
    if (n == 0) return 1 % mod;
    if (n == 1) return 0;
    vector<ll> D(n+1);
    D[0] = 1 % mod;
    D[1] = 0;
    for (int i = 2; i <= n; ++i) {
        ll sign = (i % 2 == 0) ? 1 : (mod - 1); // (-1) mod = mod-1
        D[i] = ( ( (ll)i * D[i-1] ) % mod + sign ) % mod;
    }
    return D[n];
}

int main() {
    int n;
    if (!(cin >> n)) return 0;
    cout << derange_exact(n) << "\n";
    // 如果要取模,例如 MOD=1e9+7,调用 derange_mod(n, 1000000007)
    return 0;
}
posted @ 2025-09-12 17:06  katago  阅读(16)  评论(0)    收藏  举报