组合排列和组合数 学习笔记
前置知识点
我们通常使用$P_{n}^{r}$ 表示全排列,而$ CP_{n}^{r}$ 表示圆上全排列, $CC_{n}^{r}$ 表示圆上的组合数
我们通常使用$\binom{n}{r} $ 表示 $C_{n}^{r}$,在下面都会使用 $\binom{n}{r} $ 来描述组合数。
下面给出三种最常见计算组合数计算方法,需要配合逆元食用。
- 组合数计算公式:$ \binom{n}{r} = \frac{n!}{r!(n-r)!} $
- 组合数递推公式:$\binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r} $
- 组合数lucas定理:$\binom{n}{r} \equiv \binom{\left \lfloor \frac{n}{p} \right \rfloor}{\left \lfloor \frac{r}{p} \right \rfloor} \binom{n \ mod \ p}{r \ mod \ p} \ \ (mod\ p) $
下面是对于组合数应用的一些公式和性质:
- 对称性:$ \binom{n}{r} = \binom{n}{n-r} $
- 二项式定理:$(x+y) ^n = \sum\limits_{r=0}^n \binom{n}{r}x^{n-r}y^r$
- 替换定理:$r\binom{n}{r} = n\binom{n-1}{r-1}$ (常使用在组合数求和式化简)
- 组合数求和式:$ \sum\limits_{r=0} ^n \binom{n}{r} = 2^{n} $
- 等差数列合组合数求和式:$\sum\limits_{r=0} ^n r \binom{n}{r} = n 2^{n-1}$
- 等比数列合组合数求和式:$\sum\limits_{r=0} ^n r^2 \binom{n}{r} = n(n+1) 2^{n-2} $
- 组合数平方求和式:$\sum\limits_{r=0} ^n \binom{n}{r}^2 = \binom{2n}{n}$
接下来是一个关于组合数的简单方法——隔板法。
- 求$x_1 + x_2 +...+x_n = m $正整数解的个数。
- 考虑到有$m$个小球被$n-1$块板割成不为空的$n$份的方案数。
- 由于有$m-1$个空,相当于$m-1$个空里插入$n-1$个隔板的方案数。
- 即$\binom{m-1}{n-1}$
- 求$x_1 + x_2 +...+x_n = m $非负整数解的个数。
- 考虑强制每一个$x_i$初始值为$1$ ,然后再使用上述的正整数解的隔板法。
- 转化为$m+n$个小球被$n-1$块板割成不为空的$n$份的方案数。
- 即$\binom{n+m-1}{n-1}$
接下来是一个非常经典的问题:错排列问题。
- 有$n$本书,按照一个原始排列存放,$f(n)$表示重新排列后每一本书都不在自己原来位置的排列总数。
- 考虑dp,使用$f(i)$表示答案。
- 将第$i$本书放在$[1,i-1]$的位置$j$上 。对$j$的位置分类:
- 如果$j$放在$i$的位置上,就是$i-2$个数的错排问题了。
- 如果$j$不在$i$的位置上,就是$i-1$个数的错排问题了。
- 于是答案就是: $f(i) = (i-1) (f(i-1)+f(i-2))$
对于排列数和圆排列,我们可以采用容斥原理,去除一些重复的情况。
- 排列计算公式:$P_{n}^{r} = \frac{n!}{(n-r)!} = n \times (n-1) \times ... \times (n-r+2) \times (n-r+1) $
- 圆排列计算公式:$CP_{n}^{r} = \frac{P_{n}^{r}}{r} = P_{n}^{r-1}$
- 多重集排列计算公式:$\frac{n!}{n_1! n_2! ... n_k!}$,其中$n = n_1 + n_2 +...+n_k$
- 多重集组合计算公式:含无限重复$k$种元素的集合$S$中选择$k$个元素的组合,等价于选$k$次球,每次都可以选$n$种球的方案数.
- 同时等价于$k$个相同的球放到$n$个不同的盒子里,盒子可以为空.
- 就成为了隔板法的经典题。
接下来是康拓展开和康拓逆展开:
设$Per(A) = a$ 表示排列A在所有排列中的字典序排名$a$,$acrPer(a) = A$ 表示 字典序排名$a$ 对应的排列A
显然两个函数为互逆的函数变幻。
- 求$Per(A) = a$ 康拓展开
- 设$a_i$ 表示从右往左数第$i$个数右边小于它的数的个数。
- 那么$a = \sum\limits_{i=1}^n a_i \times (i-1)! $
- 则$a$ 即为所求。
- 求$acrPer(a) = A$ 康拓逆展开
- 先将A减去1,表示排列从0开始编号(而事实上排列是以1开始编号),
- 按照进制转换的方法,按照阶乘求余数,从高到低确定每一位。
- 对于第$i$ 位上的数,A/(i-1)! 整除的商表示左边有多少个比它小,而余数表示下一次迭代的A值
- 所以第$i$ 位上的数是 A/(i-1)! 整除的商 + 1
依靠于组合数几个递推:
- 第一类斯特林数:$\begin{bmatrix} n\\ m \end{bmatrix}$ 表示$n$个元素进行$m$个非空的圆排列个数。
- 显然,第$n$个元素可以依靠之前已经完成的圆排列,也可以新成立一个排列。
- 所以递推式子是:$\begin{bmatrix} n\\ m \end{bmatrix} = (n-1) \begin{bmatrix} n-1\\ m \end{bmatrix} + \begin{bmatrix} n-1\\ m-1 \end{bmatrix}$
- 边界条件是 : $\begin{bmatrix} 0\\ m \end{bmatrix} = 0 , \begin{bmatrix} m\\ m \end{bmatrix} = 1 $
- 第二类斯特林数:$\begin{Bmatrix} n\\m \end{Bmatrix}$ 表示$n$个元素划分成$m$个集合的方案数。
- 显然,第$n$个元素可以依靠之前完成的集合,也可以成立一个新的集合。
- 所以递推式子是:$\begin{Bmatrix} n\\m \end{Bmatrix} = \begin{Bmatrix} n-1\\m-1 \end{Bmatrix} + m \begin{Bmatrix} n-1\\m \end{Bmatrix}$
- 边界条件是$\begin{Bmatrix} 1\\m \end{Bmatrix} = \begin{Bmatrix} m\\m \end{Bmatrix} = 1 $
接下来是一个非常常用的数列,卡特兰数列。
$Catalan(n) = \frac{\binom{2n}{n}}{n+1}$
基于一个递推的通项: $Catalan(n) = \sum\limits_{i=0} ^ {n-1} Catalan(i) \times Catalan(n-i-1) $
有下列应用:
- $n$对括号的合法匹配的方案数(+1/-1 法)
- $n$个节点二叉树的形态数
- $n$个数入栈后出栈的排列总数
-
对凸$n+2$边形进行不同的三角形分割的方案数,分割点不能交叉,可以
在端点处相相交
简单例题
Problem - A 不容易系列之(4)——考新郎
假设一共有不同的$N$对新婚夫妇,其中有$M$个新郎找错了新娘,求发生这种情况一共有多少种可能。
对于100%的数据 $n,m \leq 20$
Sol : 首先,从$N$对新婚夫妇中其中$N-M$对都是正确找到了,
也就是在$M$对新婚夫妇之间的错排问题,而这$M$对是在$N$对之中组合出来的,
最后答案就是$\binom{N}{M} \times f(M)$ 其中$f(M)$表示错排数目。
# include <bits/stdc++.h> # define int long long using namespace std; const int N=21; int f[N]; int C(int n,int m) { int s=1; for (int i=1;i<=n;i++) s*=i; for (int i=1;i<=m;i++) s/=i; for (int i=1;i<=n-m;i++) s/=i; return s; } signed main() { f[1]=0; f[2]=1; for (int i=3;i<=20;i++) f[i]=(i-1)*(f[i-1]+f[i-2]); int T; scanf("%lld",&T); while (T--) { int n,m; scanf("%lld%lld",&n,&m); printf("%lld\n",C(n,m)*f[m]); } return 0; }
Problem - B Shaass and Lights
有$n$个路灯,初始情况下已经有$m$个路灯被点亮,接下去每次点灯必须在已经点的灯相邻位置选择一个暗的灯点亮。
问有多少种点亮剩余所有灯方案% 1e9+7。
对于100%的数据$ n,m \leq 1000 $
Sol : 按照点亮的灯对整个序列进行划分块,每一个块当中都是暗的连续的灯。
每一个非含1,n的块中点灯的顺序,可以从左边或者右边选取,由于最后一个灯只有一种可能所有总可能性是$2^{len-1}$
其中$len$表示块的长度。
但是,对于每个块同时也可以按照不同的顺序进行选取。这应该是一个组合数的可能性。
令ret表示剩余灯的个数 ,对于第i个块当中的组合数可能性就是$\binom{ret}{len} $,然后令$ret -= len$即可。
对于第一个和最后一个(含有1或者n的块)只能从后往前遍历,所以2的幂次的可能性就是1,需要特殊判断。
预处理组合数之后, 复杂度应该是$O(n^2)$.
# include <bits/stdc++.h> # define int long long using namespace std; const int mo=1e9+7; const int N=1e3+10; int n,m,cnt,s[N]; bool b[N]; void init(){s[0]=1; for (int i=1;i<=1000;i++) s[i]=s[i-1]*i%mo;} int Pow(int x,int n,int p){int ans=1; while (n) { if (n&1) ans=ans*x%mo; x=x*x%mo; n>>=1;} return ans%mo;} int inv(int x){return Pow(x,mo-2,mo);} int C(int n,int m){return s[n]*inv(s[m])%mo*inv(s[n-m])%mo;} signed main() { init(); scanf("%lld%lld",&n,&m); memset(b,false,sizeof(b)); for (int i=1;i<=m;i++) {int t; scanf("%lld",&t); b[t]=1; } for (int i=1;i<=n;i++) { if (b[i]) continue; int j=i; while (j<=n&&!b[j]) j++; cnt+=j-i; i=j-1; } int ans=1; for (int i=1;i<=n;i++){ if (b[i]) continue; int j=i; while (j<=n&&!b[j]) j++; if (i==1) ans=ans*C(cnt,j-i)%mo; else if (j-1==n) ans=ans*C(cnt,j-i)%mo; else ans=ans*C(cnt,j-i)%mo*Pow(2,j-i-1,mo)%mo; cnt-=j-i; i=j-1; } printf("%lld\n",ans); return 0; }
Problem - C Count the Buildings
有$n$只身高为$[1,n]$牛排队,身高高的牛可以挡住身高矮的牛。
从前往后看能看到$F$只牛,从后往前看能看到$B$只牛。
求出有多少种排队方法 % 1e9+7,能够达到上述效果。
对于100%的数据$n ,B,F\leq 2000$
Sol : 显然从前面看和从后面看都能看到身高较高的,从n-1只牛里面找出F+B-2个轮换然后挑出较大的来被看,其中F-1组轮换放在左侧,B-1组轮换放在右侧。事实上,这样的方案的前后顺序是固定的,左侧的F-1组轮换能且只能有一种方法排除顺序使得恰好被看F-1只牛(不算最大的),右侧的同理。
从$n$个数里选择$m$个轮换,需要使用第一类斯特林数求解,而放左侧的$F-1$由于是任意的,所以直接组合数计算即可。
答案是$ \begin{bmatrix}n-1\\ F+B-2 \end{bmatrix} \binom{F+B-2}{F-1} $
预处理第二类斯特林数后复杂度是 $O(n^2)$
# include <bits/stdc++.h> # define int long long using namespace std; const int N=2e3+10,mo=1e9+7; int c[N][N],s[N][N]; signed main() { c[0][0]=1; for (int i=1;i<=2000;i++) { c[i][0]=c[i][i]=1; for (int j=1;j<i;j++) c[i][j]=(c[i-1][j-1]+c[i-1][j])%mo; } for (int i=1;i<=2000;i++) { s[i][i]=1; s[i][0]=0; for (int j=1;j<i;j++) s[i][j]=(s[i-1][j-1]+(i-1)*s[i-1][j]%mo)%mo; } int T; scanf("%lld",&T); while (T--) { int n,f,b; scanf("%lld%lld%lld",&n,&f,&b); int ans; if (f+b-2<N) ans=c[f+b-2][f-1]*s[n-1][f+b-2]%mo; else ans=0; printf("%lld\n",ans); } return 0; }
Problem -D Examining the Rooms
每个房间钥匙以某一种排列放在$n$个房间里,每一次可以暴力破拆随机一个房间找出该房间的钥匙,然后进行打开房门-获取钥匙-打开房门...的操作。直到无法再打开新的房门,而进行新的一轮破拆。你的破拆次数最多是$K$次并且$1$号房间的房门不能被破拆,求出你能够成功打开所有门的概率。
共有$T$组数据 ,对于100%的数据$1 \leq k\leq n \leq 20 ,T \leq 100$
Sol:需要求出$n$把钥匙分成1,2,...k 个非空圆排列的数目$s[n][i] \ (1 \leq i \leq k)$。
还需要考虑第$1$个元素必须要独自成为一个圆排列,所以我们需要把所有合法的答案累加。
即累加$s[n][i] \ (1 \leq i \leq k) - s[n-1][i-1] $ 其中减去的$s[n][i-1]$指的是n个元素形成$i-1$一个环的情况数,此时由于第$1$号房门被暴力破拆于是不合法,所有要减去。
由于排列数是$n!$所以概率就是$\frac{\sum\limits_{i=1}^k s[n][i] - s[n][i-1] }{n!}$
# include <bits/stdc++.h> # define int long long using namespace std; const int N=21; int s[N][N]; signed main() { for (int i=1;i<=20;i++) { s[i][0]=0; s[i][i]=1; for (int j=1;j<i;j++) s[i][j]=s[i-1][j-1]+(i-1)*s[i-1][j]; } int T; scanf("%lld",&T); while (T--) { int n,k; scanf("%lld%lld",&n,&k); int ret=0; for (int i=1;i<=k;i++) ret+=s[n][i]-s[n-1][i-1]; int s=1; for (int i=2;i<=n;i++) s*=i; double ans=1.0*ret/(1.0*s); printf("%.4lf\n",ans); } return 0; }
Problem - E Brackets
有长度为$n$的括号序列,给出前面一部分,前缀和前面部分的括号序列的数量%1e9+7
对于100%的数据满足$n \leq 10^6$
Sol: 对于一个合法的括号序列是满足在任意时刻(数目大于等于)数目并且最终左括号数目和右括号数目相等。
非常显然,当$n$为奇数的时候答案必然是$0$ ,在前缀括号序列在某一时刻若左括号数目小于右括号数目,答案也必然是$0$
括号序列其实和网格图记录方案非常相似,可以转化为从(1,1)出发,每读到一个(就往左走一格,每读到一个)就往上走一格,最终走到(n/2,n/2)处
还有一个限制:不能走到直线y = x 的上方(而在线上可以,满足括号序列合法性)。
并且由于前缀已经给定所以出发点并不是(1,1),而是(p,q),显然可以计算出。
为了走到(n/2,n/2)若不考虑限制,则方案数便是$\binom{n/2-p+n/2-q}{n/2-q}$
为了满足上述限制,由于可能是通过$y = x$上方而到达的最终目标,我们把这些不合法方案减去。
由于坐标都是整点,考虑直线$y = x + 1$及以上不能经过,目标点$(n/2,n/2)$对称过去就是$(n/2-1,n/2+1)$
所有经过$y = x+1$及上方的路线都等价于,从$(p,q)$出发走到$(n/2-1,n/2+1)$的方案,于是我们把这些贡献减去,就是答案。
最后答案就是$\binom{n/2-p+n/2-q}{n/2-q} - \binom{n/2-p+n/2-q}{n/2-q + 1} $
# include <bits/stdc++.h> # define int long long using namespace std; const int N=1e6+10,mo=1e9+7; int s[N],inv[N],n; char th[N]; void init() { s[0]=1; for (int i=1;i<=1000000;i++) s[i]=s[i-1]*i%mo;; inv[0]=inv[1]=1;for (int i=2;i<=1000000;i++) inv[i]=(mo-mo/i)%mo*inv[mo%i]%mo; for (int i=2;i<=1000000;i++) inv[i]=inv[i-1]*inv[i]%mo; } int C(int n,int m){return s[n]*inv[m]%mo*inv[n-m]%mo;} signed main() { init(); while (~scanf("%lld",&n)) { scanf("%s",th); int a=0,b=0,ans; int len=strlen(th); if (n&1) { puts("0"); goto End;} for (int i=0;i<len;i++) { if (th[i]=='(') a++; else b++; if (a<b) { puts("0"); goto End;} } if (n/2-a<0 || n/2-b<0) { puts("0"); goto End;} ans=((C(n/2-a+n/2-b,n/2-b)-C(n/2-a+n/2-b,n/2-b+1))%mo+mo)%mo; printf("%lld\n",ans); End:; } return 0; }
Problem - F Saving Beans
把$1 ... m$个相同的物品放到$n$个不同箱子里(允许空着不放)共有多少种方法,输出%p的答案。
共有$T$组数据,对于100%的数据$1 \leq n,m \leq 10^9 , 1 \leq p \leq 10^5$
Sol:考虑把$k$个相同$n$个不同箱子里(允许空着不放)共有多少种方法。
等价于求$x_1 + x_2 + ... + x_k = n$的非负整数解个数,就是$\binom{n+k-1}{k-1}$
所以答案对于$k \in [1,m]$ 求和, 等价于求$\sum\limits _{k=1} ^ {m}\binom{n+k-1}{k-1} = \binom{n}{0} + \binom{n+1}{1} + ... + \binom{n+m-1}{m-1} $
第一项$ \binom{n}{0} = \binom{n+1}{0} $将其替换。
由组合数递推公式$\binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r}$ 可将相邻两项分别合并。
得$ \binom{n+1}{0} + \binom{n+1}{1} + ... + \binom{n+m-1}{m-1} = \binom{n+m}{m}$
直接套用lucas定理即可。
# include <bits/stdc++.h> # define int long long using namespace std; const int N=2e5+10; int s[N],inv[N],n,m,p; void init(int len) { s[0]=inv[0]=inv[1]=1; for (int i=1;i<=len;i++) s[i]=s[i-1]*i%p; for (int i=2;i<=len;i++) inv[i]=(p-p/i)%p*inv[p%i]%p; for (int i=2;i<=len;i++) inv[i]=inv[i-1]*inv[i]%p; } int lucas(int n,int m,int p) { if (m>n) return 0; if (n<p&&m<p) return s[n]*inv[n-m]%p*inv[m]%p; return lucas(n%p,m%p,p)*lucas(n/p,m/p,p)%p; } signed main() { int T;scanf("%lld",&T); while (T--) { scanf("%lld%lld%lld",&n,&m,&p); init(n+m); printf("%lld\n",lucas(n+m,m,p)); } return 0; }