【蒟蒻の笔记】OI中组合数学

组合数学

这东西作为csp初赛常驻题目,有时也会在程序题里ex人,所以,这一块的基础一定要打牢


组合数学概述

两大原理

这里指的是加法原理和乘法原理

加法原理

加法原理即把同阶段的不同的情况相加,举个形象的例子:

从一个城市到另一个城市,有 n n n条公路,同时又有 m m m条铁路,则总共有 n + m n+m n+m种方法

乘法原理

乘法原理即把不同阶段的选择数相乘,举个例子

从a城市经过b城市到c城市,从a到b有 n n n条路,从c到b有 m m m条路,则从a经过b到c有 n ∗ m n*m nm条路径

排列数和组合数

下面我们了解一下组合数学的两大基本运算

C n m C_{n}^{m} Cnm表示在 m m m不同的物体中 n n n选择n个的方案数,叫做组合数,有

C n m = n ! m ! ( n − m ) ! C_{n}^{m}=\frac{n!}{m!(n-m)!} Cnm=m!(nm)!n!

A n m A_{n}^{m} Anm表示在 m m m不同的物体中 n n n选择n个排成序列的方案数,叫做排列数,有

A n m = n ! ( n − m ) ! A_{n}^{m}=\frac{n!}{(n-m)!} Anm=(nm)!n!

这样我们就解决了两个最基本的问题

下面我们来看一下原理(我用容易理解的方法讲一下):

先看排列数 A n m A_{n}^{m} Anm,对于待选的序列,第一项有 n n n个选择,第二项有 n − 1 n-1 n1个,一次类推,第 m m m项有 n − m + 1 n-m+1 nm+1个,根据乘法原理,把它们相乘,总共即为从 n n n乘到 n − m + 1 n-m+1 nm+1,也就是, A n m = n ! ( n − m ) ! A_{n}^{m}=\frac{n!}{(n-m)!} Anm=(nm)!n!

下面再看组合数 C n m C_{n}^{m} Cnm,它和排列数的区别在于排列数区分了物品的顺序,而组合数没有,那么就可以直接把有序转化成无序,即 C n m = A n m m ! C_{n}^{m}=\frac{A_{n}^{m}}{m!} Cnm=m!Anm,代入得 C n m = n ! m ! ( n − m ) ! C_{n}^{m}=\frac{n!}{m!(n-m)!} Cnm=m!(nm)!n!

其他实用的数

各个伟大的前辈们已经为我们整理出了一套完整的组合数学体系,其中除组合数和排列数外,还有其他的数,全部与排列组合有关

Catalan数

我没看到Catalan数的标准表示法,就自己随便写吧

首先我们来看一组题目(也可以想想为什么是一组)

已知一个凸n边型,众所周知,它可以划分为n-1个三角形,求有多少种划分方法

已知一棵二叉树,有n个节点,求这课二叉树有几种情况(注意二叉树区分左右子树)

已知n对括号,求有多少个括号序列正确匹配

有n个序列依次进栈,求有多少种合法的出栈序列

一个长度为n的序列全部由1和-1组成,要求多少个序列满足所有前缀和为非负数

由于OIer对二叉树的喜爱与赞美之情我们从第二个问题入手
令当前问题的答案为 h n h_n hn,显然,我们必须要选一个根节点,剩下的节点就被分成左子树和右子树,于是我们得到递推式:
h n = ∑ i = 0 n − 1 h i h n − i − 1 ( h 0 = h 1 = 1 ) h_n=\sum_{i=0}^{n-1} h_ih_{n-i-1} \left(h_0=h_1=1\right) hn=i=0n1hihni1(h0=h1=1)

这就是Catalan数的递推式,显然,只要符合这个递推式就是Catalan数,我们代入剩下的问题就知道了——其实这几个问题的答案完全相同

同时我们还可以得到一些其他公式:
一阶递推公式: h n = 4 n − 2 n + 1 h n − 1 h_n=\frac{4n-2}{n+1}h_{n-1} hn=n+14n2hn1
一个通项公式: h n = C n 2 n − C n + 1 2 n h_n=C_n^{2n}-C_{n+1}^{2n} hn=Cn2nCn+12n
另一个更常用的通项公式: h n = 1 n + 1 C n 2 n h_n=\frac{1}{n+1}C_n^{2n} hn=n+11Cn2n

