今天我们来拆解一道经典的排列组合问题——SDOI2016的排列计数题目。这道题看似简单,却融合了组合数学中的多个核心概念,特别适合用来训练数学思维和编程实现能力。
题目要求我们计算1到n的排列中,恰好有m个位置满足a_i=i的排列数量。换句话说,就是要在n个位置中精确选择m个位置保持不动,其余n-m个位置都必须错位排列。
要解决这个问题,我们需要理解两个关键数学概念:
问题的解可以表示为:C(n,m) × D(n-m)
这个公式的直观理解是:首先从n个位置中选择m个保持不动(组合数部分),然后剩下的n-m个位置必须全部错位排列(错排数部分)。
错位排列数D(n)有一个经典的递推公式:
D(n) = (n-1) × [D(n-1) + D(n-2)]
这个递推关系的推导思路是:
为了高效计算组合数C(n,m) = n! / (m! × (n-m)! ),我们需要预处理阶乘数组f和逆元数组inv:
cpp复制const int MAXN = 1000005, mod = 1000000007;
ll f[MAXN], inv[MAXN], d[MAXN];
void prework() {
f[0] = 1;
for(int i = 1; i < MAXN; i++) {
f[i] = f[i-1] * i % mod;
inv[i] = qpow(f[i], mod-2);
}
// 错位排列预处理
d[1] = 0, d[2] = 1, d[3] = 2;
for(int i = 4; i < MAXN; i++) {
d[i] = (i-1) * (d[i-1] + d[i-2]) % mod;
}
}
这里使用了费马小定理来计算逆元,因为模数1000000007是质数,所以a的逆元就是a^(mod-2)。
快速幂函数qpow用于计算逆元:
cpp复制ll qpow(ll a, ll b) {
ll ans = 1;
while(b) {
if(b & 1) ans = a * ans % mod;
a = a * a % mod;
b >>= 1;
}
return ans;
}
这个实现使用了二进制分解的方法,将幂次b分解为2的幂次和,时间复杂度为O(logb)。
对于每组查询(n,m),处理逻辑如下:
cpp复制if (n - m == 1) printf("0\n");
else if (m == n) printf("1\n");
else if (m == 0) printf("%lld\n", d[n]);
else {
printf("%lld\n", f[n] * inv[m] % mod * inv[n-m] % mod * d[n-m] % mod);
}
这里处理了三种特殊情况:
一般情况则计算C(n,m) × D(n-m),注意每一步都要取模防止溢出。
预处理阶乘、逆元和错位排列数的时间复杂度都是O(MAXN),其中MAXN=1e6。这个预处理只需要执行一次,之后所有查询都可以在O(1)时间内完成。
每组查询的处理时间是O(1),因为所有需要的值都已经预处理好了。对于T=5e5组查询,总查询时间是O(T),这在题目限制下是完全可行的。
我们使用了三个数组f、inv和d,每个大小都是MAXN=1e6,空间复杂度是O(MAXN)。在大多数现代编程环境中,这大约需要24MB的内存(3个1e6的long long数组),完全在合理范围内。
在实际编码中,特别容易忽略边界条件的处理。例如:
提示:在编写代码时,建议先单独测试这些边界情况,确保逻辑正确。
在模运算中,有几个常见陷阱需要注意:
在我们的代码中,组合数的计算使用了预先计算的逆元,避免了实时计算的高开销。
这个问题的解法可以扩展到许多实际应用中:
理解错位排列的性质,还能帮助我们解决其他类似问题,例如:
让我们再仔细看看代码中的一些关键实现细节:
cpp复制inv[i] = qpow(f[i], mod-2);
这里我们计算的是i!的逆元,而不是i的逆元。这样设计是为了后续计算组合数更方便:
C(n,m) = n! / (m! × (n-m)!) = f[n] × inv[m] × inv[n-m] % mod
cpp复制d[1] = 0, d[2] = 1, d[3] = 2;
这三个初始值对应:
cpp复制scanf("%lld%lld", &n, &m);
printf("%lld\n", ...);
使用%lld来读取和输出long long类型变量,确保在n和m较大时也能正确处理。
为了确保代码的正确性,可以采用以下测试策略:
一个实用的测试技巧是预先计算一些已知值,例如:
如果题目要求变化,我们可以相应调整算法:
另一个有趣的扩展是考虑部分元素有重复时的错位排列问题,这需要更复杂的组合数学技巧。
在实际编程比赛中,掌握这类排列计数问题的解法非常重要。它们经常出现在组合数学、概率统计相关的题目中,也是许多动态规划问题的基础。