[学习笔记]组合数学

前言

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

1.排列

从n个人里面取m个人来排队,看能排多少种排法

n=5m=3

那么很明显,第一个位置有5个人可选择,而第二个位置剩4个人可选择,第三个位

置剩3个人可选择,所以总方案数就是

5×4×3=60 种选择

所以得出结论(草率至极啊)

A(n,m)=n×(n1)×(n2)×(nm+1)

A(n,m)=n!(nm)!

//x!表示的是1到x的阶乘

2.组合

从n个人里面随便选出m个人,问有多少种选法,

n=5 , m=3

那么我们假设按照排列问题来选,那么就一共有60种选法。

然而我们只考虑选了某几个人,不考虑他们怎么排,

所以就用排列的方案数除以这 m 个人全排列的方案数,

就得到了组合的方案数,即(草率至极啊)

C(n,m)=A(n,m)÷A(m,m)

C(n,m)=n!(nm)!m!

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,不然必炸

好,那现在问 C(n,m)modp 的值是多少

须知

(a÷b)modc=a×(b的逆元)modc

所以求组合数必须要特殊处理一下。

直接求逆元再按原来公式算是不可行的。

用一段错误代码解释一下:

错误代码:

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

nm 很大时,

get_inv求的是所有数字的逆元,并且只能求小于模数的逆元,

n 大于模数之后就全是0。并且求 A 时排列数一旦成为

模数的倍数之后,为了防止溢出你不得不取模,但取了模 A

就是0,答案显然不对。因此当 nm过大了之后,我们

要换用一个更优的算法。

所以就有了Lucas定理

(原文出处)

但是本文相对于原文做出了一些便于个人理解的改动。


前置知识:费马小定理

如果 p 是一个质数而整数 a 不是 p 的倍数,则有公式

a(p1)1(modp),这是一个同余等式,你可以理解成

(a(p1)1)|p,也可以理解成两边都模上p的答案相等。

两边同时除以a

a(p2)inv[a](modp) //inv数组表示在 p 模数下的逆元

所以 inv[a]=a(p2)


Lucas定理是用来求 c(n,m)modpp 为素数的值

C(n,m)%p=C(n/p,m/p)×C(n%p,m%p)%p

也就是

Lucas(n,m)%p=Lucas(n/p,m/p)×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=A(n,m)A(m,m)%p=A(n,m)×inv[A(m,m)]%p

(这个就是( a÷b)modc=a× (b的逆元) modc

由于 p 是素数,由费马小定理就可知

inv[A(m,m)]=A(m,m)(p2)

注意 a×b%c=(a×b)%c

之后,我们在 p 较小时预处理出 1p 内所有阶乘 %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.错排问题

原文出处

假设我们有 1nn个数,和 1n 个位置

要求任何一个数字都不能放到与自身编号相同的位置上,比如

1 不能放到 a[1] 上,然后问你有多少种方案。

解法1:递推式

f[n] 表示有 n 个数字和 n 个位置的方案数,

n=1 时,数字只能放在自己对应的位置上,不可能出现

错排。故 f(1)=0

n=2 时,只存在一种情况,即两个数字交换位置。

f(2)=1

之后进行公式推导(草率至极啊)

首先1 号元素必定要排在第 2 ~ n 个位置

的其中之一,所以有 (n1) 种方法。

然后,假设1号元素放在了第 k 个位置,那么下一

个就要排第 k 个元素(鸠占鹊巢,背井离乡)。

再然后k 号元素的排列有两种方式,

一是放在第 1 个位置,剩下的 (n2) 个元素进行错排,共有

f(n2) 种可能。

二是不放在第 1 个位置,这时如果不放 k 场上有 (n1) 个空位,

这时就形成了包括 k 元素在内的 (n1) 个元素的错排,共有

f(n1) 种可能。

所以, k 号元素共有 f(n1)+f(n2)种可能

又因为第一号元素共有 (n1) 种放法,根据乘法原理,

我们得知,

递推式为

f(n)=(n1)×(f(n1)+f(n2) )

//解法二:容斥原理(以后填坑吧)

5.递推问题

又称数位dp,一般是枚举方案数。

一般来说 f[n] 表示的是共 n 个位置时的方案数,

然后依照从前的某个状态来推这里的方案数。

这道奶牛题举例。

它之中就存在一个递推关系,

每一个位置 n 被新加进来,首先要继承上一个位置的方案数,

因为上个位置的方案在这个位置仍然存在。

之后就要继承 nk1 位置的方案数,原因是我们把

f[n] 拆解就会发现由全是母牛,一头公牛,两头公牛……等分别的方案数组成。

很明显一头公牛的方案就是上一级的方案加一,而全是母牛

的方案数不会变,因此 nk1 位置全是母牛的这 1 个方案

就被当作一头公牛的新方案数加进继承了 n1 位置方案数的

n 位置了。

如果我们再看更高级的公牛数量,比如 2 头。

那么第 nk1 位置的 1 头公牛的方案数

就会被加入到第 n 个位置的 2 头公牛的方案数上,

因为在新增了 (k+1) 个空位之后,第 nk1 位置的

1 头公牛方案数已经可以在原来的基础上再添置一头公牛,

变成 2 头公牛的方案数了。

由此就可以得出递推式:

f[n]=f[n1]+f[nk1]   (ik10)

f[n]=f[n1]+1   (ik1<0)

所以这就是递推问题,总结一下可以拆成小块来看,而且还

可以自己多推几个式子,猜一下再证明。

当然这个题还可以用组合数学做,只要求出放 i 头公牛

至少需要的奶牛数,别的地方随便你放。i0(n1) 跑一遍就行了

posted @   liangchenming  阅读(101)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示