来来来,开推:
(以下这小段参考了这篇博客)

我们记
C ( x ) = ∑ n ≥ 0 h n x n C(x)=\sum_{n\geq 0}h_nx^n C(x)=n0hnxn(n为整数)
显然,当 n = 0 n=0 n=0时,有 h n x n = 1 h_nx^n=1 hnxn=1(此时h_0=1,x^0同样等于1)
于是把 n = 0 n=0 n=0的情况特殊考虑就有
∑ n ≥ 1 h n x n = C ( x ) − 1 \sum_{n\geq 1}h_nx^n=C(x)-1 n1hnxn=C(x)1
对于式子的左边,我们把 h n h_n hn代入得到:
∑ n ≥ 1 h n x n = ∑ n ≥ 1 ∑ i = 0 n − 1 h i h n − i − 1 x n \sum_{n\geq 1}h_nx^n=\sum_{n\geq 1}\sum_{i=0}^{n-1} h_ih_{n-i-1}x^n n1hnxn=n1i=0n1hihni1xn
然后我们把这个式子中的x提出一个来:
∑ n ≥ 1 h n x n = x ∑ n ≥ 1 ∑ i = 0 n − 1 h i h n − i − 1 x n − 1 \sum_{n\geq 1}h_nx^n=x\sum_{n\geq 1}\sum_{i=0}^{n-1} h_ih_{n-i-1}x^{n-1} n1hnxn=xn1i=0n1hihni1xn1
然后我们在值不变的情况下对式子做一些小小的调整:
∑ n ≥ 1 h n x n = x ∑ n ≥ 0 ∑ i = 0 n h i h n − i x n \sum_{n\geq 1}h_nx^n=x\sum_{n\geq 0}\sum_{i=0}^{n} h_ih_{n-i}x^n n1hnxn=xn0i=0nhihnixn
然后我们再把式子分成两部分:
∑ n ≥ 1 h n x n = x ∑ i ≥ 0 h i x i ∑ n ≥ i h n − i x n − i \sum_{n\geq 1}h_nx^n=x\sum_{i\geq 0}h_ix^i \sum_{n\geq i}h_{n-i}x^{n-i} n1hnxn=xi0hixinihnixni
熟悉吧,这就是我们之前定义的那个C,然后代入得:
∑ n ≥ 1 h n x n = x C ( x ) × C ( x ) \sum_{n\geq 1}h_nx^n=xC(x)\times C(x) n1hnxn=xC(x)×C(x)
即:
∑ n ≥ 1 h n x n = x C 2 ( x ) \sum_{n\geq 1}h_nx^n=xC^2(x) n1hnxn=xC2(x)
然后我们结合之前求的:
∑ n ≥ 1 h n x n = C ( x ) − 1 \sum_{n\geq 1}h_nx^n=C(x)-1 n1hnxn=C(x)1
得到:
x C 2 ( x ) = C ( x ) − 1 xC^2(x)=C(x)-1 xC2(x)=C(x)1
整理一下:
x C 2 ( x ) − C ( x ) + 1 = 0 xC^2(x)-C(x)+1=0 xC2(x)C(x)+1=0
直接上求根公式解得:
C ( x ) = 1 ± 1 − 4 x 2 x C(x)=\frac{1\pm \sqrt {1-4x}}{2x} C(x)=2x1±14x
由幂级数收敛条件(不会的自己去查)得:
C ( x ) = 1 − 1 − 4 x 2 x C(x)=\frac{1-\sqrt {1-4x}}{2x} C(x)=2x114x
然后直接展开:
C ( x ) = 1 − ∑ n ≥ 1 ( 1 2 n ) ( − 4 ) n x n 2 x C(x)=\frac{1-\sum_{n \geq 1}{\frac{1}{2} \choose n}(-4)^nx^n}{2x} C(x)=2x1n1(n21)(4)nxn
特殊看待n=0的情况,得到:
C ( x ) = 1 − 1 − ∑ n ≥ 0 ( 1 2 n ) ( − 4 ) n x n 2 x C(x)=\frac{1-1-\sum_{n \geq 0}{\frac{1}{2} \choose n}(-4)^nx^n}{2x} C(x)=2x11n0(n21)(4)nxn
整理一下,顺便把下面的x约掉得到:
C ( x ) = − ∑ n ≥ 0 ( 1 2 n ) ( − 4 ) n x n − 1 2 C(x)=-\frac{\sum_{n \geq 0}{\frac{1}{2} \choose n}(-4)^nx^{n-1}}{2} C(x)=2n0(n21)(4)nxn1
通过组合数里加上一个n再次调整得到:
C ( x ) = − ∑ n ≥ 0 ( 1 2 n + 1 ) ( − 4 ) n + 1 x n 2 C(x)=-\frac{\sum_{n \geq 0}{\frac{1}{2} \choose n+1}(-4)^{n+1}x^{n}}{2} C(x)=2n0(n+121)(4)n+1xn
然后得到(看到这是不是有点晕,往下看):
h n = − ( 1 2 n + 1 ) ( − 4 ) n + 1 2 h_n=-\frac{{\frac{1}{2} \choose n+1}(-4)^{n+1}}{2} hn=2(n+121)(4)n+1
接着上牛顿二项式定理得到:
h n = ( − 1 ) n + 2 2 2 n + 1 1 2 ( 1 2 − 1 ) ( 1 2 − 2 ) … ( 1 2 − n − 1 + 1 ) ( n + 1 ) ! h_n=(-1)^{n+2}2^{2n+1}\frac{\frac{1}{2}(\frac{1}{2}-1)(\frac{1}{2}-2)\dots(\frac{1}{2}-n-1+1)}{(n+1)!} hn=(1)n+222n+1(n+1)!21(211)(212)(21n1+1)
然后得到:
h n = ( − 1 ) n + 2 − n 2 2 n + 1 − n − 1 ( 2 n − 1 ) ! ! ( n + 1 ) ! h_n=(-1)^{n+2-n}2^{2n+1-n-1}\frac{(2n-1)!!}{(n+1)!} hn=(1)n+2n22n+1n1(n+1)!(2n1)!!
显然这里可以化简一下,同时把 2 n 2^n 2n放进去:
h n = 2 n ( 2 n − 1 ) ! ! ( 2 n ) ! ! ( n + 1 ) ! ( n ! 2 n ) h_n=\frac{2^n(2n-1)!!(2n)!!}{(n+1)!(n!2^n)} hn=(n+1)!(n!2n)2n(2n1)!!(2n)!!
最后的化简得到:
h n = 1 n + 1 C n 2 n h_n=\frac{1}{n+1}C_n^{2n} hn=n+11Cn2n
然后变个形就成了:
h n = C n 2 n − C n + 1 2 n h_n=C_n^{2n}-C_{n+1}^{2n} hn=Cn2nCn+12n
一阶递推公式留给大家自己推(直接用上面的结论)

