bzoj4517: [Sdoi2016]排列计数--数学+拓展欧几里得
这道题是数学题,由题目可知,m个稳定数的取法是Cnm
然后剩下n-m本书,由于编号为i的书不能放在i位置,因此其方法数应由错排公式决定,即D(n-m)
错排公式:D[i]=(i-1)*(D[i-1]+D[i-2]);
所以根据乘法原理,答案就是Cnm * D(n-m)
接下来就是怎么求组合数的问题了
由于n≤1000000,因此只能用O(n)的算法求组合,这里用乘法逆元(inv[])来辅助求组合数
即 Cnm = n! / ((n-m)! * m!) = fac[n]*inv[n-m]*inv[m]
那么乘法逆元是什么呢?
假设一个数a,且a关于P的乘法逆元为x
那么 ax≡1 (mod P). 当且仅当 a 与 P 互质时x有解
简单的说,就是找一个数x,使得(x*a) mod P = 1
不难得出三者符合 ax+Py=1 (裴蜀定理), y可能是负数
因此我们可以用拓展欧几里得算出x的值,即为乘法逆元(用inv保存)
对于求出inv的过程,我们可以不必每次暴力求拓展欧几里得,可由下列递推式O(n)求出
inv[i]=(i+1)*inv[i+1]
而D数组只要O(n)推即可,其中D[0]=1, D[1]=0;
这道题让我明白。。组合数可以O(n)求得,了解了乘法逆元是什么,并且了解到世界上有个叫错排公式的神奇东西Orz
1 #include<stdio.h> 2 #include<algorithm> 3 #include<string.h> 4 #define LL long long 5 using namespace std; 6 const int maxn = 1000005; 7 const LL MOD = 1e9+7; 8 int T,n,m; 9 LL f[maxn],inv[maxn],d[maxn]; 10 11 inline void read(int &x){ 12 char c=getchar(); x=0; 13 while (c<'0' || c>'9') c=getchar(); 14 while (c>='0' && c<='9') x=x*10+c-48, c=getchar(); 15 } 16 17 inline LL ex_gcd(LL &x, LL &y, LL a, LL b){ 18 if (b==0){ 19 x=1; y=0; 20 return a; 21 } 22 LL res=ex_gcd(x,y,b,a%b); 23 LL t=x; x=y; 24 y=t-a/b*x; 25 return res; 26 } 27 28 inline LL calc(LL a, LL b){ 29 LL x,y; 30 if (ex_gcd(x,y,a,b) == 1LL) 31 return (x+b)%b; 32 } 33 34 int main(){ 35 read(T); 36 f[0]=1; 37 for (int i=1; i<=maxn; i++) f[i]=f[i-1] * (LL)i % MOD; 38 inv[1000000]=calc(f[1000000],MOD); 39 for (int i=maxn-6; i>=0; i--) inv[i]=inv[i+1] * (LL)(i+1) % MOD; 40 d[0]=1; d[1]=0; d[2]=1; 41 for (int i=3; i<=maxn; i++) d[i]=(LL)(i-1)*(d[i-1]+d[i-2]) % MOD; 42 while (T--){ 43 read(n); read(m); 44 LL ans=1LL; 45 //printf("haha %lld %lld %lld %lld\n", f[n], inv[n-m], inv[m], d[n-m]); 46 ans=ans*f[n]*inv[n-m] % MOD; 47 ans=ans*inv[m] % MOD; 48 ans=ans*d[n-m] % MOD; 49 printf("%lld\n", ans); 50 } 51 return 0; 52 }