题目链接: https://ac.nowcoder.com/acm/contest/54484/B
题意很简单,但是数据范围偏大。
错排公式
首先来推导一下错排公式:
\[D(n) = n!\sum_{k=0}^{n}\frac{(-1)^k}{k!} \]
设一个函数:
\[S_i表示一个排列中p_i = i的方案数 \]
那么我们可以知道:
\[D(n) = n! - |\cup_{i=1}^{n}S_i| \]
这个表示 所有方案数 减去 至少有一个位置放对的方案数 。
现在来考虑一下如何处理后面这个并集,并集往往是不好求的,而 交集会好求很多 ,所以在求并集的时候我们往往采取容斥原理将一个并集 转换成诸多交集的加减运算 。
我们用一个图可以来表示当 n = 3 的情况:
其中有:
\[|S_1 \cup S_2 \cup S_3| = |S_1| + |S_2| + |S_3| - |S_1 \cap S_2| - |S_1 \cap S_3| - |S_2 \cap S_3| + |S_1 \cap S2 \cap S_3| \]
扩展一下就可以得到下面的柿子:
\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}(-1)^k\sum_{1\leq i_1 \leq i_2 \leq ... \leq i_k \leq n}|S_{i1}\cap S_{i2} ... \cap S_{ik}| \]
然后有:
\[\sum_{1\leq i_1 \leq i_2 \leq ... \leq i_k \leq n}|S_{i1}\cap S_{i2} ... \cap S_{ik}| = C_{n}^{k}(n-k)! \]
这个表示啥呢,左边这个柿子的含义其实是 i1 ~ ik 都放对了,其他位置上无所谓的方案数,就等同于在 n 个位置中选择 k 个放对,剩下的随便放的方案数。
所以可得下面的柿子:
\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}(-1)^kC_{n}^{k}(n-k)! \]
然后化简得:
\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}\frac{(-1)^k n!}{k!} \]
然后代回到一开始的答案表达式中:
\[D(n) = n! - \sum_{k=1}^{n}\frac{(-1)^k n!}{k!} \]
把 n! 提出来,再化简一下得到:
\[D(n) = n! \sum_{k=0}^{n}\frac{(-1)^k}{k!} \]
回到本题
但是有这个柿子依然不好写这题,这题如果是 1e7 就可以直接O(n)写了,但是这题是 1e9 的数据范围,可以考虑一下分段打表(一般 要求函数可以递推 ),但是这个表达式好像不是很好打,我们来分析一下。
首先网上有一个比较有名递推式(证明略):
\[D(n) = (n-1)[D(n - 1) + D(n - 2)] \]
这个递推需要用到前两项,也就是说我们需要打两个表,然后才可以做,有点麻烦,但是其实是可以只用一项的。
我看网路上都没有用下面这种方式递推的,我在这里写一下。
我们考虑 D(n) -> D(n + 1) 这样的转移:
\[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!} \newline = (n + 1)![\sum_{k=0}^{n}\frac{(-1)^k}{k!} + \frac{(-1)^{n + 1}}{(n + 1)!}] \newline = (n + 1)!\sum_{k=0}^{n}\frac{(-1)^k}{k!} + (-1)^{n + 1} \newline = (n + 1) \times n!\sum_{k=0}^{n}\frac{(-1)^k}{k!} + (-1)^{n + 1} \newline = (n+1) \times D(n) + (-1)^{n+1}\]
然后令段大小 T = 1e7 打表打出 D(0), D(T), D(2T) ... D(100T) 就好了。
最终的复杂度是 O(n) 但是常数极小,所以可以过。
Code:
#include <bits/stdc++.h> #define int long long using namespace std; const int p = 1e9 + 7, T = 1e7; int a[110] = { 1,824182295,933713113,474482547,930651136,251064654,637937211,229643390,307311871,448853213, 322273426,398890147,194914852,884947442,154199209,881788023,389699639,733217502,601739182, 372305477,213823357,713959988,498202615,196342945,324300550,154001751,974475946,540773759, 467881322,257531902,598680559,367927849,971346692,94577421,617165552,128327758,503709458, 253566817,820144401,13965056,82358069,805941568,533047638,69430220,686678173,297170813, 34546238,323435423,499126069,487532712,468899710,790590914,581347156,955359050,700529992, 518280890,98592091,64544225,988209678,422603955,40661679,174468756,573631136,757555557, 710709955,775098981,499158883,969149294,880429710,42564126,333697951,522067888,579797877, 528967798,717694718,309384913,31308092,316850320,220854491,878646494,963974981,377654637, 705101053,542246848,466289530,750036412,819636314,688721174,464087273,517164631,256789690, 482685016,276682441,473333947,340221393,762927538,624766601,984537252,977632075,34192646, 402182971,977005016 }; int mo(int x){return (x % p + p) % p;} void solve() { int n;cin >> n; int ans = a[n / T]; for(int i = n / T * T + 1;i <= n; ++ i)ans = mo(ans * i % p + ((i & 1) ? -1 : 1)); cout << ans << '\n'; } void table() { int x = 1;//d(0) = 1,这个有点特殊 cout << x << ","; int cnt = 1; for(int i = 1;i <= 1e9; ++ i) { x = x * i % p; if(i & 1)x = (x - 1 + p) % p; else x = (x + 1) % p; if(i % T == 0) { cout << x << ","; cnt ++; } if(cnt % 10 == 0) { cout << '\n'; cnt = 1; } } } signed main() { table(); solve(); //return 0; }
查看更多关于【ACM组合数学 | 错排公式】写信的详细内容...