跳转至

好东西

组合数学入门—TwelveFold Way - wyxdrqcccc - 博客园

组合数

介绍组合数的4种不同的方法。

董晓算法的个人空间-董晓算法个人主页-哔哩哔哩视频

组合数的求法

递推法求组合数

image.png

递推初始值

image.png

\(j=0\)\(c(i,j)=1\).

每个数由其上方和左上方的数相加得来(杨辉三角

void init(){
    c[0][0]=1;//从0开始!
    for(int i=1;i<=n;i++){
        c[i][0]=1;
        for(int j=1;j<=n;j++){
            c[i][j]=c[i-1][j]+c[i-1][j-1]%MOD;
        }
    }
//  for(int i=0;i<=10;i++){
//      for(int j=0;j<=i;j++){
//          cerr<<c[i][j]<<' ';
//      }
//      cerr<<endl;
//  }
}

快速幂+倒推求组合数

\(C_{n}^{m}\mod(10^9+7)\)的值。

考察题目要求

考虑用\(C_{n}^{m}=\frac{n!}{(n-m)!m!}\)直接计算。 开两个数组分别存模意义下的阶乘和阶乘的逆元, 用f[x]存x!(mod p)的值, 用g[x]存\((x!)^{-1}(\bmod~~ p)\)的值。

复习乘法逆元相关知识,理解g[x]的含义。 因为p是质数且n,m都小于p,即n,m与p互质, 所以根据费马小定理\(a\cdot a^{p-2}=1\pmod p\), 可以用快速幂求逆元。

复习**乘法逆元**相关知识,理解\(g[x]\)的含义

因为\(p\)是质数且\(n,m\)都小于\(p\),即\(n,m\)\(p\)互质,所以根据费马小定理\(a\cdot a^{n-2}\equiv1\pmod p\),可以用快速幂求逆元。

复习**费马小定理**,得:

\(p\)为质数时,\(a^{p-2}\)\(a\)在模\(p\)下的逆元

因此我们使用快速幂求\(a^{p-2}\)(也就是求\((x!)^{-1}\),当然,\(x!\)需要递推求出,因此我们可以把\(1!\sim n!\)的逆元全部预处理出来)

void comb_init(){
    fac[0]=1;
    for (int i=1;i<=n;++i) fac[i]=fac[i-1]*i%MOD;
    inv[n]=ksm(fac[n],MOD-2,MOD);
    for(int i=n;i>=1;i--)inv[i-1]=inv[i]*i%MOD;//i!的inv 
}

查询

int C(int n,int m){
  if(n<0||m<0)return 0;
    if(n<m)return 0;
    return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}

注意,这种方法只能预处理[1,p-1]内的组合数。如果越界,则结果均为0,因为出现了p的倍数。

Lucas定理求组合数

卢卡斯(Lucas)定理 $ C_{ n }^{ m }\equiv C_{ n/p }^{ m/p }.C_{ n\mod p }^{ m\mod p }( mod p)$, 其中p为质数。

方法:

递推+快速幂(即下方标识的“可以直接求解”)

n % p 和 m % p 一定是小于 p 的数,可以直接求解,

C(n/p,m/p) 可以继续用 Lucas 定理解求。

边界条件:当 m = 0 时,返回 1。

证明

引理1:\(C_{p}^{x}\equiv 0\:(mod\: p),\:\: 0<x<p\) 因 $ C_{p}^{x}= \frac{p!}{x!(p-x)!}= \frac{p(p-1)!}{x(x-1)!(p-x)!}= \frac{p}{x}C_{p-1}^{x-1}$ 故 \(C_{p}^{x}\equiv p\cdot inv(x)C_{p-1}^{x-1}\equiv 0\:(mod\: p)\)

\(inv(x)\)就是\(x\)的乘法逆元

易证。

引理2:\((1+x)^p≡1+x^p(mod p)\) 由二项式定理\((1+x)^p=\sum_{i=0}^{p}C_{p}^{i}x^i\), 由引理1知,只剩i=0,p两项,得证。

由引理2得,当\(i=1\sim p-1\)时,都满足引理1,求和时全部约掉,只剩下\(i=0\)\(i=p\)两种特殊情况,分别代入,得到两个单项式为\(1\)\(x^p\)

易证。

补充

请学习**二项式定理!**

证明: 令 \(n = ap + b, m = cp + d\) \((1+x)^n\equiv \sum_{i=0}^{n} C_n^i x^i \pmod{p}\) ---(1) \((1+x)^n\equiv (1+x)^{ap+b}\) \(\equiv ((1+x)^p)^a \cdot (1+x)^b\) \(\equiv (1+x^p)^a \cdot (1+x)^b\) \(=\sum_{i=0}^{a}C_a^ix^{ip}\cdot\sum_{j=0}^{b}C_b^jx^j\pmod{p}\)---(2)

(1)式中\(x^m\) 的系数为 \(C_n^m\)

(2)式中\(x^m=x^{cp}\cdot x^d\) 的系数为 \(C_a^cC_b^d\)

\(C_n^m\equiv C_a^cC_b^d\pmod{p}\)

\(C_n^m\equiv C_{n/p}^{m/p}C_{n\mod p}^{m\mod p}\pmod{p}\) 证毕。

在(2)中使用了**引理2**

使用的方法为**比较系数法**

/*////////ACACACACACACAC///////////
       . Code  by  Ntsc .
       . Earn knowledge .
/*////////ACACACACACACAC///////////

#include<bits/stdc++.h>
#define int long long
#define db double
#define rtn return
using namespace std;

const int N=2e5+5;
const int M=1e5;
const int Mod=1e5;
const int INF=1e5;

int n,m,p,q,T,fac[N],ans;

int ksm(int c,int k,int p) {
//  p=p;
    if(!c)return 0;
    int res=1;
    while(k){
        if(k&1)res=(res*c)%p;
        c=(c*c)%p;k>>=1;
    }
    return res;
}

void comb_init(){
    fac[0]=1;
    for (int i=1;i<=p;++i) fac[i]=fac[i-1]*i%p;
}

int inv(int x){

    return ksm(x,p-2,p);//请保证p为质数
} 

int C(int n,int m){
    if(n-m<0)return 0;
    //n==m时不能rt 0 
    return fac[n]*inv(fac[m])%p*inv(fac[n-m])%p;
}


int lucas(int n,int m){
    if(n==p)return 0;
    if(m==0)return 1;
    return lucas(n/p,m/p)*C(n%p,m%p)%p;
}

signed main(){

    cin>>T;
    while(T--){
        cin>>n>>m>>p;comb_init();
        cout<<lucas(n+m,n)<<endl;
    }
    return 0;
}

高精度&线性筛求组合数

背景

组合数的增长速度是非常快的,必须用高精度存取。\(C_{100}^{50}\)是30位数,\(C_{1000}^{500}\)是300位数,\(C_{10000}^{5000}\)是3009位数。

小技巧

n!中p的个数:\(s=\frac{n}{p} +\frac{n}{p^2}+\frac{n}{p^3}+…\)

原理

  1. 筛质数,把1~n之间的质数筛出来

  2. 枚举所有质数 a.计算\(C_n^m=\frac{n!}{m!(n-m)!}\)中质数p的个数 b.利用高精度计算\(C[i]\times p\)

我们将\(C_n^m\)分解质因数,然后逐步累乘进\(C_n^m\)

乍一看**第2步**可能看不到,这里就介绍一下:

\(c[i]\)的初始值为\(1\),后面将\(C_n^m\)的质因数逐步求出来(从小到大),即遍历质数表从小到大,求出质数\(p_i\)的个数\(n\),然后指向\(n\)次将\(c[i]*=p_i\)

代码

/*////////ACACACACACACAC///////////
       . Code by Ntsc .
       . Love by  .
/*////////ACACACACACACAC///////////

#include<bits/stdc++.h>
#define ll long long
#define db double
#define rtn return
using namespace std;

const int N=1e4;
const int M=1e5;
const int Mod=1e5;
const int INF=1e5;

int n,m,ans,cnt,p,l;
int prim[N],notp[N],b[N];
int get(int n,int p){//计算n!中质数p的个数,根据公式 
    int s=0;
    while(n)s+=n/p,n/=p;//point1 
    return s;
}
int getc(int n,int m,int p){//计算c_n^m中质数p的个数 
    return get(n,p)-get(m,p)-get(n-m,p);//分子中p的个数-分母中p的个数 
}
void mul(int c[],int p,int &l){//高精度×单精度 
    int t=0;
    for(int i=0;i<l;i++){
        t+=c[i]*p;
        c[i]=t%10;
        t/=10;
    }
    while(t){
        l++;
        c[l]=t%10;
        t/=10;
    }
}
void init(){//初始化筛质数表 
    for(int i=2;i<=N;i++){
        if(!notp[i])prim[++cnt]=i;
        for(int j=i;j<N;j+=i)notp[j]=1;
    }
}
signed main(){
    init();

    cin>>n>>m; 
    int c[N],l=1;c[0]=1;
    for(int i=1;i<=cnt;i++){//遍历每一个质数 
        int p=prim[i];
        int s=getc(n,m,p);//求出c_n^m中质数p的个数
        while(s--)mul(c,p,l) ;//将c乘上p,乘n次 
    } cout<<"OK"<<endl;
    for(int i=l;i;i--)cout<<c[i];
    return 0;
}
/*////////ACACACACACACAC///////////
       . Code by Ntsc .
       . Love by  .
/*////////ACACACACACACAC///////////

#include<bits/stdc++.h>
#define ll long long
#define db double
#define rtn return
using namespace std;

const int N=1e4+5;
const int M=1e5;
const int Mod=1e5;
const int INF=1e5;

int n,m,ans,cnt,p,l;
int prim[M],notp[M],c[M];
int get(int n,int p){//计算n!中质数p的个数,根据公式 
    int s=0;
    while(n)s+=n/p,n/=p;//point1 
    return s;
}
int getc(int n,int m,int p){//计算c_n^m中质数p的个数 
    return get(n,p)-get(m,p)-get(n-m,p);//分子中p的个数-分母中p的个数 
}
void mul(int c[],int p){//高精度×单精度 
    int t=0;
    for(int i=0;i<l;i++){
        t+=c[i]*p;
        c[i]=t%10;
        t/=10;
    }
    while(t){
        l++;
        c[l]=t%10;
        t/=10;
    }
}
void init(){//初始化筛质数表 
    for(int i=2;i<=N;i++){
        if(!notp[i])prim[++cnt]=i;
        for(int j=i;j<=N;j+=i)notp[j]=1;
    }
}
signed main(){
    init();

    cin>>n>>m; 
    l=1;c[0]=1;
    for(int i=1;i<=cnt;i++){//遍历每一个质数 
        int p=prim[i];
        int s=getc(n,m,p);//求出c_n^m中质数p的个数
        while(s--)mul(c,p) ;//将c乘上p,乘n次 
    } 
//  cout<<"OK"<<endl;
    for(int i=l-1;i+1;i--)cout<<c[i];
    cout<<endl<<"cnt="<<cnt<<" l="<<l<<endl;
    for(int i=1;i<=cnt;i++)cout<<prim[i]<<' '; 
    return 0;
}

point1:

我们要懂得将

\(\frac{n}{p^i}\)

转化为

\(\frac{\frac{n}{p^{i-1}}}{p}\)

从而递推,不需要计算\(p^i\)

代码

int C(int u, int v) { return fac[u] * inv[v] % P * inv[u - v] % P; }

signed main() {
    fac[0] = 1;
    for (int i = 1; i <= N - 2; i++) fac[i] = (fac[i - 1] * i) % P;

    inv[1] = 1, inv[0] = 1;  // att!当n=m时特判,组合为1
    for (int i = 2; i <= N - 2; i++) inv[i] = ((P - P / i) * inv[P % i]) % P;
    for (int i = 1; i <= N - 2; i++) inv[i] = (inv[i] * inv[i - 1]) % P;
  //...
}

消除法

当n很大,m很小时,数组可能无法存储n的逆元,此时我们回到组合数的公式,将能消除的都消除,剩下的只有2m个项目了。

组合数学常用模型

错排列

错排列,又称作德利克雷排列,是指一个排列中没有任何一个元素在原来位置上的排列方式。例如,对于3个元素的错排列有2种:[2, 3, 1]和[3, 1, 2]。

那么我们怎么样求出长度为n的错排列的个数呢?我们考虑如下推导:

image.png

我们考虑这里有一个n\times n的矩阵,我们要在上面选n个点,其中点(x,y)表示位置x上的数为y。因此主对角线上的点是不可选的。

我们用D_n表示长度为n的错排列的方案数,同时也是n\times n的矩阵的填法的方案数

于是目前我们有(n-1)种填法。

image.png

假设我们天在橙色处,那么淡橙色区域后面就都不可填了。问题递归到下一层

image.png

那么这个子问题的方案是多少呢?我们继续考虑这里选哪个位置

image.png

如果选绿色的呢?我们发现就是D_{n-2}了。如果选其他的(蓝色)呢?我们发现可以视为第一个位置不可选,那么就是D_{n-1}了。

综上所述,有\(D_n=(n-1)(D_{n-1}+D_{n-2})\)

圆排列

圆排列是指在一个圆周上进行的排列,由于圆的旋转对称性,不同的旋转被视为相同的排列。例如,圆周上3个点的圆排列只有2种,因为每种排列可以通过旋转得到另外两种。

鸽巢原理

鸽巢原理,又称狄利克雷抽屉原理,简单来说就是如果有n个鸽子和m个鸽巢,且n > m,那么至少有一个鸽巢里有多于一个鸽子。例如,如果有10个苹果放入9个盒子里,至少有一个盒子里有不止一个苹果。

容斥原理

容斥原理是一个计数原理,用于计算多个集合的并集的大小。其基本思想是:先加上每个集合的大小,然后减去每对交集的大小,再加上三个集合交集的大小,以此类推。例如,计算三个集合A、B、C的并集大小,公式为:|A∪B∪C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C|。

卡特兰数

卡特兰数 - OI Wiki

image.png

卡特兰数的一般形式为:

$C_n = \frac{1}{n+1} \binom{2n}{n} = \frac{(2n)!}{(n+1)!n!} $

对于第 ( n ) 个卡特兰数 ( C_n ),其前几个数值如下:

  • ( C_0 = 1 )

  • ( C_1 = 1 )

  • ( C_2 = 2 )

  • ( C_3 = 5 )

  • ( C_4 = 14 )

  • ( C_5 = 42 )

  • ( C_6 = 132 )

  • ( C_7 = 429 )

  • ...

    卡特兰数在许多组合问题中自然出现,以下是一些著名的例子:

  • 括号匹配问题:( C_n ) 表示用 ( n ) 对括号能够形成的不同括号序列的数量。

  • 二叉树问题:有 ( n ) 个内部节点(即非叶节点)的二叉树的不同形态数量是 ( C_{n-1} )。

  • 多边形划分问题:将一个凸多边形用 ( n-3 ) 条对角线划分成 ( n ) 个三角形的方法数是 ( C_{n-2} )。

  • 栈操作问题:给定 ( n ) 个“进栈”操作和 ( n ) 个“出栈”操作的所有合法序列的数量是 ( C_n )。 卡特兰数在算法、计算机科学、数学证明等多个领域都有着重要的应用。它们通常通过递推关系式来计算: $ C_{n+1} = \sum_{i=0}^{n} C_i C_{n-i} $ 或者通过直接使用上面的公式计算。

    c[1]=c[0]=1;
    for(int i=1;i<=n;i++)c[i]=c[i-1]*(4*i-2)/(i+1);

www.luogu.com.cn

image.png

例题

www.luogu.com.cn

隔板法

例题 #1

image.png

思路

注意了,你潜意识里的隔板法是错误的!

看看这道题,我们也许回想把樱花树当成隔板插进去,那我们就有n-m+1个位置可以放置隔板,那么我们就是要在n-m+1个位置中选择m个位置。那么不就是组合数C(n-m+1,m)吗?然后我们在考虑樱花的编号,那么就再乘上一个A(m,m)。这是可行的。

再看看另外一种做法**:**

我们先满足每两个花之间必须要有一个空位,总共n-m个空位,那么我们就使用了m-1个空位,剩下还有n-2m+1个空位,我们把它分配给m+1个位置。隔板法后就是\(x_1+\dots+x_{m+1}=n-m+2\)

注意了,这里我们进行隔板法,是m个隔板放入n-m+1个位置!!!!而不是m+1个隔板放入n-m+2个位置。那么现在就可解了。

多重集合

多重集的排列数

假设你有 \(k\) 种不同的球,每个球的个数分别为 \(n_1, n_2,..., n_k\) (其中 \(\sum_{i=1}^{k}n_i = n\) )。这 \(n\) 个球的全排列数即为多重集合的排列数。多重集合的排列数通常被称为多重组合数。我们可以使用多重组合数的符号来表示上述公式: \(\left(\begin{array}{c}n\\n_{1}, n_{2}, \ldots, n_{k}\end{array}\right)=\frac{n !}{\prod_{i=1}^{k} n_{i} !}\)

多重集的组合数

\(S=\{n_{1}\cdot a_{1},n_{2}\cdot a_{2},\cdots,n_{k}\cdot a_{k}\}\)表示由\(n_{1}\)\(a_{1}\)\(n_{2}\)\(a_{2}\),…,\(n_{k}\)\(a_{k}\)组成的多重集。那么对于整数\(r(r< n_{i},\forall i\in[1,k])\),从\(S\)中选择\(r\)个元素组成一个多重集的方案数就是多重集的组合数。这个问题等价于\(x_{1}+x_{2}+\cdots+x_{k}=r\)的非负整数解的数目,可以用插板法解决,答案为\(\binom{r+k-1}{k-1}\)

二项式定理

及将\((a+b)^p\)拆分后各个项目的系数。第i项形如\(ka^{i-1}b^{n-i+1}\)

那么怎么样计算这个k呢?我们发现将\((a+b)^p\)展开为p个括号相乘后,我们要在每一个括号中选择a或者b。这里我们要选择i-1个a,所以方案就是在p个不同的球中选i-1个的方案数,就是C(p,i-1)。