组合数计算(从1000到1e9的组合数各类求法)

组合数涉及的问题很多,求解过程中涉及的定理判定也有很多,期间断断续续参考了很多大佬们的博客,最终进行了总结。在此我使用用问题引入算法的方法来一步一步了解组合数

简单的组合数问题(运用快速幂及递归实现杨辉三角)

组合数求解

  • 像关于组合数求和问题如∑ Cni(0<=i<=n) 可利用快速幂,此时∑ Cni = 2^n

    此为二项式定理推导出的结果 练习题可见洛谷P3414

  • 若求较小的组合数如C(n,m)(m<=n<=1000 保守估计1000,实际可能可以更大些,但不会超数量级)可直接利用杨辉三角的法则来计算组合数C(n,m)=C(n-1,m-1)+C(n-1,m)。以下为杨辉三角的简单介绍和理解:

杨辉三角简单介绍


性质

1.每个数等于它上方两数之和–>最重要的一个性质

2.每行数字左右对称,由1开始逐渐变大–>由第5个性质可知

3.第n行的数字有n项

4.第n行的m个数可表示为C(n-1,m-1),即为从n-1个不同元素中取m-1个元素的组合数

5.第n行的第m个数和第n-m+1个数相等,为组合数性质之一,即C(n,m)==C(n,n-m+1)

6.每个数字等于上一行的左右两个数字之和。可用此性质写出整个杨辉三角。即第n+1行的第i个数等于第n行的第i-1个数和第i个数之和,这也是组合数的性质之一。即 C(n+1,i)=C(n,i)+C(n,i-1)。

7.(a+b)n的展开式中的各项系数依次对应杨辉三角的第(n+1)行中的每一项–>二项式定理

杨辉三角还有其他性质(例如斐波那契数列与其的关系)在此不做说明

由第6个性质可知上面第二类问题C(n,m)=C(n-1,m-1)+C(n-1,m)的来由

但是,利用杨辉三角递归,只可以处理n,m较小的组合数。当n,m很大时,我们如何求解呢?


较复杂的组合数问题(n,m较大时)

逆元在组合数中的应用

对欧拉-费马小定理的理解

在了解逆元之前,需要先了解欧拉-费马小定理,并有此推导逆元(如果已经了解了欧拉-费马小定理,可直接跳到标题:通过逆元计算组合数 )

欧拉定理: 若正整数 a , n 互质(公因数只有1),则 aφ(n)≡1(mod n) 其中 φ(n) 是欧拉函数,此函数代表(1~n) 与 n 互质的数的个数。

简单证明:

令 x1 x2 x3…xφ(n) 为1-n中与n互质的数 (互质即gcd(m,n)=0)

我们先来研究 ax1 ax2 ax3… axφ(n) 这一队数列

**其必有任意两个元素mod n 不相同 **

反证:若存在ax1 ≡ ax2(mod n),则 n |( ax1 - ax2 )即n可以整除( ax1 - ax2 )

而(ax1-ax2)/n ==(x1-x2)/n * a 或 a/n * (x1-x2) ;

第一种除法由于 (x1-x2)< n ∴一定除不开

第二种除法由于 a和 n互质 ∴也一定除不开。综上不可能存在ax1 ≡ ax2(mod n),得证。

且∵ a与n互质 所以 axi 与n也互质 所以根据欧几里得算法(辗转相除法)有gcd(axi,n)1 所以:gcd(axi,n) gcd(n,axi%n)== 1

又∵ax1%n, ax2%n, ax3%n… axφ(n)%n 均小于 n,且ax1%n, ax2%n, ax3%n… axφ(n)%n又都与n互质

∴ ax1%n, ax2%n, ax3%n… axφ(n)%n必为x1 x2 x3…xφ(n) 以一定顺序排列而成的。

那么必有:ax1%n, ax2%n, ax3%n… axφ(n)%n = x1 x2 x3…xφ(n)