第一类Stiring数

定义很简单,表示把n个元素划分成m个非空循环排列集合的方案数。
记为 s n m s_n^m snm

直接先看递推式吧:
分两种情况:
这个元素若取,答案就是 ( n − 1 ) s n − 1 m (n-1)s_{n-1}^m (n1)sn1m
若不取,答案就是 s n − 1 m − 1 s_{n-1}^{m-1} sn1m1
根据加法原理直接相加得到:
s n m = s n − 1 m − 1 + ( n − 1 ) s n − 1 m s_n^m=s_{n-1}^{m-1}+(n-1)s_{n-1}^m snm=sn1m1+(n1)sn1m

第二类Stiring数

定义也非常简单,表示把n个元素划分成m个非空集合的方案数。
记为 S n m S_n^m Snm

还是直接先看递推式吧:
分两种情况:
这个元素若取,答案就是 m S n − 1 m mS_{n-1}^m mSn1m
若不取,答案就是 S n − 1 m − 1 S_{n-1}^{m-1} Sn1m1
根据加法原理直接相加得到:
S n m = S n − 1 m − 1 + m S n − 1 m S_n^m=S_{n-1}^{m-1}+mS_{n-1}^m Snm=Sn1m1+mSn1m

Bell数

表示把n个元素划分成若干个非空集合的方案数。
记为 B n B_n Bn

