组合数学

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


组合数学概述

两大原理

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

加法原理

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

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

乘法原理

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

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

排列数和组合数

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

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

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

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

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

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

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

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

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

其他实用的数

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

Catalan数

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

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

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

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

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

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

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

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

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

以下是推导:

我们可以通过同一个问题两个解法的转换来获得Catalan数的通项公式。
对于第三个问题,我们设合法的序列个数为hnh_nhn​,不合法的序列为unu_nun​
显然有:
hn+un=Cn2nh_n+u_n=C_n^{2n}hn​+un​=Cn2n​
若括号序列的最短的不合法的前缀长度是kkk,显然kkk是奇数且这个前缀由k/2+1k/2+1k/2+1个-1和k/2k/2k/2个1组成,同时第kkk个值是-1。

如果我们把这kkk个数,得到了一个由n+1n+1n+1个-1和n−1n-1n−1个1组成的序列,显然这个取反操作是可逆的(找大于0的前缀和并且取反即可),那么不合法的序列和由n+1n+1n+1个-1和n−1n-1n−1个1组成的序列就是一一对应的,因此
un=Cn+12nu_n=C_{n+1}^{2n}un​=Cn+12n​
相减可以得到:
hn=Cn2n−Cn+12nh_n=C_n^{2n}-C_{n+1}^{2n}hn​=Cn2n​−Cn+12n​
稍稍转化一下可以得到:
hn=1n+1Cn2nh_n=\frac{1}{n+1}C_n^{2n}hn​=n+11​Cn2n​
然后一阶递推公式就显而易见了,直接用通项公式减一下就行。

第一类Stiring数

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

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

第二类Stiring数

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

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

Bell数

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

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

亿些经典问题

咕咕咕

程序中的排列组合

逆元

什么是逆元

我们计算组合数的时候,经常会遇到要除以一个很大的数,同时要求余,我们知道一般情况下,乘法的逆运算是除法,但是,在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互质

逆元怎么算

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

费马小定理法

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

那么,根据费马小定理:在aaa与ppp互质的情况下,有:

ap−1≡1modpa^{p-1} \equiv 1 \mod p ap−1≡1modp

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

ap−2×a≡1modpa^{p-2} \times a \equiv 1 \mod p ap−2×a≡1modp

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

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

由于一般这个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<bits/stdc++.h>
#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(logP)O(log P)O(logP),算是一个常数但是有时还是有点慢,加上记忆化之后感觉也不够快,看着难受。

拓展欧几里得法

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

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

规定gcd⁡(m,0)=n\gcd(m,0)=ngcd(m,0)=n
则有gcd⁡(m,n)=gcd(mmodn,n)\gcd(m,n)=gcd(m \mod n,n)gcd(m,n)=gcd(mmodn,n)

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

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

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

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