自然就有 ax1%n, ax2%n, ax3%n… axφ(n)%n ≡ x1 x2 x3…xφ(n) (mod n)

即 ax1 ax2 ax3 … axφ(n) ≡ x1 x2 x3…xφ(n) (mod n),把x1 x2 x3…xφ(n) 移到左边(因为肯定不会出现小数,所以可以直接移)

∴有 **(aφ(n)-1)x1 x2 x3…xφ(n) ≡ 0 (mod n) **

又∵x1 x2 x3…xφ(n) 均与 n互质

∴自然有(aφ(n)-1) ≡ 0 (mod n)

∴ 证得 aφ(n)≡1(mod n)

接下来,我们利用欧拉定理,简单证明费马小定理

费马小定理:对于质数p,任意整数,均满足:ap≡a(mod p)

简单证明:

由于 ap≡a(mod p)∴有ap-1≡1 (mod p) 这和 aφ(n)≡1(mod n)类似,所以只需证明p-1φ(n)即可,且因为p是一个质数,易得与其互质的数为1,2,3…p-1,共p-1个所以有p-1φ(n),得证

(注意:若有a是p的倍数,则一定有ap≡a≡0(mod p),不必证明)

(拓展)欧拉定理的推论:若正整数a,n互质,那么对于任意正整数b,有ab≡ab mod φ(n)(mod n)

简单证明:若ab ≡ ab mod φ(n)(mod n)则有ab-b mod φ(n)≡ 1(mod n),因为φ(n)| b-b mod φ(n) 所以令 p=(b-b mod φ(n))/φ(n) ∴即证(ap)φ(n)≡1 (mod n) 易得ap和 n是互质的 所以得证

以上引用,总结自:https://www.cnblogs.com/zylAK/p/9569668.html (这篇博客里还有一道例题,想具体了解欧拉定理推论可以去看一下)

通过逆元计算组合数

首先了解逆元的定义:

逆元:对于a和p(a和p互素),若a*b%p≡1,则称b为a的逆元。(mod p的情况下)

首先,我们弄清,为什么求组合数需要用到逆元呢?

此时我们就需要使用上面👆说过的费马小定理。

根据欧拉-费马小定理可知 aφ§ ≡ 1 (mod p) 又因为当p为质数时,满足欧拉定理,且φ§=p-1,所以有ap-2 × a≡1(mod p),所以有ap-2 是a的逆元,此时,我们只需要把ap-2 求出来,即可求出逆元。然后须用到快速幂算法(快速幂相关内容可在我的博客主页搜索:快速幂算法)

finally,让我们来总结下如何求:
C n m m o d    p ( p 为 质 数 ) C_n^m\mod p (p为质数) Cnmmodp(p)

  • 求出n!%p,m!%p,(n-m)!%p

    (注1:阶乘过程中,由同模运算的乘法原则,可在每一步阶乘中都mod上p)

    (注2:若求同n下的多个m,即求多组组合数,可以定义F[i],为i!mod p的值,那么就可以把多组的m!%p和(n-m)!%p 都存在F数组中)

  • 求出m!p-2%p和(n-m)!p-2%p (即对m!%p,(n-m)!%p 快速幂)

  • 求出(n!%p×m!p-2%p*(n-m)!p-2%p)%p 即为最终答案。

以上引用,总结自:https://www.cnblogs.com/liziran/p/6804803.html

例题:牛客挑战赛37 B

简单概述:输入一组合数Cnm,若与所给数相同,输出Yes!否则输出No!

思路:使用逆元求解,但这里面没有给出模数,我们可以采用随机质数的方法,打一个随机质数表,通过多次取余与所给数取余比较,来判断是否相同。注意到所给数p可能很大,在保存时,使用字符串保存,再分部取余。

(这是一个AC代码的质数表,可以适当采用: { 88897 ,58787 ,125453 , 7853 , 78569 ,145267 ,25867 ,105899 ,8803,4591 })

