排列组合学习笔记
以下部分内容摘自OI Wiki
排列数
从 个数中选出 个数按照一定的顺序排列,用 表示。排列的计算公式如下:
。
组合数
从 个不同的元素中,选出 个元素组成一个集合,用 表示,也常用 表示。组合的计算公式如下:
。
特别地,当 时,。
插板法
插板法(Stars and bars)是用于求一类给相同元素分组的方案数的一种技巧,也可以用于求一类线性不定方程的解的组数。
正整数和的数目
问题一:现有 个 完全相同的元素,要求将其分为 组,保证每组至少有一个元素,一共有多少种分法?
这个问题的本质就是求 的正整数解的组数。
考虑在这些元素的 个空隙中插入 块板子,因为元素完全相同,那么答案就是 。
非负整数和的数目
问题二:在问题一的基础上,允许每组元素为空。
本质就是求 的非负整数解的组数。
考虑先借来 个元素,在 个空隙中插入 个空格。最后再把借来的元素从每组中删去,即可满足题意。答案为 。
不同下界整数和的数目
问题三:在问题二的基础上,要求第 组的元素数量至少为 ,满足 。
即求满足 , 的解的组数。
类比问题二,第 组先借来 个元素,令 ,那么就有 。得到新方程:
。
那么问题转为求这个方程的非负整数解的组数。答案为 。
不相邻的排列
问题四:从 中选 个不相邻的数,求方案数。
设第 个数与第 个数的间隔为 ,其中 表示第一个元素与 的间隔, 表示最后一个元素与 的间隔。于是 ,。得到方程:
。
令 ,。
那么就转化为了问题一。答案就是 。
二项式定理
、
组合数的性质:
将选出的集合对全集取补集,数值不变:
。
根据定义可以推出吸收恒等式:
。
用杨辉三角的表达式,可以推出:
。
取二项式定理中 的特殊情况,可以得到:
。
同理,取二项式定理中 的特殊情况,可以得到:
。
拆式子,感性理解一下可以得到范德蒙德卷积:
。
取 中 的特殊情况,可以得到:
。
通过对 所对应的多项式函数求导,可以得到:
。
在 中选出 个元素,可以考虑枚举最大的元素的值,那么就得到等式:
。
在 个元素中选取 个元素,再在 中选取 个元素,等价于先选 个元素,再在剩下的 个元素中选取 个元素:
。
而从杨辉三角上也不难发现:
。
其中 为斐波那契数列。
组合数问题
组询问,给定 和 ,对于所有的 有多少对 满足 。
,。
思路
注意到 的范围,同时 对于每一组询问相同。可以考虑 预处理出答案, 询问。
根据组合数在模 意义下的递推公式:
如果 在模 意义下为 ,那么显然就是一对满足题意的数对。只需要处理前缀和即可。
code:
#include<bits/stdc++.h>
using namespace std;
const int M=2e3+5;
int c[M][M],s[M][M],t,k,n,m;
int main()
{
cin>>t>>k;
for(int i=1;i<M;i++) c[i][0]=c[i][i]=1;
for(int i=1;i<M;i++)
for(int j=1;j<i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%k;
for(int i=1;i<M;i++)
{
for(int j=1;j<=i;j++)
{
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
if(c[i][j]==0) s[i][j]++;
}
s[i][i+1]=s[i][i];
}
while(t--)
{
cin>>n>>m;
if(n<m) m=n;
cout<<s[n][m]<<endl;
}
}
回忆京都
次询问,每次询问求:
,其中当的时候,钦定为
。
思路
可以直接预处理出 范围内的组合数,再用前缀和来统计答案。那么就是 预处理,单次询问的复杂度为 。
但我们也可以利用上面得到的等式 ,对原式进行变换:
。
那么也可以做到 。
code:
#include<cstdio>
using namespace std;
const int mod=19260817,N=1050;
int c[N][N],s[N][N],n,m,q;
int main()
{
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
if(!j) c[i][j]=1;
else c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
for(int i=1;i<N;i++) for(int j=1;j<N;j++) s[i][j]=(s[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1]+c[i][j]+mod)%mod;
scanf("%d",&q);
while(q--)
{
scanf("%d%d",&m,&n);printf("%d\n",s[n][m]);
}
return 0;
}
一个仇的复
你有 ( 为任意正整数)的矩形各无穷多个和一个 的网格,请求出恰好选择其中 个矩形(可以选择相同的矩形)不重不漏地铺满整个网格的方案数。矩形可以旋转。
,。
思路:
首先可以考虑原问题的简化版,用 个横着的长方形铺满 的方格的方案数。
考虑上面一行放置 个长方形,那么下面一行就放置 个长方形,利用插板法,可以得到方案数为:
。
根据等式 ,即范德蒙德卷积,原式可以简化为:
。
回到原问题,考虑用 个 的长木板将原来的大木板分割成 个 个小区间,再套用上面子问题的公式。
设第 个分割的地方有 个长方形,规定 表示第左边界开始的木块数, 表示右边界开始的木块数,那么就可以得到下述方程:
,其中 ,。
应用插板法,不难得出这里的方案数为:。
其次,假设分割后第 块区域的长度为 ,可以得到方程:
,其中 。
再次应用插板法,得出这里的方案数为 。
最后考虑填满剩下的 个小区间,套用最开始推出的公式,可以得到:
。
考虑范德蒙德卷积,这里实际上可以看成是在 个空格中选择 个空格插入,那么合起来的方案数就是: 。
最终还需要考虑特判 的情况,只有这种情况下可以用 个 的长方形竖着填满大木板。那么最终的答案就是:
。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=4e7+10,mod=998244353;
int fac[N],infac[N],inv[N],n,k,ans;
void init()
{
fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
for(int i=2;i<N;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=2;i<N;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<N;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
scanf("%d%d",&n,&k);init();ans=(n==k);
for(int j=0;j<=k;j++) for(int i=1;i<=k;i++) add(ans,1ll*C(2*n-2*j-2*i,k-j-2*i)*C(j+1,i)%mod*C(n-j-1,i-1)%mod);
printf("%d\n",ans);
return 0;
}
游戏
题意比较复杂,可以看原题面。
思路:
首先不难想到枚举 的取值来计算贡献。设 ,如果顺序枚举走的办公室,比较困难,可以考虑枚举第 个走到的办公室。由于之后走的办公室都已经被提醒过,那么第 次走到的办公室一定不会被 中的任意一个办公室提醒,换言之,第 次走的办公室在 区间中不存在约束。可以直接用埃氏筛预处理出这部分数字,设一共有 个这样的数。
第 个办公室可以在 中任意取,方案数为 。
第 个办公室之后走的办公室,显然就不能是这 个办公室其中之一,那么这一部分的方案数就为 ,
第 个办公室之前走的办公室,由于我们已经将 的顺序确定好了,那么这一部分的方案数就是 。
最后别忘了乘以贡献 。最终的表达式为:
。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e7+10,mod=1e9+7;
int fac[N],infac[N],inv[N],l,r,m,ans;bool vis[N];
void init()
{
fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=r;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=2;i<=r;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=r;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
for(int i=l;i<=r;i++)
{
if(vis[i]) continue;m++;
for(int j=i;j<=r;j+=i) vis[j]=1;
}
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
scanf("%d%d",&l,&r);init();int n=r-l+1;
for(int i=1;i<=n;i++) add(ans,1ll*i*m%mod*C(n-m,n-i)%mod*fac[n-i]%mod*fac[i-1]%mod);printf("%d\n",ans);
return 0;
}
构造数组
你现在有一个长度为 的数组 。一开始,所有 均为 。给出一个同样长度为 的目标数组 。求有多少种方案,使得通过若干次以下操作,可以让 数组变成 。
- 选出两个不同的下标 ,并将 和 同时增加 。
两种方案被称之为不同的,当且仅当存在一个 使得一种方案中第 次操作选择的两个下标 与另一种方案中的不同。
答案对 取模。
,,。
思路:
注意到总的操作次数一定为 ,令 。由于直接统计每次操作的下标比较复杂,考虑依次将 个数填入到每次操作中。
令 。设 表示将前 个数全部填入操作之后,有 个操作已经被填入 个数。同时也就可以得出,当前有 个操作被填入了 个数,剩下的 个操作还没有被填入过数字。
考虑枚举将当前 个数全部填入操作后,新增加了 个被全部填满的操作。不难发现,这些操作在此之前已经被填入过 个数,那么剩下的 个数就被填入没有被填过数字的操作中。对答案的贡献就是 。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=5050,M=30010,mod=998244353;
int n,m,b[N],s[N],f[2][M],fac[M],infac[M],inv[M];
void init()
{
fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
for(int i=2;i<M;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=2;i<M;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<M;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%d",&b[i]),s[i]=s[i-1]+b[i];if(s[n]%2) return puts("0"),0;
m=s[n]/2;f[0][0]=1;init();
for(int i=1;i<=n;i++)
{
for(int j=0;j<=(s[i-1])/2;j++)
{
int c2=j,c1=s[i-1]-j*2,c0=m-c2-c1;
if(c0<0) continue;f[i&1][j]=0;
}
for(int j=0;j<=(s[i-1])/2;j++)
{
int c2=j,c1=s[i-1]-j*2,c0=m-c2-c1;if(c0<0) continue;
for(int k=0;k<=b[i];k++)
{
if(c1<k||c0<b[i]-k) continue;
if(f[i-1&1][c2]) add(f[i&1][c2+k],1ll*f[i-1&1][c2]*C(c1,k)%mod*C(c0,b[i]-k)%mod);
}
}
}
printf("%d\n",f[n&1][m]);
return 0;
}
Lucas定理
对于质数 ,有:
吉夫特
题意比较繁琐,可以看原题题面。
思路:
应用 Lucas 定理, 。
对于后面的 ,其取值有 四种,其中只有 。
不难发现,应用 Lucas 定理后就是将 与 在二进制下的每一位单独拎出来,为了避免出现 的情况,需要满足在二进制下, 为 的子集。那么就可以用枚举子集的方法记录以每一个数结尾的方案数,最终的复杂度为
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=3e5+10,mod=1e9+7;
int n,x,f[N],ans;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
scanf("%d",&x);
for(int j=x-1&x;j;j=j-1&x) f[j]=(f[j]+f[x]+1)%mod;
ans=(ans+f[x])%mod;
}
printf("%d\n",ans);
return 0;
}
排列计数
称一个 的排列 是 Magic 的,当且仅当
计算 的排列中有多少是 Magic 的,答案对 取模。
, , 是一个质数。
思路:
考虑将排列中的大小关系构成一棵二叉树,那么对于 号节点,它的两个儿子分别为 和 。表示 且 。
注意到左右儿子中的节点互不影响,且当前节点是最小的点,假设 表示 子树的大小。 表示填满子树 的方案数,不难得出表达式:
。
需要注意,本题中可能存在 的情况,所以求组合数要用到Lucas。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e6+10;
int fac[N],infac[N],n,inv[N],mod,ans,siz[N];
void init()
{
fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=2;i<=n;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=n;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
int lucas(int n,int m){return m?1ll*C(n%mod,m%mod)*lucas(n/mod,m/mod)%mod:1; }
void dfs(int u){siz[u]=1;for(int v=u*2;v<=u*2+1;v++) if(v<=n) dfs(v),siz[u]+=siz[v];}
int calc(int u){if(u>n) return 1;return 1ll*lucas(siz[u]-1,siz[u*2])*calc(u*2)%mod*calc(u*2+1)%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
scanf("%d%d",&n,&mod);init();dfs(1);printf("%d\n",calc(1));
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】