显然有
B n = ∑ k = 1 n S n k B_n=\sum_{k=1}^{n}S_n^k Bn=k=1nSnk
我真不知道这玩意有什么用,就只是一个求和吗?

亿些经典问题

咕 咕

程序中的排列组合

逆元

什么是逆元

我们计算组合数的时候,经常会遇到要除以一个很大的数,同时要求余,我们知道一般情况下,乘法的逆运算是除法,但是,在mod p的情况下,这将不再成立,比如:

1*3%2=1
1%2*3%2=1

但是,

9/3%2=1
9%2/3%2=1

显然除法不适用于mod运算,因此我们需要一种全新的运算。

众所周知,若a*b=1,则x/a=x*b
我们尝试把他推到至mod运算:若a*b≡1 (mod p),则x/a%p=x%p*b%p
此时我们称ab的逆元,同时ba的逆元

看着挺玄乎是不是,把逆元的定义和倒数的定义结合起来看就明白了

有了逆元这玩意,我们就可以解决代码里爆精度或者爆long long的问题了,直接把除法转换成逆元运算就行了。

注意,在逆元运算中,a一定与p互质

逆元怎么算

有了逆元的定义,我们就该在程序中计算逆元了

费马小定理法

首先我们来看伟大的费马小定理

那么,根据费马小定理:在 a a a p p p互质的情况下,有:

a p − 1 ≡ 1 m o d    p a^{p-1} \equiv 1 \mod p ap11modp

那么,我们直接拉出来一个a得到:

a p − 2 × a ≡ 1 m o d    p a^{p-2} \times a \equiv 1 \mod p ap2×a1modp

熟不熟悉!!这就符合我们逆元的定义,于是我们得到了一个重要信息:

在mod p 的情况下, a a a a p − 2 a^p-2 ap2互为逆元

由于一般这个P大得离谱,所以我们采取快速幂(这个原理大家应该都懂,不懂得看看代码应该也懂了):

int _pow(int x,int y){
    if(!y){
        return 1;
    }
    int t=_pow(x,y>>1);
    if(y&1){
        return t*t%P*x%P;
    }
    else{
        return t*t%P;
    }
}

然后就放出代码:

#include
#define int long long
#define P 1000000007
using namespace std;
int inv[1000005];
int _pow(int x,int y){
    if(!y){
        return 1;
    }
    int t=_pow(x,y>>1);
    if(y&1){
        return t*t%P*x%P;
    }
    else{
        return t*t%P;
    }
}
int Inverse(int x){
    if(!inv[x]){
        inv[x]=_pow(x,P-2)
    }
    return inv[x];
}
signed main(){
    int a;
    scanf("%lld%lld",&a);
    printf("%lld",Inverse(a));
    return 0;
}

单次时间复杂度为 O ( l o g P ) O(log P) O(logP),算是一个常数但是有时还是有点慢,加上记忆化之后感觉也不够快,看着难受。

拓展欧几里得法

不管难不难受了我们直接看下一种方法:拓展欧几里得法

所谓欧几里得算法,就是我们平时说的“辗转相除法”求最大公因数,我们来复习一下欧几里得算法:

规定 gcd ⁡ ( m , 0 ) = n \gcd(m,0)=n gcd(m,0)=n
则有 gcd ⁡ ( m , n ) = g c d ( m m o d    n , n ) \gcd(m,n)=gcd(m \mod n,n) gcd(m,n)=gcd(mmodn,n)

在这个非常有趣的欧几里得算法之上,我们有了拓展欧几里得算法:

那么,我们尝试对于不定方程 m x + n y = g c d ( m , n ) mx+ny=gcd(m,n) mx+ny=gcd(m,n)求解