👇下给出通过逆元求组合数的做法

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int mo[10]={88897,58787},mt,nt;
ll mp=0;
char p[400010];
void mop(int mod)
{
    mp=0;
    int len=strlen(p);
    for(int i=0;i<len-1;i++)
    mp=(mp+(p[i]-'0')%mod)*10%mod;mp=(mp+p[len-1]-'0')%mod;
}
ll fastpow(ll baser,ll power,int mod)
{
    ll ans=1;
    while(power)
    {
        if(power&1)
        ans=ans*baser%mod;
        baser=baser*baser%mod,power>>=1;
    }
    return ans%mod;
}
ll ie(ll n,ll m,int mod)//inverse element 的缩写
{
    ll nj=1,mj=1,nmj=1;
    for(int i=2;i<=m;i++)
        mj=mj*i%mod;
    for(int i=2;i<=n-m;i++)
        nmj=nmj*i%mod;
        nj=mj;
    for(int i=m+1;i<=n;i++)
        nj=nj*i%mod;
    return (nj*fastpow(mj,mod-2,mod)%mod*fastpow(nmj,mod-2,mod)%mod)%mod;
}
int main()
{

    cin>>nt>>mt>>p;
    for(int i=0;i<2;i++)
    {
        mop(mo[i]);//求mp
        if(ie(nt,mt,mo[i])!=mp){cout<<"No!";return 0;}
    }
    cout<<"Yes!";
}

那么,我们会得到以下结果:

分析为什么会超时呢?

我们查看题目描述,发现1≤n≤1e9;

在进行阶乘运算时,由于其复杂度为O(n),那么可知,当n接近于1e9时,复杂度会过大

那么剩余的60%,我们如何去求组合数?那么我们就需要了解Lucas定理👇

Lucas定理在组合数中的应用

(注意以下的除法均为向下取整)

*Lucas定理的定义:C(n,m)%p=C(n/p,m/p)C(n%p,m%p)%p

那么我们只要不断对C(n/p,m/p)调用Lucas定理就可以通过递归来实现对组合数计算的简化,其复杂度为O(logn)

卢卡斯定理证明

(网上找了很多证明都不太懂,去了b站听,豁然开朗😂)

首先我们令:n=a×p+b m=c×p+d,也就是a=n/p b=n%p c=m/p d=m%p

那么Lucas定理可以写成:
C n m ≡ C a b ∗ C c d ( m o d    p ) C_n^m\equiv C_a^b*C_c^d (\mod p) CnmCabCcd(modp)
我们先把这种写法放一边,来看这两个式子:

(1+x)p ≡ 1+x (mod p) 以及 1+xp ≡ 1+x (mod p)

这两个式子我们都可以用欧拉—费马小定理来证明,所以有(1+x)p ≡ 1+xp

那么这个同模有啥用呢 我们可以利用它来得到下面这个式子👇(n=a×p+b m=c×p+d)

(1+x)n = (1+x)a×p+b

​ = (1+x)p×a × (1+x)b

​ ≡ (1+xp)a × (1+x)b (mod p)

接下来,我们利用二项式定理,可知道在式子左边(1+x)n 中的xm 项为:
C n m x m C_n^mx^m Cnmxm
而式子右端 (1+xp)a × (1+x)b 的xm 项为
C a c C b d x p ∗ c x d = C a c C b d x p ∗ c + d = C a c C b d x m C_a^c C_b^d x^{p*c}x^d=C_a^c C_b^d x^{p*c+d}=C_a^c C_b^d x^{m} CacCbdxpcxd=CacCbdxpc+d=CacCbdxm
又∵式子左端和右端同余

∴有
C n m ≡ C a c C b d ( m o d    p ) C_n^m≡C_a^c C_b^d (\mod p) CnmCacCbd(modp)
得证。

