前言

组合数就是 Cmn C n m C_n^m,是排列组合中非常重要的一部分,最近做到了几道与计算组合数有关的题,想在此总结一下如何编程计算组合数。
某位大佬在我之前也写过类似的文章,这是他的博客:https://blog.csdn.net/u010582475/article/details/47707739

递推(杨辉三角)

先给出递推公式: Cmn=Cmn−1+Cm−1n−1 C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1}
证明:从n个数中选m个数,若第n个数不选,则相当于从n-1个数中选m个数,即 Cmn−1 C n − 1 m C_{n-1}^m;若第n个数选,则相当于从n-1个数中选m-1个数,即 Cm−1n−1 C n − 1 m − 1 C_{n-1}^{m-1}。那么总方案数即为两者之和。
当然,也可以根据组合数公式,展开来证明其相等,这里就不写出来了,有兴趣的话也可以自己去推。
如果写成数组形式,就是c[n][m]=c[n-1][m]+c[n-1][m-1],这同时是杨辉三角的递推公式,所以组合数满足杨辉三角。边界是c[0][0]=1。若只要求一个组合数的话,也可以滚动一下这个数组,节约空间,但同时m这一维要倒着枚举,就像01背包一样。程序如下:

#include<iostream>
using namespace std;
int n,m;
long long c[10005];
int main()
{cin>>n>>m;m=min(m,n-m);c[0]=1;for (int i=1;i<=n;i++){for (int j=m;j>=1;j--){c[j]=c[j]+c[j-1];}}cout<<c[m];
}

因为 Cmn=Cn−mn C n m = C n n − m C_n^m=C_n^{n-m},所以m可以取m和n-m中小的那一个,以节省时间。但复杂度还是过高,约为O( n2 n 2 n^2)。

乘法逆元

先强调一遍,这种方法只适用于对答案模一个大质数的情况。
一般组合数会非常非常大,可能long long都会爆,所以题目会让我们模一个大质数,然后输出。既然要取模,那就要用到数论中的内容了。
观察组合数的公式: Cmn=n!m!(n−m)!=n∗(n−1)∗...∗(n−m+1)m! C n m = n ! m ! ( n − m ) ! = n ∗ ( n − 1 ) ∗ . . . ∗ ( n − m + 1 ) m ! C_n^m=\frac{n!}{m!(n-m)!}=\frac{n*(n-1)*...*(n-m+1)}{m!}
对于取模来说,乘法可以每做一步都取模,但除法却不行。为了解决除法的取模问题,人们发明了乘法逆元这种奇妙的东西。若a*b=1(mod p),则称b为a在模p下的乘法逆元,一般认为, b<p b < p b

,b记为 a−1 a − 1 a^{-1}。当然,a和p要互质,不然这个乘法逆元不存在。但乘法逆元有什么用呢?看如下的式子(以下等式均在mod p的前提下):
ab a b \frac{a}{b}= ab∗b∗b−1 a b ∗ b ∗ b − 1 \frac{a}{b}*b*b^{-1}=a* b−1 b − 1 b^{-1}(先是乘以1,再是约掉b)
这样我们就把除法取模转化成了乘以它的乘法逆元再取模。
那么乘法逆元如何计算呢?

递推

求i在mod p下的乘法逆元(gcd(i,p)=1, i<p i < p i

):
令p=k*i+r,则k=p/i,r=p%i
显然,k*i+r=0(mod p)
两边同乘 i−1∗r−1 i − 1 ∗ r − 1 i^{-1}*r^{-1},得到k* r−1+i−1 r − 1 + i − 1 r^{-1}+i^{-1}=0(mod p)
移项,再进行处理,得 i−1=−k∗r−1=(p−k)∗r−1 i − 1 = − k ∗ r − 1 = ( p − k ) ∗ r − 1 i^{-1}=-k*r^{-1}=(p-k)*r^{-1}(mod p)
把k和r用p和i代掉,得 i−1=(p−p/i)∗(p%i)−1 i − 1 = ( p − p / i ) ∗ ( p % i ) − 1 i^{-1}=(p-p/i)*(p\% i)^{-1}
若用inv[i]表示i的乘法逆元,则上式可以写成inv[i]=(p-p/i)*inv[p%i]
复杂度为线性。伪代码如下:

long long inv[10000005];
inv[1]=1;
long long ni(int x,int p)
{if (inv[x]!=0) return inv[x];inv[x]=(p-p/x)*ni(p%x,p)%p;return inv[x];
}

费马小定理

费马小定理: ap−1 a p − 1 a^{p-1}=1(mod p)(p是质数,gcd(a,p)=1)
求a在mod p下的乘法逆元可以求 ap−2 a p − 2 a^{p-2},用快速幂就好了。

long long pow(long long x,long long y,long long p)//快速幂,求x^y%p
{if (y==0) return 1;long long z=pow(x,y/2)%p;if (y%2==0) return z*z%p;return z*z%p*x%p;
}
long long ni(long long x,long long p)//求x在mod p下的乘法逆元
{return pow(x,p-2,p);
}

欧拉定理

费马小定理是欧拉定理在p为质数下的特殊情况,既然费马小定理可以,那么欧拉定理也可以。但使用乘法逆元求组合数的前提是模一个大质数,能用简单的费马小定理,为什么要用麻烦的欧拉定理呢?所以我在此只是提一提,并不想多讲。

刚刚介绍了三种求乘法逆元的方法,现在回到主题,如何求组合数。
还记得刚才的公式吗?
Cmn=n!m!(n−m)!=n∗(n−1)∗...∗(n−m+1)m! C n m = n ! m ! ( n − m ) ! = n ∗ ( n − 1 ) ∗ . . . ∗ ( n − m + 1 ) m ! C_n^m=\frac{n!}{m!(n-m)!}=\frac{n*(n-1)*...*(n-m+1)}{m!}
对于分子,我们可以边乘边mod p,对于分母,只需要求出分母的乘法逆元,再乘以分子就行了。
以下是程序(此程序用的是费马小定理求乘法逆元):

#include<iostream>
using namespace std;
int n,m,p;
long long pow(long long x,long long y)
{if (y==0) return 1;long long z=pow(x,y/2)%p;if (y%2==0) return z*z%p;return z*z%p*x%p;
}
long long ni(long long x,long long p)
{return pow(x,p-2);
}
long long c(int n,int m,int p)
{long long x=1,y=1;for (int i=n;i>=n-m+1;i--) x=x*i%p;for (int i=1;i<=m;i++) y=y*i%p;return x*ni(y,p)%p;
}
int main()
{cin>>n>>m>>p;cout<<c(n,m,p);
}

再说一遍,p一定要是大质数,起码得大于m。为什么呢?若要求a在模p下的乘法逆元,必须要保证a与p互质。在此题中,要求m!的乘法逆元,那么p必须与m!互质,那么p就要大于m,且是个质数。

卢卡斯定理

先把这种方法的适用条件写在前面,适用于对答案模一个质数的情况
和上面的乘法逆元求组合数的条件对比一下,只相差了一个大字。也就是说这个质数不用很大,非常小也行。
介绍一下卢卡斯定理,公式: Cmn=Cm/pn/p∗Cm%pn%p C n m = C n / p m / p ∗ C n % p m % p C_n^m=C_{n/p}^{m/p}*C_{n\%p}^{m\%p}(mod p),要求p为质数。
公式很好记吧,至于证明呢,看懂了也下次也不会证,会证了也没什么用(主要是我不会),我就不证明了。
观察公式,有没有发现,在n和m都小于p的时候,公式一点用都没有( C00=1 C 0 0 = 1 C_0^0=1)。所以这个公式是在n>=p或m>=p的情况下使用的。这样可以减小n和m,使之小于p,再用乘法逆元去求组合数。
其实卢卡斯定理就是一个小优化,大部分和乘法逆元求组合数一样,不过要注意在n=0和m=0时的边界处理。

