【笔记】组合数学
组合数
\(\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\) 个可分为两种情况。
- 在前 \(n-1\) 个小球中选择 \(m\) 个,方案数 \(\dbinom{n-1}{m}\);
- 在前 \(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\) 放这个球。
接下来分两种情况考虑。
- \(k\) 放在位置 \(n\),那么剩下 \(n-2\) 个元素对应关系不变,也就是 \(n-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\) 个偶数。
- \(n\times m\) 为奇数,我们如果钦定一种奇偶性的格子数是奇数,另一种格子数一定是偶数。任意一种奇偶分配方案都合法。方案为从所有格子里选 \(k\) 个的方案 \(\times\) 值域里选数的方案。所以答案为:
- \(n\times m\) 为偶数,两种奇偶性的格子都只能是偶数。所以答案为:
对于第一种情况就是裸的二项式定理,直接计算 \((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\) 个集合相并。
更具体地,容斥原理的一般公式是:
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;
}

浙公网安备 33010602011771号