前言
近期遇到两个组合数的题,于是写一下组合数的解法,当然虽然确实有四种,但是我目前只会三种
标题致敬一下孔乙己而已,但是后面学了也会补的
正文
杨辉三角求组合数
利用了这个公式
作用于数组也就是c[n][m]=c[n-1][m]+c[n-1][m-1]
for (int i = 0; i <=n ; i++)
for (int j = 0; j <=i; j++)
{
if(j==0)a[i][j]=1;
else a[i][j] = (a[i - 1][j - 1] + a[i - 1][j])%mod;
}
这个算法就可以让你得到c n m(n为底,m为头)的所有值,并且直接取模就好了
优点是很好写
缺点就是时间o2,空间是二维数组,也挺大的,基本上n,m大于1e4就很难了
快速幂求逆元
这个方法就要看这个组合数的求解方程了
这个为啥不能直接取模呢,因为有除法,所以要求逆元
而应用费马小定理,就可知x=a^(p-2)就是a对于p取模的逆元了,并且用可以用带取模的快速幂
int ksm(int x, int n)//x^n%mod
{
int ans = 1;
while (n)
{
if (n & 1)ans = ans * x % mod;
x = x * x % mod;
n >>= 1;
}
return ans;
}
然后因为我们要算三个数的阶乘n,m,(n-m),所以可以进行一个预处理,也就是把阶乘先存储起来(注意存的也是取模后的阶乘)
有咩有同学会疑惑,为啥这个要存模过的阶乘呢?毕竟模了以后可就不是这个阶乘本身了啊,
这里可以用阶乘其实是乘法运算啊,所以阶乘取模后得到的数,在模运算下是等价的
或者啊,你如果不对阶乘取模,那其实就是算1~n倒数的逆元了吧,然后再乘起来,所以其实可以先乘了取模再算逆元,实则是等价的
void jc(int n)//算出0-n的阶乘
{
a[0] = 1;
for (int i = 1; i <= n; i++)
{
a[i] = a[i - 1] * i%mod;
}
}
这样我们就可以直接带公式求解了,下面是完整代码
#include<bits/stdc++.h>
using namespace std;
int a[2000005];
#define mod 1000000007
#define int long long//全局long long
int ksm(int x, int n)//求x^n%mod的逆元
{
int ans = 1;
while (n)
{
if (n & 1)ans = ans * x % mod;
x = x * x % mod;
n >>= 1;
}
return ans;
}
void jc(int n)//算出0-n的阶乘
{
a[0] = 1;
for (int i = 1; i <= n; i++)
{
a[i] = a[i - 1] * i % mod;
}
}
int c(int n, int m)
{
return a[n] * ksm(a[m], mod - 2) % mod * ksm(a[n - m], mod - 2) % mod ;//多次取模
}
signed main() //这里就不能用int了
{
int n, m,t;
jc(2000001);
while (cin >> n >> m)
{
cout << c(n, m) << endl;
}
return 0;
}
注意要全局都开long long并且最后求答案也要多次取模,防止爆掉
这个方法
n,m<=1e7都是可以的,时间在n*log^2n,还是比较小的,就是需要mod为质数。
如果最后是多组输入的话,对于逆元我们也可以优先进行一个计算,存起来,直接用也可以
扩展欧几里得求逆元
基本原理和快速幂一样的,只是ex_gcd进行求解而已
ll ex_gcd(ll a, ll b, ll& x, ll& y)
{
if (b == 0)
{
x = 1;
y = 0;
return a;
}
// x,y调换传给下一次递归等价于x1 = y2
ll t = extend_gcd(b, a % b, y, x);
//等价y1 = x2 -(a/b) * y2
y -= a / b * x;
return t;
}
这个在使用过程中,要定义两个变量x,y
那么如果求a对于p的逆元其实就是ex_gcd(a,p,x,y),那么x就是a对p的逆元,但是用ex_gcd需要注意一点是,一定要防止得到负数,也就是下面的代码
ex_gcd(x, mod, x, y);
x = (x + mod) % mod;
有这个才能保证不出现负数的情况
然后其他和快速幂一样进行求解即可
#include<bits/stdc++.h>
using namespace std;
int a[2000005];
#define mod 1000000007
#define int long long
int ex_gcd(int a, int b, int& x, int& y)
{
if (b == 0)
{
x = 1;
y = 0;
return a;
}
// x,y调换传给下一次递归等价于x1 = y2
int t = ex_gcd(b, a % b, y, x);
//等价y1 = x2 -(a/b) * y2
y -= a / b * x;
return t;
}
void jc(int n)//算出0-n的阶乘
{
a[0] = 1;
for (int i = 1; i <= n; i++)
{
a[i] = a[i - 1] * i % mod;
}
}
int c(int n, int m)
{
int ans,x, y;
ex_gcd(a[m], mod, x, y);
x = (x + mod) % mod;
ans = a[n] * x % mod;
ex_gcd(a[n-m], mod, x, y);
x = (x + mod) % mod;
ans = ans * x % mod;
return ans;
}
signed main()
{
int n, m,t;
jc(2000001);
while (cin >> n >> m)
{
cout << c(n, m) << endl;
}
return 0;
}
卢卡斯定理求组合数
卢卡斯定理用来求组合数时,适用于非常大的 n 和 m ,但是 p 却比较小的计算,这点很重要,因为其实卢卡斯定理使用的场景并不多,所以需要判断该题需要卢卡斯定理才能用。
ps:此处的括号(n,m)其实是组合数的简写形式,也就是c(n,m)
并且观察卢卡斯定理,可以看出来,对于第一项部分,也可以继续进行卢卡斯定理计算,所以易得可以用函数的递归。
他的意义就是把n和m对p除过以后在计算即可,在n,m比较大的时候很好用,P也不能太大,不然求出来就没有什么意义。下面挂一道板子,可以训练一下。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100005
#define mod 1000000007
int p;
int qpow(int n)
{
int m=p-2;
int ans=1,t=n;
while(m)
{
if(m&1)ans=ans*t%p;
t=t*t%p;
m>>=1;
}
return ans;
}
int jc(int n)
{
int t=1;
for(int i=1;i<=n;i++)
t=t*i%p;
return t;
}
int c(int n,int m)
{
if(n<m)swap(n,m);return 0;//若m比n大,则输出0
int t;
t=jc(n)*qpow(jc(m))%p;
t=t*qpow(jc(n-m))%p;
return t;
}
int lcs(int n,int m)
{
if(m==0)return 1;//递归 出口
return lcs(n/p,m/p)*c(n%p,m%p)%p;//递归求Lcs,来求阶乘
}
signed main()
{
int n,m,t;
cin>>t;
while(t--)
{
cin>>n>>m>>p;
m+=n;
cout<<lcs(m,n)<<endl;
}
return 0;
}
这里其实还是用到了快速幂取模的算法,毕竟卢卡斯定理只是降低时间复杂度的算法嘛,所以即使减低了,还是挺大的,还是要用快速幂取模
后记
这就是三种方法,其实还有一个高精度求组合数。那就是五个方法了,但还是回字的四种写法,就还是四种吧。
这其实主要是为了给我以后打比赛整板子用的,嘻嘻
所以注释偏应用一点,如果想看具体逻辑的话,可以找其他大佬的博客的,就这样,睡了!