错排问题
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. 小结
错排数的递推式常见有三种写法:
- 标准形式:\[D_n = (n-1)(D_{n-1}+D_{n-2}) \]
- 展开形式:\[D_n = (n-1)D_{n-1} + (n-1)D_{n-2} \]
- 另一种简洁形式:\[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;
}

浙公网安备 33010602011771号