🐱‍👤进阶:Lucas定理的真正面目(看懂上面的递归就可以解决问题了)

我们由上面可以知道:C(n,m)%p=C(n/p,m/p)*C(n%p,m%p)%p

然后C(n/p,m/p)继续套用Lucas定理,那么,我们真正在一直乘的其实是后面的C(n%p,m%p),且这个这里面的两个余数,是经过某些次连除p后得到的"n"和"m"再取余

那么我们将n,m写成p进制数,则有

n=(n1n2n3…ni)p m=(m1m2m3…mj)p

我们令两个p进制数右对齐,m若位数和n不同,用0在左边补上,保证都为K位数。

那么这p进制下的n,m相对应的每一位数,其实就是一个个n%p,m%p!

那么我们就可以得知Lucas定理的另一种写法(原始写法):
C n m ≡ ∏ i = 1 K C n i m i m o d    p C_n^m≡\prod_{i=1}^{K} {C_{n_i}^{m_i}} \mod p Cnmi=1KCnimimodp

具体使用方法

Lucas(n,m)=Lucas(n/p,m/p)*ie(n%p,m%p) 其中ie为逆元函数

边界问题:lucas和ie函数 m==0时 返回1,ie函数 n<m时 返回 0;

所以,我们继续完善以上的例题

例题AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int mo[10]={88897,58787,125453,7853,78569,145267,25867,105899,8803,4591},mt,nt;
ll mp=0;
char p[400010];
void mop(int mod)
{
    mp=0;
    int len=strlen(p);
    for(int i=0;i<len-1;i++)
    mp=(mp+(p[i]-'0')%mod)*10%mod;mp=(mp+p[len-1]-'0')%mod;
}
ll fastpow(ll baser,ll power,int mod)
{
    ll ans=1;
    while(power)
    {
        if(power&1)
        ans=ans*baser%mod;
        baser=baser*baser%mod,power>>=1;
    }
    return ans%mod;
}
ll ie(ll n,ll m,int mod)//inverse element 的缩写
{
    if(n<m) return 0;if(m==0) return 1;
    ll nj=1,mj=1,nmj=1;
    for(int i=2;i<=m;i++)
        mj=mj*i%mod;
    for(int i=2;i<=n-m;i++)
        nmj=nmj*i%mod;
        nj=mj;
    for(int i=m+1;i<=n;i++)
        nj=nj*i%mod;
    return (nj*fastpow(mj,mod-2,mod)%mod*fastpow(nmj,mod-2,mod)%mod)%mod;
}
ll lucas(ll n,ll m,int mod)
{
    if(m==0) return 1;
    return (lucas(n/mod,m/mod,mod)*ie(n%mod,m%mod,mod))%mod;
}
int main()
{

    cin>>nt>>mt>>p;
    for(int i=0;i<10;i++)
    {
        mop(mo[i]);//求mp
        if(lucas(nt,mt,mo[i])!=mp){cout<<"No!";return 0;}
    }
    cout<<"Yes!";
}

以上,我们都是在讨论模数为质数时的情况,那么当模数不为质数时,我们就要用到扩展Lucas定理👇

扩展Lucas定理(当模数不为质数时):

中国剩余定理:

为了了解并证明扩展lucas定理,我们先了解什么是中国剩余定理:

1585043571712

其证明可以在知乎中得到答案:https://zhuanlan.zhihu.com/p/44591114 (写得很好)

拓展Lucas定理具体使用:

当Cnm mod p 其中p不为质数时,那么对p进行质因子分解,有
p = ∏ i p i k i p=\prod_i p_i^{k_i} p=ipiki
其中ki表示含有的第i个质因子的个数,比如12=22×31。那么我们易得piki 之间是互素的

