[学习笔记]组合数学

前言

这篇博客是为了防止自己遗忘知识点,并且对知识加深理解的

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)\) 跑一遍就行了

posted @ 2022-08-08 17:37  liangchenming  阅读(92)  评论(0编辑  收藏  举报