已知
gcd⁡(n,m)=gcd(mmodn,n){gcd⁡(m,n)=mx1+ny1gcd⁡(mmodn,n)=(mmodn)x2+ny2\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​+ny1​gcd(mmodn,n)=(mmodn)x2​+ny2​​
然后我们进行推论:
∵mmodn=m−m/n×n∴gcd(mmodn,n)=(m−m/n×n)x2+ny2∵gcd⁡(n,m)=gcd(mmodn,n)∴(m−m/n×n)x2+ny2=mx1+ny1\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=m−m/n×n∴gcd(mmodn,n)=(m−m/n×n)x2​+ny2​∵gcd(n,m)=gcd(mmodn,n)∴(m−m/n×n)x2​+ny2​=mx1​+ny1​
我们对着最后那个式子一通爆算(这里实在懒得写了),就可以得出:
{x1=y2y1=x2−m/n∗y2\left\{ \begin{array}{lc} x_1=y_2\\ y_1=x_2-m/n*y_2\\ \end{array} \right. \\ {x1​=y2​y1​=x2​−m/n∗y2​​
有了这个伟大的结论,我们就可以通过类似欧几里得算法的方式得到xxx和yyy的一组解

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

然后我们回来继续狂推:

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

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

#include<bits/stdc++.h>
#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=pmodi∴p=k∗i+r∴k∗i+r≡0(modp)∴k∗r−1+i−1≡0(modp)∴i−1≡−k∗r−1(modp)∴inv[i]=−(p/i)∗inv[pmodi]modp∵i>0∴inv[i]=(p−p/i)∗inv[pmodi]modp现在假设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=pmodi∴p=k∗i+r∴k∗i+r≡0(modp)∴k∗r−1+i−1≡0(modp)∴i−1≡−k∗r−1(modp)∴inv[i]=−(p/i)∗inv[pmodi]modp∵i>0∴inv[i]=(p−p/i)∗inv[pmodi]modp
因此,递推式即为inv[i]=(p−p/i)∗inv[pmodi]modpinv[i]=(p−p/i)∗inv[p \mod i]\mod pinv[i]=(p−p/i)∗inv[pmodi]modp

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

#include<bits/stdc++.h>
#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<bits/stdc++.h>
#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(nlog⁡n)O(n\log n)O(nlogn)预处理,O(1)O(1)O(1)查询(鬼知道这个板子用了多少次)

【蒟蒻の笔记】OI中组合数学相关推荐

  1. 【蒟蒻の笔记】CSP初赛复习笔记

    CSP初赛复习笔记 初赛什么都能考?就nm离谱/doge 计算机科学发展史 起源 图灵和图灵机以及其他成就 Alan Mathison Turing--艾伦·麦席森·图灵,于1936年发表了图灵机这一 ...

  2. 【蒟蒻の笔记】圆方树初识

    圆方树 首先描述一下圆方树: 注:我们把一条边连接两个节点的图也认为是点双连通的. 对于一个无向连通图,对于每个点双连通分量建立一个新的点,新的点作为"方点",原本的点作为&quo ...

  3. 一只蒟蒻的A*学习笔记

    更多请见DUMBLOG 一只蒟蒻的A* 学习笔记 A* 是啥?? A* 用来干啥?? 首先,A-Star 算法只能用在数据规模很大的搜索题中,这时直接用 BFS 会超时,而利用 启发函数(估价函数) ...

  4. 蒟蒻的python 学习笔记 ——北京理工大学嵩天mooc(有时间就看就更新,尽量让笔记容易懂,蟹蟹各位大神不吝赐教)

    蒟蒻的python 学习笔记 前言 课一:python语言程序设计 第一周:python 基本语法元素 1.1 程序设计基本方法 1.2 python 环境配置 1.3 实例1:温度转换 1.4 py ...

  5. python字符串去头尾_悉尼大学某蒟蒻的Python学习笔记

    About me 本蒟蒻是悉尼大学计算机科学大一的学生,这篇博客记录了学习INFO1110这门课的一些心得,希望能对大家有帮助. To start with 因为计算机只能识别机器语言,所以我们需要编 ...

  6. 蒟蒻的线段树入门模板笔记

    线段树适合处理那些问题? 线段树是算法竞赛中常用的用来维护 区间信息 的数据结构. 线段树可以在O(logN)的时间复杂度内实现单点修改.区间修改.区间查询(区间求和,求区间最大值,求区间最小值,求区 ...

  7. 漫谈OI中的群论入门

    前言 本文以群论的一些基本概念及定理证明为主,且多为信息学竞赛所应用,如有不当之处,还望指正 本文对burnside引理与Polya定理仅作引入与证明,达到初步理解的目的,不作深入讨论,具体题目和实现 ...

  8. 蒟蒻的C盘怎么又变红了?(C盘空间快满了怎么清理,AppData,winsxs,Local,Roaming等文件夹如何清理,哪些文件可以删除,哪些属于垃圾)

    关注作者:JuruoAssociation 原创于 CSDN 原创不易,谢绝转载!不曾授权任何转载!建立在抄袭上的流量不会持久.你选择的光,将驱散你身边的黑暗,也将给我们伟大祖国版权事业的未来添一把炬 ...

  9. [颓废史]蒟蒻的刷题记录

    QAQ蒟蒻一枚,其实我就是来提供水题库的. 以下记录从2016年开始. 1.1 1227: [SDOI2009]虔诚的墓主人 树状数组+离散化 3132: 上帝造题的七分钟 树状数组 二维区间加减+查 ...

最新文章

  1. 2010.12.14 关于decimal和Numeric类型
  2. android rebound平移,Android 仿 IOS 拖拽回弹之进阶 ReboundFrameLayout
  3. C# 通用Clone
  4. Silverlight 解谜游戏 之十三 游戏暗示(2)
  5. 【详细解读】进程管理 -死锁问题 系统有三个进程:A B C 这3个进程都需要5个系统资源。如果系统至少有多少个资源,则不可能发生死锁
  6. 使用Apache KeyedObjectPool的ssh连接池
  7. C语言——循环控制语句
  8. 49 CO配置-控制-获利能力分析-把控制范围分配给经营范围
  9. nginx实现动态分离,解决css和js等图片加载问题
  10. 安卓传感器全解:注册、注销传感器、监听传感器,距离传感器、方向传感器、陀螺仪、加速计、磁场、气压传感器
  11. pandas—pandas.DataFrame.iterrows的使用
  12. MyEclipse10破解详解过程
  13. 如何注册PayPal账户
  14. 2020年新年新气象
  15. 手机屏幕取词翻译软件哪个比较好?快看这篇文章,它能告诉你
  16. 第1章第1节:启动PowerPoint并创建和放映幻灯片 [PowerPoint精美幻灯片实战教程]
  17. tp5 对接腾讯云聊天
  18. “啪”一炮就通!管道疏通神器终于诞生,马桶、下水道再也不怕堵!
  19. vue设置scrollTop不起作用
  20. 解读机器学习实践者必须熟悉的14种机器学习非算法类型

热门文章

  1. 龙族幻想最新东京机器人位置_龙族幻想凌晨四点的东京机器人位置在哪?
  2. 《python灰帽子》笔记--构建自己的调试器
  3. 2022 Java面试题道通科技
  4. USACO 3.2 Sweet Butter 香甜的黄油
  5. 转:写一个块设备驱动
  6. Windows打开局域网共享快捷方式脚本制作
  7. [转载]中国最致命的薄弱环节!(一个机械类毕业生的心声)
  8. 隨手可及的預測方法之二
  9. 逻辑 java 猜心术_如何玩简单的数字猜心术
  10. RPA学习-数据表处理