接下来,我们只要分别求出
a i ≡ C n m m o d    p i k i a_i≡C_n^m\mod p_i^{k_i} aiCnmmodpiki
对应上方👆中国剩余定理我们可知,x就是Cnm ,且:
C n m ≡ ∑ i ( a i × p p i k i × [ ( p p i k i ) − ] p i k i ) m o d    p C_n^m≡ \sum_i (a_i\times \frac p {p_i^{k_i}}\times[ (\frac p {p_i^{k_i}} )^- ]_ {p_i^{k_i}}) \mod p Cnmi(ai×pikip×[(pikip)]piki)modp
其中
[ ( p p i k i ) − ] p i k i ) [ (\frac p {p_i^{k_i}} )^- ]_ {p_i^{k_i}}) [(pikip)]piki)
表示分式的逆元 mod piki (看不懂就去看看上面的知乎链接,比对来理解)

那么我们只剩下最终一个问题:如何求ai ?

以下图引用自:https://www.bilibili.com/video/BV1xb411x74F?from=search&seid=7989126052916540468

由于
a i ≡ C n m m o d    p i k i a_i≡C_n^m\mod p_i^{k_i} aiCnmmodpiki
则问题可以转换为以下图片的求解:

其中k1 k2 k3 分别为 n! m! (n-m)! 的阶乘中所含的因子p的个数

→得知一般规律后带入,((m!/pk2)%pk)φ(pk)-1 ,(((n-m)!/pk3)%pk)φ(pk)-1 就是逆元

我们以n=19,p=3,k=2为例:

n!=1∗2∗3∗4∗5∗6∗7∗8∗9∗10∗11∗12∗13∗14∗15∗16∗17∗18∗19

=(1∗2∗4∗5∗7∗8)∗(10∗11∗13∗14∗16∗17)∗19∗36∗(1∗2∗3∗4∗5∗6)

将其分为3部分:第一部分是p的幂的部分,也就是36即p⌊n/p⌋;第二部分是一个新的阶乘,也就是6!即⌊n/p⌋!,并且可以继续递归直到其中不含有质因子p;第三部分是除了前两部分之外剩下的数

我们注意到pa全部由第一部分和第二部分提供,且a为n!中所有的质因子p的个数;

所以(n!/pa)%pk 就是第二部分递归所得的最终的值乘上第三部分 mod pk(程序中顺便记录出a的值好最后再乘回来)

那么第三部分有什么特点呢?

注意到(1∗2∗4∗5∗7∗8)≡(10∗11∗13∗14∗16∗17)(mod p^k)所以直接求第一组再快速幂就可以了。幂值为组数,为⌊n/pk⌋。

且19<=p^k 可以暴力直接乘上

第三部分怎么来的呢?

首先第一组为(1∗2∗4∗5∗7∗8)是数i(1<=i<pk)且与p互质的数 ,且和其他组数加起来总组数为⌊n/pk

这个19 就是 最终剩下的 没有形成完整一组的数 其个数易得为 从1到n%pk中与p互质的个数(记作s);且其相乘的模数就是第一组的前 s个数相乘模p^k的值

那么
C n m m o d    p C_n^m \mod p Cnmmodp
向回推导

((m!/pk2)%pk)φ(pk)-1 ,(((n-m)!/pk3)%pk)φ(pk)-1 此逆元就可以求出来了,然后根据“通过逆元计算组合数”,就可以算出最终答案。

p不为质数的组合数求解,到此其中的所有问题都已解决,可以求出最终答案🐱‍👤。

由于菜鸡本菜还没遇到相关类型的题,无法测试扩展Lucas算法的代码正确性,所以没有贴出来,有兴趣者可以在其他地方找到相关代码。

终言:洋洋洒洒五千字,说不尽组合事。欲穷千里目,还需更上一层楼。

安利一个markdown编辑器:Typora,真的好用。其实也是偶然发现的( •̀ ω •́ )y

我的个人博客网站aei.today 但是GitHub托管好像出了问题,可能会找不到ip地址

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wizardAEI

喜欢就投喂一下吧O(∩_∩)O

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值