#include<iostream>
using namespace std;
int n,m,p;
long long pow(long long x,long long y)
{if (y==0) return 1;long long z=pow(x,y/2)%p;if (y%2==0) return z*z%p;return z*z%p*x%p;
}
long long ni(long long x)
{return pow(x,p-2);
}
long long c(int n,int m)
{if (m==0) return 1%p;if (n==0) return 0;if (n>=p||m>=p) return c(n/p,m/p)*c(n%p,m%p)%p;//卢卡斯定理核心语句long long x=1,y=1;for (int i=n;i>=n-m+1;i--) x=x*i%p;for (int i=1;i<=m;i++) y=y*i%p;return x*ni(y)%p;
}
int main()
{cin>>n>>m>>p;cout<<c(n,m);
}

质因数分解

乘法逆元只能处理模数为大质数的情况,卢卡斯定理只能处理模数为质数的情况,那有没有一种方法能处理模数不是质数的情况呢?显然是有的。而且不取模也是可以的。
我们可以把组合数中要乘或除的每一个数分解质因数,再把分母的质因数减掉,最后把剩下的质因数乘起来,边乘边模p就行了。
那如何分解质因数呢?可以用欧拉筛把每个数的最小质因数求出来,把i的最小质因数的编号保存在min_prime[i]里。
具体看代码吧。

#include<iostream>
using namespace std;
int n,m,p,b[10000005],prime[1000005],t,min_prime[10000005];
void euler_shai(int n)//用欧拉筛求出1~n中每个数的最小质因数的编号是多少,保存在min_prime中
{for (int i=2;i<=n;i++){if (b[i]==0) {prime[++t]=i;min_prime[i]=t;}for (int j=1;j<=t&&i*prime[j]<=n;j++){b[prime[j]*i]=1;min_prime[prime[j]*i]=j;if (i%prime[j]==0) break;}}
}
long long c(int n,int m,int p)//计算C(n,m)%p的值
{euler_shai(n);int a[t+5];//t代表1~n中质数的个数 ,a[i]代表编号为i的质数在答案中出现的次数 for (int i=1;i<=t;i++) a[i]=0;//注意清0,一开始是随机数 for (int i=n;i>=n-m+1;i--)//处理分子 {int x=i;while (x!=1){a[min_prime[x]]++;//注意min_prime中保存的是这个数的最小质因数的编号(1~t) x/=prime[min_prime[x]];}}for (int i=1;i<=m;i++)//处理分母 {int x=i;while (x!=1){a[min_prime[x]]--;x/=prime[min_prime[x]];}}long long ans=1;for (int i=1;i<=t;i++)//枚举质数的编号,看它出现了几次{while (a[i]>0){ans=ans*prime[i]%p;a[i]--;}}return ans;
}
int main()
{cin>>n>>m>>p; m=min(m,n-m);//小优化 cout<<c(n,m,p);
}

总结

写了这么多种算法,每种算法都有其优点与局限性。递推写起来快,思维简单,但时间复杂度高。乘法逆元用得比较普遍,因为一般都是模一个大质数,复杂度也几乎是线性的。卢卡斯定理只会在特定的题目里做到,但其实编程复杂度并不高,就是在乘法逆元的基础上加几句话。质因数分解的适用性最广,编程复杂度也最高,这就是完美的代价吧。

