[学习笔记]组合数学
前言
这篇博客是为了防止自己遗忘知识点,并且对知识加深理解的
1.排列
从n个人里面取m个人来排队,看能排多少种排法
,
那么很明显,第一个位置有5个人可选择,而第二个位置剩4个人可选择,第三个位
置剩3个人可选择,所以总方案数就是
种选择
所以得出结论(草率至极啊)
即
//x!表示的是1到x的阶乘
2.组合
从n个人里面随便选出m个人,问有多少种选法,
,
那么我们假设按照排列问题来选,那么就一共有60种选法。
然而我们只考虑选了某几个人,不考虑他们怎么排,
所以就用排列的方案数除以这 个人全排列的方案数,
就得到了组合的方案数,即(草率至极啊)
即
3.组合数取模(卢卡斯定理)
首先了解一下四则运算的取模
(A + B) % mod = ((A % mod) + (B % mod)) % mod
(A B) % mod = ((A % mod) (B % mod)) % mod
(A - B) % mod =((A % mod) - (B % mod) +mod) % mod(注意减法防止出现负数要加一个mod)
(A / B) % mod = ((A % mod) * (inv(B) % mod)) % mod(注意除法要乘逆元取模)
有大数据取模的话,千万别最后一步再取,一定用公式分散到各步取模,最后还一定要开long long,不然必炸
好,那现在问 的值是多少
须知
(b的逆元)
所以求组合数必须要特殊处理一下。
直接求逆元再按原来公式算是不可行的。
用一段错误代码解释一下:
错误代码:
void get_inv(int a)
{
inv[1]=1;
for(int i=2;i<=a;++i)
inv[i]=(mod-(mod/i))*inv[mod%i]%mod;
}
int A(int x,int y)
{
int ans=1;
for(int i=x-y+1;i<=x;i++)
{
ans=(ans%mod*i%mod)%mod;
}
return ans;
}
int C(int x,int y)
{
return (A(x,y)*inv[A(y,y)]%mod)%mod;
}
当 和 很大时,
get_inv求的是所有数字的逆元,并且只能求小于模数的逆元,
当 大于模数之后就全是0。并且求 时排列数一旦成为
模数的倍数之后,为了防止溢出你不得不取模,但取了模
就是0,答案显然不对。因此当 和 过大了之后,我们
要换用一个更优的算法。
所以就有了Lucas定理
但是本文相对于原文做出了一些便于个人理解的改动。
前置知识:费马小定理
如果 是一个质数而整数 不是 的倍数,则有公式
,这是一个同余等式,你可以理解成
,也可以理解成两边都模上p的答案相等。
两边同时除以
//数组表示在 模数下的逆元
所以
Lucas定理是用来求 , 为素数的值。
也就是
(证明什么的,我也不会,但这个定理还算好记,请叫我一声蒟蒻。)
递归出口为 时返回
求时,即 ,(p是素数,n和m均小于p)
(这个就是( (b的逆元) )
由于 是素数,由费马小定理就可知
注意
之后,我们在 较小时预处理出 内所有阶乘 的值,
然后用快速幂费马小定理求出逆元就可以求出解。
而如果 较大时,中间就容易炸,就只能逐项求出分母 和分子 模上p的值,然后通过
快速幂费马小定理求逆元求解。
较大(如 ),不打表:
ll Pow(ll a, ll b, ll m)
{
ll ans = 1;
a %= m;
while(b)
{
if(b & 1)ans = (ans % m) * (a % m) % m;
b /= 2;
a = (a % m) * (a % m) % m;
}
ans %= m;
return ans;
}
ll inv(ll x, ll p)//x关于p的逆元,p为素数
{
return Pow(x, p - 2, p);
}
ll C(ll n, ll m, ll p)//组合数C(n, m) % p
{
if(m > n)return 0;
ll up = 1, down = 1;//分子分母;
for(int i = n - m + 1; i <= n; i++)up = up * i % p;
for(int i = 1; i <= m; i++)down = down * i % p;
return up * inv(down, p) % p;
}
ll Lucas(ll n, ll m, ll p)
{
if(m == 0)return 1;
return C(n % p, m % p, p) * Lucas(n / p, m / p, p) % p;
}
较小(大约 范围内就叫小),阶乘打表
inline void get_pw()
{
pw[0]=1;
for(int i=1;i<=1000000;i++)
{
pw[i]=pw[i-1]*i%p;
}
}
inline ll Pow(ll a,ll b)
{
ll ans=1;
ans%=p;
while(b)
{
if(b&1)ans=(ans%p)*(a%p)%p;
b/=2;
a=(a%p)*(a%p)%p;
}
ans%=p;
return ans;
}
inline ll inv(ll x)
{
return Pow(x,p-2);
}
inline ll C(ll n,ll m)
{
if(m>n)return 0;
ll up=1,down=1;
up=pw[n];down=pw[m]*pw[n-m];
return up*inv(down)%p;
}
inline ll Lucas(ll n,ll m)
{
if(m==0)return 1;
return Lucas(n/p,m/p)*C(n%p,m%p)%p;
}
注意 是内置函数,自己打应写成 。
4.错排问题
假设我们有 到 共 个数,和 到 个位置
要求任何一个数字都不能放到与自身编号相同的位置上,比如
不能放到 上,然后问你有多少种方案。
解法1:递推式
设 表示有 个数字和 个位置的方案数,
当 时,数字只能放在自己对应的位置上,不可能出现
错排。故 。
当 时,只存在一种情况,即两个数字交换位置。
故 。
之后进行公式推导(草率至极啊)
首先, 号元素必定要排在第 ~ 个位置
的其中之一,所以有 种方法。
然后,假设1号元素放在了第 个位置,那么下一
个就要排第 个元素(鸠占鹊巢,背井离乡)。
再然后, 号元素的排列有两种方式,
一是放在第 个位置,剩下的 个元素进行错排,共有
种可能。
二是不放在第 个位置,这时如果不放 场上有 个空位,
这时就形成了包括 元素在内的 个元素的错排,共有
种可能。
所以, 号元素共有 种可能
又因为第一号元素共有 种放法,根据乘法原理,
我们得知,
递推式为:
。
//解法二:容斥原理(以后填坑吧)
5.递推问题
又称数位dp,一般是枚举方案数。
一般来说 表示的是共 个位置时的方案数,
然后依照从前的某个状态来推这里的方案数。
以这道奶牛题举例。
它之中就存在一个递推关系,
每一个位置 被新加进来,首先要继承上一个位置的方案数,
因为上个位置的方案在这个位置仍然存在。
之后就要继承 位置的方案数,原因是我们把
拆解就会发现由全是母牛,一头公牛,两头公牛……等分别的方案数组成。
很明显一头公牛的方案就是上一级的方案加一,而全是母牛
的方案数不会变,因此 位置全是母牛的这 个方案
就被当作一头公牛的新方案数加进继承了 位置方案数的
位置了。
如果我们再看更高级的公牛数量,比如 头。
那么第 位置的 头公牛的方案数
就会被加入到第 个位置的 头公牛的方案数上,
因为在新增了 个空位之后,第 位置的
头公牛方案数已经可以在原来的基础上再添置一头公牛,
变成 头公牛的方案数了。
由此就可以得出递推式:
所以这就是递推问题,总结一下可以拆成小块来看,而且还
可以自己多推几个式子,猜一下再证明。
当然这个题还可以用组合数学做,只要求出放 头公牛
至少需要的奶牛数,别的地方随便你放。 从 到 跑一遍就行了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】