Loading

【笔记】组合数学

组合数

\(\dbinom{n}{m}\) 表示从 \(n\) 个互不相同的小球中选择 \(m\) 个的方案数。有公式 \(\dbinom{n}{m}=\frac{n!}{m!(n-m)!}\)。特别地,我们认为 \(m=0\) 时存在一种方案,因此 \(0!=1\)

除暴力计算外,组合数有两种常用的计算方法。

\(1\):逆元法。在有取模时可以使用。

我们可以 \(O(n)\) 预处理所有 \(n,m\) 值域内的阶乘以及他们的逆元,然后 \(O(1)\) 套公式计算。

\(2\):递推法。在 \(n,m\) 较小时可以使用。

考虑组合意义,在 \(n\) 个物品中选 \(m\) 个可分为两种情况。

  1. 在前 \(n-1\) 个小球中选择 \(m\) 个,方案数 \(\dbinom{n-1}{m}\)
  2. 在前 \(n-1\) 个小球中选择 \(m-1\) 个,并选择新增的第 \(n\) 个小球,方案数 \(\dbinom{n-1}{m-1}\)

两种情况相加即为 \(\dbinom{n}{m}\) 的答案。

以及组合恒等式:\(\dbinom{n}{m}=\dbinom{n}{n-m}\),选要选的小球等价于选要剩下的小球。

P2822 [NOIP 2016 提高组] 组合数问题

考察一个组合数的计算。

首先注意到 \(n,m\) 很小,可以递推预处理出所有的 \(\dbinom{n}{m}\)。并且我们只关心是否整除 \(k\),边计算边取模边判断整除。

然后注意到题目的条件即是计算 \(n\) 作横坐标、\(m\) 作纵坐标,上存有此位置是否为 \(k\) 的倍数的矩阵的二维前缀和。

这个矩阵叫杨辉三角,我们后面还会提到。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2010;
int T,n,m,k;
int f[N][N];
int ans[N][N],sum[N][N];
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>T>>k;
    for(int i=0;i<N;i++) f[i][0]=1;
    for(int i=1;i<N;i++){
        for(int j=1;j<=i;j++){
            f[i][j]=f[i-1][j-1]+f[i-1][j];
            if(f[i][j]%k==0) ans[i][j]=1;
            f[i][j]%=k;
        }
    }
    for(int i=1;i<N;i++){
        for(int j=1;j<N;j++){
            sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+ans[i][j];
        }
    }
    while(T--){
        cin>>n>>m;
        cout<<sum[n][m]<<'\n';
    }
    return 0;
}

组合意义

这部分是转化问题的关键,即怎么把一些复杂的计数问题转为选小球。

插板法

考虑这样一个问题:\(n\)相同的小球,放入 \(m\) 个不同的桶,不允许空桶,求方案数。

这个问题等价于把球放在一排,划分成 \(m\) 个连续段,从左到右的第 \(i\) 段相当于第 \(i\) 个桶。两种方案不同仅当存在对应两段长度不同。

于是转化为对区间分割点考虑,我们可以在 \(n-1\) 个空隙中选择 \(m-1\) 空隙设为分割点,容易发现选择方案不同就存在对应区间长度不同,所以答案为 \(\dbinom{n-1}{m-1}\)

如果允许空桶呢?

我们可以增加若干个虚拟球,如果一个桶要求为空,那他可以选择若干个虚拟球放入。由于最多 \(m\) 个桶都为空,所以要增加 \(m\) 个虚拟球,做到与原问题一一对应。答案为 \(\dbinom{n+m-1}{m-1}\)

P5520 [yLOI2019] 青原樱

问题等价于 \(n\) 个球排成一排,从中选出 \(m\) 个不相邻球的方案数,再乘上 \(m\) 个球的全排列。

我们可以从组合意义的角度考虑。对于一个位置需要其相邻位置没有球,我们可以把它们绑定,去看选相邻两个位置的方案。当选完 \(m-1\) 个相邻两个位置后,最后一个球放在任意一个选过的两位置旁边,选过的两位置都可以把挨着的空出来,所以最后一个球只需要选一个位置。有 \(m-1\) 个位置要跟球绑定不能选,因此答案为 \(\dbinom{n-m+1}{m}\)

