关于求阶乘逆元

以下思路背景均来自题目牡牛和牝牛


  • 预处理做法

众所周知:

\[C_n^m=\frac{n!}{m!(n-m)!} \]

众所周知知:

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\)的值很大时,时间复杂度会大幅上升。

例如

image

  • 预处理优化

一个小优化:可以只需预处理阶乘。

预处理为:

	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; 
}
少预处理一个数组的时间优化还是挺多的。

image

这道题\(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;
}
  • 总结

上面三种做法都很常用,主要依据于大家自己的习惯。

做题时应先根据题意做出判断,如果预处理的范围超过了数组能承受的大小,则只能选用(大概)第三种方法;若询问次数较多,则前两种方法更优。

所以:寸有所长 尺有所短还是都记下来最保险。

完结撒花

image

posted @ 2024-04-13 19:26  DrRatio  阅读(71)  评论(4编辑  收藏  举报