组合数取模 合集

0. 介绍

组合数取模,即给定 n,m,p,求

(nm)modp

下面按数据范围讨论一些解法:

p / n,m n,m200 n,m106 n,m1018
p106p 为素数 杨辉三角暴力递推 Lucas 定理 Lucas 定理
p>109p 为素数 杨辉三角暴力递推 预处理阶乘及其逆元 exLucas
p=p1p2p3pkp1k2000 为互不相同的素数 杨辉三角暴力递推 Lucas 定理 CRT 合并 Lucas 定理 CRT 合并
p 无特殊限制 杨辉三角暴力递推 exLucas exLucas

p 的第二档本来想表达 p 远大于 n 的,但这样 n,m1018 的档就不可做了 qwq)
p 的第三档就是 p 为 square-free number)
(CRT 指中国剩余定理)

时空复杂度:

做法 预处理时间复杂度 询问时间复杂度 空间复杂度
杨辉三角暴力递推 O(n2) O(1) O(n2)
预处理阶乘及其逆元 O(n) O(1) O(n)
Lucas 定理 预处理 1p 的组合数,取决于所用的算法 O(plogn) 预处理 1p 的组合数,取决于所用的算法
Lucas 定理 CRT 合并 预处理 1p 的组合数,取决于所用的算法 O(Plogn+plogP) 预处理 1p 的组合数,取决于所用的算法
exLucas 无预处理,O(1) O(pklogn(lognk)+plogP) O(1)
多组询问 exLucas(模数相同) O(pk) O(logpkn) O(1)

在 Lucas 定理 CRT 合并中, P 是模数,p 是最大质因子

1. 杨辉三角暴力递推

我们有

(nm)=(n1m1)+(n1m)

(考虑 n 里选 m,讨论选不选第一个即可证明)

O(n2) 递推即可 .

2. 预处理阶乘及其逆元

众所周知

(nm)=n!m!(nm)!=n!m!1(nm)!1

O(n) 预处理阶乘及其逆元即可

3. Lucas 定理

Lucas 定理:

形式一


n,mpp 是质数)进制下表示为

nknk1nk2n1¯,mkmk1mk2m1¯

其中 0ni<p0mi<p .

则有:

(nm)(nkmk)(nk1mk1)(n1m1)(modp)


形式二(简单)

CmnCmmodpnmodpCm/pn/p

Lucas 定理的证明

xN+,且 x<p,则:

Cpx=p!x!(px)!=p(p1)!x(x1)!(px)!=pxCp1x1

由于 x<pp 是素数,所以 x 存在模 p 意义下的逆元,因此:

Cpxpx1Cp1x1(modp)

因为 px1Cp1x10(modp) ,故 Cpx0(modp).

由二项式定理,

(1+x)p=i=0pCpixi

除了 i=0,i=p 的项数,别的模 p 均为 0,所以:

(1+x)p1+xp(modp)

现在我们设:

{m/p=qmn/p=qn , {mmodp=rmnmodp=rn

从而:

{m=qmp+rmn=qnp+rn

再由二项式定理有

(1+x)m=i=0mCmixi

而同时又有:

(1+x)m=(1+x)qmp+rm=(1+x)qmp(q+x)rm=((1+x)q)mp(q+x)rm(1+xp)qm(1+x)rm=i=0qmCqmixipi=0rmCrmixi=i=0qmj=0rmCqmiCrmjxip+j(modp)

注意到 ip+j 正好能取到 0m 内所有整数一次,所以枚举 k=ip+j,得

(x+1)mk=0nCqmk/pCrmkmodpxk(modp)

结合二项式定理,得

k=0mCmkxkk=0nCqmk/pCrmkmodpxk(modp)

对比系数,得:

CmnCmmodpnmodpCm/pn/p=CqmqnCrmrn(modp)

命题获证 .

用形式一可以写出一个递推的代码:

int Lucas(int n,int m,int p)
{
	for (int i=0;i<p;i++)
	{
		C[i][0]=1;
		for (int j=1;j<=i;j++)
		{
			C[i][j]=C[i-1][j-1]+C[i-1][j];
			if (C[i][j]>=p) C[i][j]-=p;
		}
	} // init
	int ans=1;
	while (n||m){ans=ans*C[n%p][m%p]; n/=p; m/=p;}
	return ans;
}

而用形式二则可以写出一个递归的代码:

ll lucas(ll n,ll m){return (n<m)?0:(n<p)?C(n,m):C(n%p,m%p)*lucas(n/p,m/p)%p;}

4. Lucas 定理 CRT 合并

A=(nm)

{A(nm)(modp1)A(nm)(modp2)A(nm)(modpk)

用 Lucas 定理求组合数,然后 CRT 合并即可

正确性显然

5. exLucas

先把 p 分解质因数,令

p=i=1tpiki

用同样的做法,列出同余方程组,求出 (nm)modpiki 然后 CRT 合并 .

那么问题来了,(nm)modpiki 怎么求?

我们知道,(nm)=n!m!(nm)!,由于逆元可能不存在,考虑提取出质因子 pi

n!m!(nm)!n!piAm!piB(nm)!piCpiABC(modpiki)

其中 An! 中质因子 pi 的次数,B,C 同理 .

此时分母就存在逆元了,现在问题又转化成了求形如

n!pamodpk

先考虑如何计算 n!modpk .

举个例子(22!mod32

22!1×2×3×4×5×6×7×8×9×10×11×12×13×14×15×16×17×18×19×20×21×2237×(1×2×3×4×5×6×7)×(1×2×4×5×7×8×10×11×13×14×16×17×19×20×22)(modpk)

可以看出上式分为三个部分:
第一部分是 p 的幂,次数是小于等于 np 的倍数的个数(即 np).

第二部分是一个阶乘 7!,即 np!,可以递归求

第三部分是 n! 中与 p 互质的部分的乘积,这一部分具有如下性质(加个 pk):

1×2×4×5×7×810×11×13×14×16×17(modpk)

一般的,有

i,i,(i,p)=1pkii,(i,p)=1pk(i+tpk)(modpk)

整块快速幂小块暴力即可求出 n!modpk

回到原问题,求

n!pamodpk

分母全部由上述第一部分和第二部分贡献(第三部分和 p 互质)。而递归计算第二部分的时候已经除去了第二部分中的因子 p,所以最终的答案就是上述第二部分递归返回的结果和第三部分的乘积(与第一部分无关) .

然后一层层回去即可求出组合数取模 .

typedef long long ll;
ll qpow(ll a,ll n,ll p)
{
	ll ans=1;
	while (n)
	{
		if (n&1) ans=ans*a%p;
		a=a*a%p; n>>=1;
	} return ans;
}
namespace extra_lucas_detail
{
	ll fac(ll n,ll p,ll pk)
	{
		if (!n) return 1;
		ll ans=1;
		for (int i=1;i<pk;i++)
			if (i%p) ans=ans*i%pk;
		ans=qpow(ans,n/pk,pk);
		for (int i=1; i<=n%pk;i++)
			if (i%p) ans=ans*i%pk;
		return ans*fac(n/p,p,pk)%pk;
	}
	ll exgcd(ll a,ll b,ll& x,ll& y)
	{
	    if (!b){x=1; y=0; return a;}
	    ll d=exgcd(b,a%b,x,y);
		int z=x; x=y; y=z-y*(a/b);
		return d;
	}
	ll inv(ll a,ll P){ll x,y,d=exgcd(a,P,x,y); return (d==1)?x%P:-1;}
	ll C(ll n,ll m,ll p,ll pk)
	{
		if (n<m) return 0;
		ll A=fac(n,p,pk),B=fac(m,p,pk),C=fac(n-m,p,pk),cnt=0;
		for (ll i=n;i;i/=p) cnt+=i/p;
		for (ll i=m;i;i/=p) cnt-=i/p;
		for (ll i=n-m;i;i/=p) cnt-=i/p;
		return A*inv(B,pk)%pk*inv(C,pk)%pk*qpow(p,cnt,pk)%pk;
	}
}
ll exlucas(ll n,ll m,ll p)
{
	using namespace extra_lucas_detail;
	ll ans=0,P=p;
	for (int i=2;(p>1)&&(i*i<=p);i++)
	{
		ll pk=1;
		while (!(p%i)){p/=i; pk*=i;}
		if (pk>1)
		{
			ll now=C(n,m,i,pk);
			ans=(ans+now*(P/pk)%P*inv(P/pk,pk)%P)%P;
		}
	}
	if (p>1)
	{
		ll now=C(n,m,p,p);
		ans=(ans+now*(P/p)%P*inv(P/p,p)%P)%P;
	} return (ans%P+P)%P;
}
posted @   yspm  阅读(189)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
😅​
点击右上角即可分享
微信分享提示