关于求阶乘逆元
以下思路背景均来自题目牡牛和牝牛
-
预处理做法
众所周知:
众所周知知:
c++中除法很不稳定,所以需要逆元。
所以真正在运用时,我们的函数会这么写:
int C(int a,int b)
{
if(a<b)
return 0;
return jc[a]*ny[b]%mod*ny[a-b]%mod;
}
啊哦忘说要取模了
这样写看着十分简便,不过需要预处理出\(jc\)(阶乘)与\(ny\)(阶乘逆元)的值。
而这个范围往往是很大的,一般情况下处理范围为\(0\)$mod-1$,但在范围$0$\(maxn\)之间也是可以实现的。
所以,范围右边界取值,应为\(min(mod,maxn)\),大多数情况有\(mod>maxn\),但是也有太大导致开不了数组的情况,这就得下面优化了。
故有:
jc[0]=ny[0]=1;
for(int i=1;i<=mod-1;i++)
jc[i]=jc[i-1]*i%mod;
ny[mod-1]=iv(jc[mod-1]);
for(int i=mod-2;i>=1;i--)
ny[i]=ny[i+1]*(i+1)%mod;
而这之中又出现了一个新的函数取逆元,它需要用到\(exgcd\),故又有:
void exgcd(int a,int b,int &x,int &y)
{
if(b==0)
{
x=1,y=0;
return;
}
int x0,y0;
exgcd(b,a%b,x0,y0);
x=y0;
y=x0-(a/b)*y0;
}
int iv(int a)
{
int x,y;
exgcd(a,mod,x,y);
return (x%mod+mod)%mod;
}
这种写法的好处在于预处理已经做完了几乎全部工作,整个程序的复杂度集中在这里,之后再进行大量运用\(C\)函数时会非常节省时间。
但问题也很明显:当\(mod\)的值很大时,时间复杂度会大幅上升。
例如
-
预处理优化
一个小优化:可以只需预处理阶乘。
预处理为:
jc[0]=1;
for(int i=1;i<=maxn;i++)
jc[i]=jc[i-1]*i%mod;
当然,这里范围取\(n\)还是\(mod-1\)是个玄学问题,请大佬指教%%%
组合函数为:
int C(int n,int m){
if(n<m)
return 0;
return jc[n]*qpow(jc[m]*jc[n-m]%mod,mod-2)%mod;
}
少预处理一个数组的时间优化还是挺多的。
这道题\(maxn\)和\(mod\)的时间差距还是挺大的,上面是\(maxn\)。
-
优化做法
优化后时间已经很短了,但有一种情况,它还是不能实现:
\(998244353\),\(1000000007\),都是比较常见的取模数,显然,数组是无法容纳它们的。
这时候,就有了另一种处理方式:优化掉预处理,在每次应用函数时算出每个阶乘。
它的特点也很明显:在处理单次或少次询问时有着更优的时间复杂度;也因此,它在询问次数较多时表现就很不尽人意了。
实现方法:
与上方优化后一样,用快速幂求逆元,只需要一个快速幂函数,而且不需要预处理,只是组合函数内东西稍微多了点。
int C(int a,int b)
{
int aa=1,bb=1;
fo(i,a-b+1,a)//这是化简后的阶乘结果
aa=aa*i%mod;
fo(i,1,b)
bb=bb*i%mod;
return aa*qpow(bb,mod-2)%mod;
}
-
总结
上面三种做法都很常用,主要依据于大家自己的习惯。
做题时应先根据题意做出判断,如果预处理的范围超过了数组能承受的大小,则只能选用(大概)第三种方法;若询问次数较多,则前两种方法更优。
所以:寸有所长 尺有所短,还是都记下来最保险。