显而易见,我们套用之前的 gcd ⁡ \gcd gcd的话,运算结束时,
方程为 m × x + 0 × y = g c d ( m , 0 ) m \times x+0 \times y=gcd(m,0) m×x+0×y=gcd(m,0)
转化一下就是 m × x = m m \times x=m m×x=m
直接得到
{ x = 1 y = 0 \left\{ \begin{array}{lc} x=1\\ y=0\\ \end{array} \right. {x=1y=0

这里我们知道了最终状态的解,然后看一般情况:
(注意:以下出现的 / / /意思都是整除)

已知
gcd ⁡ ( n , m ) = g c d ( m m o d    n , n ) { gcd ⁡ ( m , n ) = m x 1 + n y 1 gcd ⁡ ( m m o d    n , n ) = ( m m o d    n ) x 2 + n y 2 \gcd(n,m)=gcd(m \mod n,n) \\ \left\{ \begin{array}{lc} \gcd(m,n)=mx_1+ny_1 \\ \gcd(m \mod n,n)=(m \mod n)x_2+ny_2 \\ \end{array} \right. \\ gcd(n,m)=gcd(mmodn,n){gcd(m,n)=mx1+ny1gcd(mmodn,n)=(mmodn)x2+ny2
然后我们进行推论:
∵ m m o d    n = m − m / n × n ∴ g c d ( m m o d    n , n ) = ( m − m / n × n ) x 2 + n y 2 ∵ gcd ⁡ ( n , m ) = g c d ( m m o d    n , n ) ∴ ( m − m / n × n ) x 2 + n y 2 = m x 1 + n y 1 \because m \mod n=m-m/n \times n \\ \therefore gcd(m \mod n,n)=(m-m/n \times n)x_2+ny_2 \\ \because \gcd(n,m)=gcd(m \mod n,n) \\ \therefore (m-m/n \times n)x_2+ny_2=mx_1+ny_1 mmodn=mm/n×ngcd(mmodn,n)=(mm/n×n)x2+ny2gcd(n,m)=gcd(mmodn,n)(mm/n×n)x2+ny2=mx1+ny1
我们对着最后那个式子一通爆算(这里实在懒得写了),就可以得出:
{ x 1 = y 2 y 1 = x 2 − m / n ∗ y 2 \left\{ \begin{array}{lc} x_1=y_2\\ y_1=x_2-m/n*y_2\\ \end{array} \right. \\ {x1=y2y1=x2m/ny2
有了这个伟大的结论,我们就可以通过类似欧几里得算法的方式得到 x x x y y y的一组解

int ex_gcd(int a,int b,int &x,int &y){
    if(b==0){
        x=1;
        y=0;
        return a;
    }
    int res=ex_gcd(b,a%b,x,y);
    int t=x;
    x=y;
    y=t-a/b*y;
    return res;
}

然后我们回来继续狂推:

我们刚才实际上解了 m x + n y = gcd ⁡ ( m , n ) mx+ny=\gcd(m,n) mx+ny=gcd(m,n)这个方程,这个方程也叫“贝祖等式”,显然,在这个等式中 x x x y y y互质,同时,贝祖等式一定存在整数解 x , y x,y x,y,于是……
一个更加伟大的结论浮出水面:
a,b互质的充分必要条件是方程ax+by=1有整数解。

下面我们回归逆元,根据限制, a a a p p p互质,我们就可以得到 a x + p y = 1 ax+py=1 ax+py=1
根据逆元的定义: a × b ≡ 1 ( m o d    p ) a\times b≡1 (\mod p) a×b1(modp)
不讲详细证明了,我们直接对着这两个等式瞪一会,我们就会发现两个等式中的x是相等的,于是,我们就可以直接用拓展欧几里得法求出 a a a的逆元,以下放出代码:

#include
#define int long long
#define p 1000000007
using namespace std;
int inv[1000005];
int ex_gcd(int a,int b,int &x,int &y){
    if(b==0){
        x=1;
        y=0;
        return a;
    }
    int res=ex_gcd(b,a%b,x,y);
    int t=x;
    x=y;
    y=t-a/b*y;
    return res;
}
int Inverse(int a){
    int res,x,y;
    res=ex_gcd(a,n,x,y);
    if(res==1){
        inv[i]=(x%n+n)%n;
    }
    else{
        inv[i]=-1;
    }
    return inv[i];
}
signed main(){
    int a;
    scanf("%lld%lld",&a);
    printf("%lld",Inverse(a));
    return 0;
}

所以搞半天它单次计算的时间复杂度仍然是 O ( log ⁡ p ) O(\log p) O(logp)!@#%^&*()
但是没关系,如果P不是质数,拓展欧几里得算法就有发挥的余地了。
反正相信大家现在已经懂了基本的拓展欧几里得算法,可以在各种地方使用了。

线性递推法

O(n)的方法终于来了

这是一种递推的方法,肉眼可见,1的逆元就是1,然后我们开始推递推公式(这是我在网上学的,但找不到出处了):
这里的/仍然是整除
现在假设 k = p / i , r = p m o d    i ∴ p = k ∗ i + r ∴ k ∗ i + r ≡ 0 ( m o d    p ) ∴ k ∗ r − 1 + i − 1 ≡ 0 ( m o d    p ) ∴ i − 1 ≡ − k ∗ r − 1 ( m o d p ) ∴ i n v [ i ] = − ( p / i ) ∗ i n v [ p m o d    i ] m o d    p ∵ i > 0 ∴ i n v [ i ] = ( p − p / i ) ∗ i n v [ p m o d    i ] m o d    p 现在假设k=p/i,r=p\mod i\\ \therefore p=k∗i+r\\ \therefore k∗i+r≡0(\mod p)\\ \therefore k∗r−1+i−1≡0(\mod p)\\ \therefore i−1≡−k∗r−1(modp)\\ \therefore inv[i]=−(p/i)∗inv[p\mod i]\mod p\\ \because i>0\\ \therefore inv[i]=(p−p/i)∗inv[p \mod i]\mod p\\ 现在假设k=p/i,r=pmodip=ki+rki+r0(modp)kr1+i10(modp)i1kr1(modp)inv[i]=(p/i)inv[pmodi]modpi>0inv[i]=(pp/i)inv[pmodi]modp
因此,递推式即为 i n v [ i ] = ( p − p / i ) ∗ i n v [ p m o d    i ] m o d    p inv[i]=(p−p/i)∗inv[p \mod i]\mod p inv[i]=(pp/i)inv[pmodi]modp

下面直接看代码就没问题了,式子都出来了:

#include
#define int long long
#define P 1000000007
using namespace std;
int inv[1000005];
void init(){
    inv[1]=1;
    for(int i=2;i<=1000000;i++){
        inv[i]=(P-P/i)*inv[P%i]%P;
    }
}
signed main(){
    int a;
    scanf("%lld%lld",&a);
    printf("%lld",Inverse(a));
    return 0;
}

时间复杂度可算是来到了 O ( n ) O(n) O(n)预处理, O ( 1 ) O(1) O(1)查询

特殊逆元:阶乘逆元

这是最特殊也是最常用的逆元求法,排列组合中非常常用,其实就是费马小定理求逆元,不过应用在阶乘上,并没有本质区别,顺手也求个阶乘

由于阶乘逆元通常用来求排列组合,所以直接拿这个当板子:

#include
#define int long long
#define P 1000000007
using namespace std;
int inv[1000005],fac[1000005];
int _pow(int a,int b){
    if(b==0){
        return 1;
    }
    int t=_pow(a,b/2);
    if(b%2==1){
        return t*t%P*a%P;
    }
    else{
        return t*t%P;
    }
}
void init(){
    inv[0]=1;
    fac[0]=1;
    for(int i=1;i<=1000000;i++){
        fac[i]=fac[i-1]*i%P;
        inv[i]=_pow(fac[i],P-2);
    }
}
int C(int n,int m){
    return fac[n]*inv[m]%P*inv[n-m]%P;
}
int A(int n,int m){
    return fac[n]*inv[n-m]%P;
}
void solve(){
    int a,b;
    scanf("%lld%lld",&a,&b);
    printf("%lld\n",C(a,b));
    printf("%lld\n",A(a,b));
}
signed main(){
    int T;
    init();
    scanf("%lld",&T);
    while(T--){
        solve();
    }
}

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)预处理, O ( 1 ) O(1) O(1)查询(鬼知道这个板子用了多少次)

你可能感兴趣的:(漫漫OI路,算法,c++)