计算组合数的几种方法总结相关推荐

  1. 计算组合数的几种方法

    数据比较小,直接采用C(a, b) = a * (a - 1) *....* (a - b + 1) / (b * (b - 1) *...* 2 * 1) 试用数据范围:a <= 29.在a ...

  2. 现代密码学3.1--定义计算安全的两种方法

    现代密码学3.1--定义计算安全的两种方法 三种安全性定义 定义计算安全的两种方法 具体方法/concrete approach 渐进方法/asyptotic approach "高效/PP ...

  3. 网络基础知识 快速计算子网掩码的2种方法

    网络基础知识 快速计算子网掩码的2种方法<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office ...

  4. php中计算时间差的几种方法,php计算时间差的方法

    一个简单的例子:计算借书的天数,根据每天的日期进行计算. (1) 有数据库的情况 MSSQL可以使用触发器!用专门计算日期差的函数datediff()便可. MYSQL那就用两个日期字段的差值计算的计 ...

  5. linux c计算时间差值,获取时间和计算时间差的几种方法总结,时间差几种方法...

    获取时间和计算时间差的几种方法总结,时间差几种方法 一.标准C和C++都可用 1.获取时间用time_t time( time_t * timer ),计算时间差使用double difftime( ...

  6. [C++] 计算行列式的若干种方法

    计算行列式的三种方法 测试样例 按行(列)展开法 计算结果 高斯消元转化为上三角阵 计算结果 尝试引入permutation operation 尝试使用分数运算 定义法 代码 测试结果 有问题欢迎提 ...

  7. 个人所得税递归函数C语言,C语言实现 计算个人所得税务2种方法

    #include #include /* 基于C语言的个人所得税计税系统 问题描述: 我国现行的个人所得税计算方法如下: 级数 全月应纳税所得额   税率(%) 1 不超过500元的    5 2 超 ...

  8. 漫谈分子动力学计算热导率的五种方法

    关注 Mr.material,\color{Violet} \rm Mr.material\ ,Mr.material , 更\color{red}{更}更多\color{blue}{多}多精\col ...

  9. 【算法】计算组合数的四种常用方法

    [算法]计算组合数的四种常用方法 算法一:Cab=Ca−1b−1+Ca−1bC_{a}^{b}=C_{a-1}^{b-1}+C_{a-1}^{b}Cab​=Ca−1b−1​+Ca−1b​ 解析: Ca ...

最新文章

  1. .net学习之母版页执行顺序、jsonp跨域请求原理、IsPostBack原理、服务器端控件按钮Button点击时的过程、缓存、IHttpModule 过滤器...
  2. BZOJ-1012[JSOI2008]最大数maxnumber 线段树区间最值
  3. 图形系统中的仿射变换
  4. POJ 2315:Football Game(博弈论)
  5. Apache Cassandra和Java入门(第二部分)
  6. 关于Python爬虫,一条高效的学习路径
  7. Open3d之点云上色
  8. 计算机禁止安装游戏,win7系统禁用自动安装游戏应用的详细教程
  9. 实在不行来瓶敌敌畏吧!Scrapy框架爬取某农药肥料网站+异步Mysql数据库存储
  10. Centos 6.5安装最新版谷歌浏览器-Chrome
  11. 针对支付宝-当面付实现的个人支付
  12. 子选择器和后代选择器
  13. 项目管理,如何从疲于奔命中解脱出来?
  14. 微信小程序隐藏tabBar
  15. 单机大天使服务器端修改商店,《大天使之剑h5》变态单机版最新开服_无限钻石打造最强阵容_满V公益私服...
  16. 建模师一个月的真实工资是多少
  17. 干掉AlibabaProtect【windows10】
  18. CSS 单行/多行文本溢出显示省略号(...)的实现
  19. 阿里云服务器入门教程 (图文教程)
  20. 面试第一个问题,请先做个自我介绍吧!

热门文章

  1. 工业物联网的战国时代
  2. dede网站SEO教程:织梦seo优化入门手册
  3. 中级软考-软件设计师(三)
  4. Android 框架问题分析案例 - 谁杀了桌面?
  5. 不可思议的大公司组织结构图
  6. VS打开后界面找不到win32或者x64的选项怎么办?
  7. pandas_datareader下载雅虎财经股价数据
  8. 贤胜足球分析系统 v2.4.4 大小球测试版 怎么用
  9. flink1.13 upsert-kafka connector 实时报表 视频演示
  10. RK3568烧录系统