[学习笔记]组合数学
前言
这篇博客是为了防止自己遗忘知识点,并且对知识加深理解的
1.排列
从n个人里面取m个人来排队,看能排多少种排法
\(n=5\) , \(m=3\)
那么很明显,第一个位置有5个人可选择,而第二个位置剩4个人可选择,第三个位
置剩3个人可选择,所以总方案数就是
\(5\times4\times3=60\) 种选择
所以得出结论(草率至极啊)
\(A(n,m)=n \times (n-1) \times (n-2) ……\times (n-m+1)\)
即\(A(n,m)=\frac{n!}{(n-m)!}\)
//x!表示的是1到x的阶乘
2.组合
从n个人里面随便选出m个人,问有多少种选法,
\(n=5\) , \(m=3\)
那么我们假设按照排列问题来选,那么就一共有60种选法。
然而我们只考虑选了某几个人,不考虑他们怎么排,
所以就用排列的方案数除以这 \(m\) 个人全排列的方案数,
就得到了组合的方案数,即(草率至极啊)
\(C(n,m)=A(n,m)\div A(m,m)\)
即\(C(n,m)=\frac{n!}{(n-m)!m!}\)
3.组合数取模(卢卡斯定理)
首先了解一下四则运算的取模
(A + B) % mod = ((A % mod) + (B % mod)) % mod
(A \(\times\) B) % mod = ((A % mod) \(\times\) (B % mod)) % mod
(A - B) % mod =((A % mod) - (B % mod) +mod) % mod(注意减法防止出现负数要加一个mod)
(A / B) % mod = ((A % mod) * (inv(B) % mod)) % mod(注意除法要乘逆元取模)
有大数据取模的话,千万别最后一步再取,一定用公式分散到各步取模,最后还一定要开long long,不然必炸
好,那现在问 \(C(n,m)\mod p\) 的值是多少
须知
\((a\div b)\mod c=a\times\)(b的逆元)\(\mod c\)
所以求组合数必须要特殊处理一下。
直接求逆元再按原来公式算是不可行的。
用一段错误代码解释一下:
错误代码:
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;
}
当 \(n\) 和 \(m\) 很大时,
get_inv求的是所有数字的逆元,并且只能求小于模数的逆元,
当 \(n\) 大于模数之后就全是0。并且求 \(A\) 时排列数一旦成为
模数的倍数之后,为了防止溢出你不得不取模,但取了模 \(A\)
就是0,答案显然不对。因此当 \(n\) 和 \(m\)过大了之后,我们
要换用一个更优的算法。
所以就有了Lucas定理
但是本文相对于原文做出了一些便于个人理解的改动。
前置知识:费马小定理
如果 \(p\) 是一个质数而整数 \(a\) 不是 \(p\) 的倍数,则有公式
$a^{(p-1)}≡1(\mod p) $,这是一个同余等式,你可以理解成
\((a^{(p-1)}-1)|p\),也可以理解成两边都模上p的答案相等。
两边同时除以\(a\)
\(a^{(p-2)}≡inv[a](\mod p)\) //\(inv\)数组表示在 \(p\) 模数下的逆元
所以 \(inv[a]=a^{(p-2)}\)
Lucas定理是用来求 \(c(n,m) \mod p\) ,\(p\) 为素数的值。
\(C(n,m)\% p=C(n/p,m/p) \times C(n\% p , m \% p)\% p\)
也就是
\(Lucas(n,m)\% p=Lucas(n/p,m/p)\times C(n\% p ,m\%p)\%p\)
(证明什么的,我也不会,但这个定理还算好记,请叫我一声蒟蒻。)
\(Lucas\) 递归出口为 \(m=0\) 时返回 \(1\)
求\(C(n\%p,m\%p)\)时,即 \(C(n,m)\%p\),(p是素数,n和m均小于p)
\(C(n,m)\%p=\frac{A(n,m)}{A(m,m)}\%p=A(n,m)\times inv[A(m,m)]\%p\)
(这个就是( \(a \div b )\mod c=a\times\) (b的逆元) \(\mod c\))
由于 \(p\) 是素数,由费马小定理就可知
\(inv[A(m,m)]=A(m,m) ^{(p-2)}\)
注意 \(a\times b \%c=(a\times b)\%c\)
之后,我们在 \(p\) 较小时预处理出 \(1-p\) 内所有阶乘 \(\%p\) 的值,
然后用快速幂费马小定理求出逆元就可以求出解。
而如果 \(p\) 较大时,中间就容易炸,就只能逐项求出分母 \(A(n,m)\) 和分子 \(A(m,m)\) 模上p的值,然后通过
快速幂费马小定理求逆元求解。
\(p\) 较大(如 \(1e9+7\) ),不打表:
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;
}
\(p\) 较小(大约 \(1e6\) 范围内就叫小),阶乘打表
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;
}
注意 \(pow\) 是内置函数,自己打应写成 \(Pow\)。
4.错排问题
假设我们有 \(1\) 到 \(n\) 共 \(n\)个数,和 \(1\) 到 \(n\) 个位置
要求任何一个数字都不能放到与自身编号相同的位置上,比如
\(1\) 不能放到 \(a[1]\) 上,然后问你有多少种方案。
解法1:递推式
设 \(f[n]\) 表示有 \(n\) 个数字和 \(n\) 个位置的方案数,
当 \(n=1\) 时,数字只能放在自己对应的位置上,不可能出现
错排。故 \(f(1)=0\) 。
当 \(n=2\) 时,只存在一种情况,即两个数字交换位置。
故 \(f(2)=1\) 。
之后进行公式推导(草率至极啊)
首先, \(1\) 号元素必定要排在第 \(2\) ~ \(n\) 个位置
的其中之一,所以有 \((n-1)\) 种方法。
然后,假设1号元素放在了第 \(k\) 个位置,那么下一
个就要排第 \(k\) 个元素(鸠占鹊巢,背井离乡)。
再然后, \(k\) 号元素的排列有两种方式,
一是放在第 \(1\) 个位置,剩下的 \((n-2)\) 个元素进行错排,共有
\(f(n-2)\) 种可能。
二是不放在第 \(1\) 个位置,这时如果不放 \(k\) 场上有 \((n-1)\) 个空位,
这时就形成了包括 \(k\) 元素在内的 \((n-1)\) 个元素的错排,共有
\(f(n-1)\) 种可能。
所以, \(k\) 号元素共有 \(f(n-1) + f(n-2)\)种可能
又因为第一号元素共有 \((n-1)\) 种放法,根据乘法原理,
我们得知,
递推式为:
\(f(n)=(n-1)\times (f(n-1)+f(n-2)\ )\)。
//解法二:容斥原理(以后填坑吧)
5.递推问题
又称数位dp,一般是枚举方案数。
一般来说 \(f[n]\) 表示的是共 \(n\) 个位置时的方案数,
然后依照从前的某个状态来推这里的方案数。
以这道奶牛题举例。
它之中就存在一个递推关系,
每一个位置 \(n\) 被新加进来,首先要继承上一个位置的方案数,
因为上个位置的方案在这个位置仍然存在。
之后就要继承 \(n-k-1\) 位置的方案数,原因是我们把
\(f[n]\) 拆解就会发现由全是母牛,一头公牛,两头公牛……等分别的方案数组成。
很明显一头公牛的方案就是上一级的方案加一,而全是母牛
的方案数不会变,因此 \(n-k-1\) 位置全是母牛的这 \(1\) 个方案
就被当作一头公牛的新方案数加进继承了 \(n-1\) 位置方案数的
\(n\) 位置了。
如果我们再看更高级的公牛数量,比如 \(2\) 头。
那么第 \(n-k-1\) 位置的 \(1\) 头公牛的方案数
就会被加入到第 \(n\) 个位置的 \(2\) 头公牛的方案数上,
因为在新增了 \((k+1)\) 个空位之后,第 \(n-k-1\) 位置的
\(1\) 头公牛方案数已经可以在原来的基础上再添置一头公牛,
变成 \(2\) 头公牛的方案数了。
由此就可以得出递推式:
\(f[n]=f[n-1]+f[n-k-1]\ \ \ (i-k-1 ≥ 0)\)
\(f[n]=f[n-1]+1\ \ \ (i-k-1 < 0)\)
所以这就是递推问题,总结一下可以拆成小块来看,而且还
可以自己多推几个式子,猜一下再证明。
当然这个题还可以用组合数学做,只要求出放 \(i\) 头公牛
至少需要的奶牛数,别的地方随便你放。\(i\) 从 \(0\) 到 \((n-1)\) 跑一遍就行了