如果你觉得直接从组合意义思考过于困难,那么还有一种从代数层面入手的思路。我们考虑把位置划分为 \(m\) 个连续段,每个段的长度都 \(\ge2\),我们设这些段的长度为 \(x_1,x_2,\dots,x_m\),且 \(x_1+x_2+\dots+x_m=n\)。联想到小球装桶,每个桶非空即每个连续段长度 \(\ge1\)。注意到 \(x_1-1,x_2-1,\dots,x_m-1\ge1\),将这些作为新的连续段,又能得到 \(x_1-1+x_2-1+\dots+x_m-1=n-m\),相当于在一排 \(n-m\) 个位置中划分出 \(m\) 个非空连续段,运用插板法即可。

错排

再考虑这样一个问题:\(n\) 个不同的球对应 \(n\) 个不同的桶,每个桶放一个球,求所有球都不放在对应桶的方案数。

考虑已完成 \(n-1\) 个球的错排,怎么计算 \(n\) 个球的错排。

对于新增的第 \(n\) 个球进行考虑,我们需要在前 \(n-1\) 个位置里选一个位置 \(k\) 放这个球。

接下来分两种情况考虑。

  1. \(k\) 放在位置 \(n\),那么剩下 \(n-2\) 个元素对应关系不变,也就是 \(n-2\) 个元素错排的方案数;
  2. \(k\) 不放在位置 \(n\),相当于我们钦定 \(k\) 的对应位置是 \(n\),也就是 \(n-1\) 个元素错排的方案数。

则方案数 \(D_n\) 的递推式:\(D_n=(n-1)(D_{n-1}+D_{n-2})\)

对于这类问题还有一种别的计算方法,我们在后面会提到。

二项式定理与杨辉三角

完全平方公式:\((x+y)^2=x^2+2xy+y^2\)

完全立方公式:\((x+y)^3=x^3+3x^2y+3xy^2+y^3\)

注意到对于 \((x+y)^n\) 展开式的一项 \(x^ay^b(a+b=n)\),我们要从 \(n\) 个相乘的 \((x+y)\) 中选 \(a\) 个为 \(x\) 做贡献,选 \(b\) 个为 \(y\) 做贡献。选择的方案数即多项式的系数。

因此有一般的二项式定理:\((x+y)^n=\sum{\dbinom{n}{k}x^{(n-k)}y^k}\)。并可以推广到更多项式。

\(n=1,2,3,\dots\) 次的二项式系数从上往下排列,就得到了杨辉三角。

1
1 2 1
1 3 3 1
1 4 6 4 1
...

杨辉三角的第 \(i\) 行第 \(j\) 列存储的是 \(\dbinom{i}{j}\) 的值。

杨辉三角中一个数等于他左上和正上方数的和。这也印证了递推式 \(\dbinom{n}{m}=\dbinom{n-1}{m-1}+\dbinom{n-1}{m}\) 的正确性。

运用二项式定理可以将组合数转化为二项式的值,从而用快速幂等算法高效求解。

Codeforces 1332E Height All the Same

很有意思的思维题。

首先只有当每个格子的奇偶性都相同时,才能使用若干次操作 \(2\) 完成补齐。一次操作 \(1\) 可以改变相邻两个格子的奇偶性。合法条件即,能否通过若干次操作 \(1\) 使整个网格奇偶性相同。

然后发现进行过两次操作 \(1\) 的格子奇偶性是不变的。这能带来特别好的一个性质,就是如果我们要修改两个任意格子的奇偶性,只需要沿着这两个格子之间任意一条路径,每个格子都修改一次就可以了。这样,路径两端进行了一次修改,而中间都进行了两次修改,只有要修改的格子奇偶性变了。也就是说,是否合法与排列顺序无关,只与奇偶格子数量有关。

知道了选哪些格子为奇、哪些格子为偶后,每个格子可以在值域内的所有对应奇偶数里随便选。根据乘法原理可得答案为格子数个值域大小相乘。

接下来考虑怎么统计所有合法的格子奇偶分配情况。根据上面的推导我们已经得到了合法情况的充要条件,即至少有一种奇偶性的格子数量是偶数,这样他们可以两两配对,修改为另一种奇偶性。那么我们得按照总格子数的奇偶性分类讨论。我们设当前选 \(k\) 个奇数格,值域内有 \(x\) 个奇数,\(y\) 个偶数。

  1. \(n\times m\) 为奇数,我们如果钦定一种奇偶性的格子数是奇数,另一种格子数一定是偶数。任意一种奇偶分配方案都合法。方案为从所有格子里选 \(k\) 个的方案 \(\times\) 值域里选数的方案。所以答案为:

\[\sum_{k=1}^{n\times m}\dbinom{n\times m}{k}x^ky^{n\times m-k} \]

  1. \(n\times m\) 为偶数,两种奇偶性的格子都只能是偶数。所以答案为:

\[\sum_{k=1}^{\frac{n\times m}{2}}\dbinom{n\times m}{2k}x^{2k}y^{n\times m-2k} \]

对于第一种情况就是裸的二项式定理,直接计算 \((x+y)^{nm}\) 即可(其实就是每个位置随便填)。第二种情况,注意到 \((x-y)^{nm}\) 所有 \(k\) 为奇数的项都被乘上了 \(-1^k\)\(-1\),所以答案为 \(\frac{(x+y)^{nm}+(x-y)^{nm}}{2}\)。因为取模,你还要计算 \(2\) 在模 \(998244353\) 意义下的乘法逆元(\(499122177\))。

容斥原理

有时我们难以直接求出合法的方案数量,此时我们就需要用正难则反和容斥原理等间接求出。

一个最简单的容斥原理是,求三个集合的 \(|S_1\cup S_2\cup S_3|\)(为了方便均用绝对值表示 \(\operatorname{card}\))。答案为 \(|S_1|+|S_2|+|S_3|-|S_1\cap S_2|-|S_2\cap S_3|-|S_1\cap S_3|+|S_1\cap S_2\cap S_3|\)

也就是说如果我们知道了每个集合的大小,两两集合相交的大小,三三集合相交的大小……就能通过交叉加减求得 \(n\) 个集合相并。

更具体地,容斥原理的一般公式是:

\[|\bigcup_{i=1}^n S_i|=\sum_{k=1}^n(-1)^{k+1}\sum_{p_1<p_2<\dots<p_k}|S_{p_1}\cap S_{p_2}\cap\dots\cap S_{p_k}| \]

P1450 [HAOI2008] 硬币购物

容斥原理大典。

首先不考虑硬币数量限制,只需要跑一个完全背包。时间复杂度 \(O(nm)\),此题中 \(m=4\)。对于此题多组数据预处理 \(n=10^5\) 即可。

然后考虑怎么把不合法的情况容斥掉。对于一个硬币 \(d_i\),我们可以直接先钦定使用了 \(d_i+1\) 个,这时剩下的物品价值是 \(s-(d_i+1)\times c_i\),再用四种硬币买这个价值的东西的方案全都一定不合法。直接调用此前预处理过的完全背包即可。这样我们就求得了至少有 \(d_i\) 不合法(购买剩余价值物品时不能保证其他硬币数量合法)的方案数,记为 \(|S_i|\)

接着你发现所有不合法的情况就是 \(|S_1\cup S_2 \cup S_3\cup S_4|\),所以你要求出 \(|S_1\cap S_2|\) 等等两两集合的交,三三集合的交,以及四个集合的交。这些也都是好求的,对于 \(|S_i\cap S_j|\),去求购买 \(s-(d_i+1)\times c_i-(d_j+1)\times c_j\) 价值物品的方案数,以此类推。

最后我们暴力容斥,根据容斥公式我们要用所有单个集合大小之和,减去所有两两相交集合大小之和,加上所有三三相交集合大小之和,减去所有四四相交集合大小之和……如果有更多以此类推。

对于求解不合法情况的各种并,不难发现是一个枚举子集,我们可以用状压简化代码实现。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+10;
int T,s;
int c[5],d[5],f[N];
int ret[5];
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>c[1]>>c[2]>>c[3]>>c[4]>>T;
    f[0]=1;
    for(int i=1;i<=4;i++){
        for(int j=c[i];j<N;j++) f[j]+=f[j-c[i]];
    }
    while(T--){
        cin>>d[1]>>d[2]>>d[3]>>d[4]>>s;
        int ans=f[s];
        for(int i=1;i<=4;i++) ret[i]=0;
        for(int i=1;i<=15;i++){
            int t=i,sum=0,cnt=0,j=1;
            while(t){
                if(t&1) sum+=(d[j]+1)*c[j],cnt++;
                t/=2;
                j++;
            }
            if(s>=sum){
                int res=f[s-sum];
                ret[cnt]+=res;
            }
        }
        cout<<ans-ret[1]+ret[2]-ret[3]+ret[4]<<'\n';
    }
    return 0;
}
posted @ 2026-05-31 00:16  Seqfrel  阅读(25)  评论(0)    收藏  举报