组合数取模(回字)的四种写法

前言

近期遇到两个组合数的题,于是写一下组合数的解法,当然虽然确实有四种,但是我目前只会三种

标题致敬一下孔乙己而已,但是后面学了也会补的

正文

杨辉三角求组合数

利用了这个公式

作用于数组也就是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也不能太大,不然求出来就没有什么意义。下面挂一道板子,可以训练一下。

【模板】卢卡斯定理/Lucas 定理

#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;
}

这里其实还是用到了快速幂取模的算法,毕竟卢卡斯定理只是降低时间复杂度的算法嘛,所以即使减低了,还是挺大的,还是要用快速幂取模

后记

这就是三种方法,其实还有一个高精度求组合数。那就是五个方法了,但还是回字的四种写法,就还是四种吧。

这其实主要是为了给我以后打比赛整板子用的,嘻嘻

所以注释偏应用一点,如果想看具体逻辑的话,可以找其他大佬的博客的,就这样,睡了!

晚安,一夜好眠~

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值