数数题整理
组合数
定义:\(n\) 个不同的元素里选择 \(m\) 个的方案数,记作 \(n\choose m\)。
性质:
二项式定理:
容斥原理
有时容斥系数会是莫比乌斯函数 \(\mu(n)\)。
子集反演
两种形式:
构造 \(f,g\) 时的要求:\(f\) 能知道答案,\(g\) 比较好求。
入手点一般是题目中对答案的限制条件。
二项式反演
三种形式:
直接求不好求,但是至少好求;直接求不好求,但是至多好求。
构造 \(f,g\) 时的要求:\(f\) 能知道答案,\(g\) 比较好求。
一般来说,\(f(i)=\text{exactly }i\),\(g(i)=\text{at least/most }i\)。
小球盒子模型(八重计数法)
以下用 \(n\) 表示小球个数,\(m\) 表示盒子个数。
- 球相同,盒子不同,不能有空盒:\({n-1\choose m-1}\)(插板)。
- 球相同,盒子不同,可以有空盒:\({n+m-1\choose m-1}\)(先给每个盒子放一个球,然后插板)。
- 球不同,盒子不同,可以有空盒:\(m^n\)(每个球 \(m\) 种选择)。
- 球不同,盒子相同,不能有空盒:\(n\brace m\)(第二类斯特林数的定义)。
- 球不同,盒子不同,不能有空盒:\(m!{n\brace m}\)。
- 球不同,盒子相同,可以有空盒:\(\sum_{i=1}^n{n\brace i}\)(贝尔数)。
- 球相同,盒子相同,可以有空盒:\(f_{n,k}=f_{n,k-1}+f_{n-k,k}\)。
- 球相同,盒子相同,不能有空盒:\(f_{n-k,k}\)(先给每个盒子放一个球,然后随便放)。
斯特林数
第一类斯特林数
定义:将 \(n\) 个两两不同的元素,划分为 \(k\) 个互不区分的非空轮换的方案数,记作 \(n\brack m\)。
递推式:
证明与第二类斯特林数的式子类似。
没有实用的通项公式。
性质:
第二类斯特林数
定义:将 \(n\) 个不同的球放在 \(m\) 个相同的盒子里(盒子不能为空)的方案数,记作 \(n\brace m\)。
递推式:
证明可以考虑组合意义:如果第 \(n\) 个球放到一个新的盒子里,那就是 \(n-1\brace m-1\),否则先 \(n-1\brace m\),再考虑第 \(n\) 个球放在哪个盒子里,于是就要再乘 \(m\)。证毕。
通项公式:
证明:设 \(f(i)\) 表示将 \(n\) 个不同的球放在 \(i\) 个不同的盒子里(盒子不能为空)的方案数,设 \(g(i)\) 表示将 \(n\) 个不同的球放在 \(i\) 个不同的盒子里(盒子可以为空)的方案数。则 \(g(i)=i^n,f(i)=i!{n\brace i}\)。
则有:
由二项式反演,得:
证毕。
上升幂与下降幂
定义:
下降幂转组合数:
普通幂转上升幂、下降幂:
上升幂、下降幂转普通幂:
斯特林反演
Burnside 引理和 Polya 定理
\(G\) 为置换群,\(f(g)\) 为在置换 \(g\) 作用下仍不变的方案数,\(m\) 为颜色数,\(c(g)\) 为置换 \(g\) 能拆出来的循环数。
习题
UVA10325 The Lottery
补集转化:将都不能被 \(a_i\) 整除转化为全集减能被 \(a_i\) 整除,然后用容斥原理解决。
\(A_i=\{x:1\le x\le n,a_i\mid x\}\)
Code
#include<algorithm>
#include<cstdio>
typedef long long ll;
int n,m;
ll a[30];
ll lcm(ll a,ll b){
return a/std::__gcd(a,b)*b;
}
bool mian(){
if(scanf("%d%d",&n,&m)==EOF)return 0;
for(int i=1;i<=m;i++)
scanf("%lld",a+i);
ll ans=n;
for(int i=1;i<=(1<<m)-1;i++){
ll _lcm=1;
for(int j=0;j<m;j++)
if((i>>j)&1){
_lcm=lcm(_lcm,a[j+1]);
if(_lcm>n)break;
}
if(__builtin_popcountll(i)%2==1)ans-=n/_lcm;
else ans+=n/_lcm;
}
printf("%lld\n",ans);
return 1;
}
int main(){
while(mian());
return 0;
}
UVA11806 Cheerleaders
补集转化:将四条边上都有人转化为全集有边上没人,然后用容斥原理解决。
\(A_i=\{x:\text{arrangement }x\text{ that satisfies there are }i\text{ sides which don't have person}\}\)
算 \(|A_i|\) 时枚举有哪些边上没人,那么相应的能放人的地方是一个矩形。然后用组合数计算即可。
Code
#include<algorithm>
#include<cstdio>
const int N=500;
const int P=int(1e6)+7;
int C[N+10][N+10];
void init(){
C[0][0]=1;
for(int i=1;i<=N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
}
}
void mian(int tc){
int n,m,k;scanf("%d%d%d",&n,&m,&k);
int ans=0;
for(int msk=0;msk<=15;msk++){ // msk 表示哪些边上放人了
int x=n,y=m;
for(int i=0;i<4;i++)
if((msk>>i)&1){
if(i%2==0)x--;
else y--;
}
if(__builtin_popcount(msk)%2==0)
(ans+=C[x*y][k])%=P;
else (ans-=C[x*y][k])%=P;
}
printf("Case %d: %d\n",tc,(ans+P)%P);
}
int main(){
init();
int T;scanf("%d",&T);
for(int i=1;i<=T;i++)mian(i);
return 0;
}
SP6285 NGM2 - Another Game With Numbers
UVA10325 双倍经验。
SP4168 SQFREE - Square-free integers
没看出这个题怎么用容斥做。。
结论:\(\mu^2(i)=\sum_{d^2\mid i}\mu(d)\)。证明:
设 \(i\) 的因数中,最大的平方数是 \(q=p_1^{2\alpha_1}p_2^{2\alpha_2}\cdots p_k^{2\alpha_k}\),则 \(d^2\mid i\Leftrightarrow d\mid\sqrt q\)
\([q=1]\) 相当于 \(i\) 没有平方因数,也就是 \(\mu^2(i)=1\)。证毕。
于是我们来推式子:
整除分块即可。时间复杂度 \(O(\sqrt[3]n)\),证明:
复杂度即为不同的 \(\left\lfloor\frac n{d^2}\right\rfloor\) 的个数。
当 \(d\le\sqrt[3]n\) 时,显然最多有 \(\sqrt[3]{n}\) 个数。
当 \(d\gt\sqrt[3]n\) 时,\(\left\lfloor\frac n{d^2}\right\rfloor\lt\left\lfloor\frac n{(\sqrt[3]n)^2}\right\rfloor=\left\lfloor \sqrt[3]n\right\rfloor\),最多 \(\sqrt[3]n\) 个数。
综上,最多有 \(2\sqrt[3]n\) 个数。证毕。
Code
#include<algorithm>
#include<cstdio>
#include<cmath>
typedef long long ll;
const int N=1e7;
int prm[N+10],notPrm[N+10],totp,mu[N+10],smu[N+10];
void sieve(){
notPrm[1]=1,mu[1]=1;
for(int i=2;i<=N;i++){
if(!notPrm[i])prm[++totp]=i,mu[i]=-1;
for(int j=1;j<=totp&&i*prm[j]<=N;j++){
notPrm[i*prm[j]]=1;
if(i%prm[j]==0)break;
mu[i*prm[j]]=-mu[i];
}
}
for(int i=1;i<=N;i++)
smu[i]=smu[i-1]+mu[i];
}
void mian(){
ll n;scanf("%lld",&n);
ll ans=0;
for(ll l=1,r;l<=ll(sqrt(n));l=r+1){
r=std::min(ll(sqrt(n/(n/(l*l)))),ll(sqrt(n)));
ans+=1LL*(n/(l*l))*(smu[r]-smu[l-1]);
}
printf("%lld\n",ans);
}
int main(){
sieve();
int T;scanf("%d",&T);
while(T--)mian();
return 0;
}
P4318 完全平方数
二分答案之后就和上一个题一模一样。
Code
#include<algorithm>
#include<cstdio>
#include<cmath>
typedef long long ll;
const int N=1e7;
int prm[N+10],notPrm[N+10],totp,mu[N+10],smu[N+10];
ll k;
void sieve(){
notPrm[1]=1,mu[1]=1;
for(int i=2;i<=N;i++){
if(!notPrm[i])prm[++totp]=i,mu[i]=-1;
for(int j=1;j<=totp&&i*prm[j]<=N;j++){
notPrm[i*prm[j]]=1;
if(i%prm[j]==0)break;
mu[i*prm[j]]=-mu[i];
}
}
for(int i=1;i<=N;i++)
smu[i]=smu[i-1]+mu[i];
}
bool check(ll x){
ll ans=0;
for(ll l=1,r;l<=ll(sqrt(x));l=r+1){
r=std::min(ll(sqrt(x/(x/(l*l)))),ll(sqrt(x)));
ans+=1LL*(x/(l*l))*(smu[r]-smu[l-1]);
}
return ans>=k;
}
void mian(){
scanf("%lld",&k);
ll l=1,r=10000000000LL,mid,ans=-1;
while(l<=r){
mid=(l+r)>>1;
if(check(mid))ans=mid,r=mid-1;
else l=mid+1;
}
printf("%lld\n",ans);
}
int main(){
sieve();
int T;scanf("%d",&T);
while(T--)mian();
return 0;
}
P3349 [ZJOI2016] 小星星
考虑暴力 dp:设 \(f_{u,x,S}\) 表示现在饰品中的 \(u\) 映射到了 \(x\),以 \(u\) 为根的子树中用掉了的映射的集合为 \(S\) 时的答案。
(因为我们要做树形 dp,所以要设状态 \(u\),因为映射到的数不能重复,所以要设 \(x,S\))
转移方程如下(\(E\) 表示原来饰品的边集):
由于要枚举子集,时间复杂度为 \(\mathcal O(n^33^n)\),过不了。
把枚举子集改成容斥:先枚举 \(S\),然后钦定我们只能映射到集合 \(S\) 里的数,但是映射到的数可以重复。然后通过容斥解决答案会被算多的问题。
这样 dp 状态就可以去掉一维了:
时间复杂度降到了 \(\mathcal O(n^32^n)\),可以通过。
Code
#include<algorithm>
#include<cstdio>
#include<cstring>
typedef long long ll;
const int N=17;
int n,m,G[N+10][N+10],T[N+10][N+10];
ll f[N+10][N+10],ans;
void DFS(int u,int _fa,int msk){
for(int x=1;x<=n;x++)
if((msk>>(x-1))&1)
f[u][x]=1;
for(int v=1;v<=n;v++)
if(v!=_fa&&T[u][v]){
DFS(v,u,msk);
for(int x=1;x<=n;x++)
if((msk>>(x-1))&1){
ll sum=0;
for(int y=1;y<=n;y++)
if((msk>>(y-1))&1&&G[x][y])
sum+=f[v][y];
f[u][x]*=sum;
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
G[u][v]=G[v][u]=1;
}
for(int i=1;i<n;i++){
int u,v;scanf("%d%d",&u,&v);
T[u][v]=T[v][u]=1;
}
for(int msk=0;msk<=(1<<n)-1;msk++){
memset(f,0,sizeof(f));
DFS(1,0,msk);
ll res=0;
for(int x=1;x<=n;x++)
if((msk>>(x-1))&1)res+=f[1][x];
ans+=((n-__builtin_popcount(msk))&1)?-res:res;
}
printf("%lld\n",ans);
return 0;
}
SP9097 NOVICE65 - Derangements HARD
考虑对 \(\forall 1\le i\le n,a_i\ne b_i\) 这个限制条件运用子集反演:
\(f(S)\) 表示 \(\forall i\in S,a_i=b_i\) 且 \(\forall i\notin S,a_i\ne b_i\) 时的答案,显然 \(ans=f(\varnothing)\)。
\(g(S)\) 表示 \(\forall i\in S,a_i=b_i\) 时的答案。
计算 \(g(S)\) 时相当于一个可重排列,答案即(\(cnt_i\) 表示 \(i\) 在 \(S\) 外的出现次数):
Code
#include<algorithm>
#include<cstdio>
#include<cstring>
typedef long long ll;
const int N=15;
int n,a[N+10],cnt[N+10];
ll fac[N+10];
void mian(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",a+i);
ll ans=0;
for(int msk=0;msk<=(1<<n)-1;msk++){
ll res=0;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
if(!((msk>>(i-1))&1))cnt[a[i]]++;
res=fac[n-__builtin_popcount(msk)];
for(int i=0;i<n;i++)res/=fac[cnt[i]];
ans+=(((__builtin_popcount(msk))&1)?-1:1)*res;
}
printf("%lld\n",ans);
}
int main(){
fac[0]=1;
for(int i=1;i<=N;i++)fac[i]=fac[i-1]*i;
int T;scanf("%d",&T);
while(T--)mian();
return 0;
}
P3298 [SDOI2013] 泉
如果直接做,则由于我们不好判断这 \(k\) 个以外的东西是否相等,所以没法做。
恰好 \(k\) 个满足条件不好做,但至少 \(k\) 个满足条件是可以做的:
我们枚举所有满足 \(S\subseteq\{1,2,3,4,5,6\},|S|=k\) 的集合 \(S\),然后钦定所有以 \(S\) 内元素 \(j\) 为下标的 \(a_{i,j}\) 必须一样,其它随便。判断这 \(k\) 个元素是否相等可以通过 hash \(a\) 数组实现。将所有 \(S\) 的答案加起来就是我们想要的。
于是设恰好 \(k\) 个时答案为 \(f(k)\),至少 \(k\) 个时答案为 \(g(k)\)。
Code
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<utility>
typedef long long ll;
const int N=1e5,M=6;
const int P1=1212121,P2=19260817,B1=101,B2=233;
int n,k,a[N+10][M+10];
std::pair<int,int> hsh[N+10];
ll g[1<<M];
int fac[M+10];
ll calc(int msk){
if(msk==0)return 1LL*n*(n-1)/2;
memset(hsh,0,sizeof(hsh));
for(int i=1;i<=M;i++)
if((msk>>(i-1))&1)
for(int j=1;j<=n;j++){
hsh[j].first =(1LL*hsh[j].first *B1%P1+a[j][i])%P1;
hsh[j].second=(1LL*hsh[j].second*B2%P2+a[j][i])%P2;
}
std::sort(hsh+1,hsh+n+1);
ll res=0;
for(int i=1,j=1;i<=n;i=j,j=i){
while(hsh[j]==hsh[i])j++;
res+=1LL*(j-i)*(j-i-1)/2;
}
return res;
}
int main(){
fac[0]=1;
for(int i=1;i<=M;i++)fac[i]=fac[i-1]*i;
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
for(int j=1;j<=M;j++)
scanf("%d",&a[i][j]);
for(int msk=0;msk<=(1<<M)-1;msk++)
g[__builtin_popcount(msk)]+=calc(msk);
ll ans=0;
for(int i=k;i<=M;i++)
ans+=(((i-k)&1)?-1LL:1LL)*(fac[i]/fac[k]/fac[i-k])*g[i];
printf("%lld\n",ans);
return 0;
}
P5505 [JSOI2011] 分特产
我们可以一个一个特产的分,但是每个人不一定要拿到所有特产,所以没法这样。
所有人都有特产不好做,但是至少 \(k\) 个人没特产是可以做的:
先选 \(k\) 个人,钦定他们没特产,剩下的随便。那么对于每一个特产,它就是一个小球盒子模型:
设 \(f(i)\) 为恰好 \(i\) 个人没有特产的答案,\(g(i)\) 为至少 \(i\) 个人没有特产的答案,那么答案就是 \(f(0)\)。
Code
#include<algorithm>
#include<cstdio>
const int N=2e3;
const int P=1e9+7;
int n,m,a[N+10];
int C[N+10][N+10];
void initC(){
C[0][0]=1;
for(int i=1;i<=N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%P;
}
}
int calc(int x){
int res=1;
for(int i=1;i<=m;i++)
res=1LL*res*C[a[i]+n-x-1][n-x-1]%P;
return 1LL*res*C[n][x]%P;
}
int main(){
initC();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d",a+i);
int ans=0;
for(int i=0;i<n;i++)
(ans+=((i&1)?-1LL:1LL)*calc(i)%P)%=P;
printf("%d\n",(ans+P)%P);
return 0;
}
CF1342E Placing Rooks
因为所有格子都要被攻击到,所以每行必须有一个棋子或者每列都必须有一个棋子。
除了 \(k=0\) 时(此时答案即 \(n!\)),以上两种情况互不重合且等价。我们现在只考虑每行都有棋子的情况。
首先恰好 \(k\) 列没有棋子,这个读者自证不难。
然后这 \(n\) 不同行的棋子要放在 \(n-k\) 个不同列中,方案数是 \((n-k)!{n\brace n-k}\)。
所以总的方案数是:
那个第二类斯特林数可以用通项公式求。
Code
#include<algorithm>
#include<cstdio>
typedef long long ll;
const int N=2e5;
const int P=998244353;
int fac[N+10],ifac[N+10];
int qpow(int a,int b,int p=P){
int res=1;
for(;b;a=1LL*a*a%p,b>>=1)
if(b&1)res=1LL*res*a%p;
return res;
}
void initC(){
fac[0]=1;
for(int i=1;i<=N;i++)fac[i]=1LL*fac[i-1]*i%P;
ifac[N]=qpow(fac[N],P-2);
for(int i=N-1;i>=0;i--)ifac[i]=1LL*ifac[i+1]*(i+1)%P;
}
int C(int a,int b){
return 1LL*fac[a]*ifac[a-b]%P*ifac[b]%P;
}
int S(int n,int k){
int res=0;
for(int i=0;i<=k;i++)
(res+=((i&1)?-1LL:1LL)*C(k,i)*qpow(k-i,n)%P)%=P;
return 1LL*res*ifac[k]%P;
}
int main(){
initC();
int n;ll k;
scanf("%d%lld",&n,&k);
if(k>=n)return puts("0"),0;
if(k==0)return printf("%d\n",fac[n]),0;
printf("%d\n",(2LL*C(n,k)*fac[n-k]%P*S(n,n-k)%P+P)%P);
return 0;
}
CF932E Team Work
我们来推式子:
\({k\brace j}\) 可以 \(\mathcal O(k^2)\) 求,于是这个题就做完了。
Code
#include<algorithm>
#include<cstdio>
const int N=5000;
const int P=int(1e9)+7;
int n,k,S[N+10][N+10];
int ans;
void init(){
S[0][0]=1;
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
S[i][j]=(S[i-1][j-1]+1LL*S[i-1][j]*j%P)%P;
}
int qpow(int a,int b){
int res=1;
for(;b;b>>=1,a=1LL*a*a%P)
if(b&1)res=1LL*res*a%P;
return res;
}
int main(){
init();
scanf("%d%d",&n,&k);
int Cni=1;
for(int i=0;i<=std::min(n,k);i++){
(ans+=1LL*S[k][i]%P*Cni%P*qpow(2,n-i)%P)%=P;
Cni=1LL*Cni*(n-i)%P;
}
printf("%d\n",ans);
return 0;
}
CF1278F Cards
首先答案就是 \(\sum_ii^k\ \times\) 恰好 \(i\) 次是王牌的概率。
设 \(p=\frac 1m,q=1-p\),则答案为:
(倒数第二步是用了二项式定理)
于是就做完了。
Code
#include<algorithm>
#include<cstdio>
const int N=5000;
const int P=998244353;
int n,m,k,S[N+10][N+10];
int ans;
void init(){
S[0][0]=1;
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
S[i][j]=(S[i-1][j-1]+1LL*S[i-1][j]*j%P)%P;
}
int qpow(int a,int b){
int res=1;
for(;b;b>>=1,a=1LL*a*a%P)
if(b&1)res=1LL*res*a%P;
return res;
}
int main(){
init();
scanf("%d%d%d",&n,&m,&k);
int Cni=1,inv=qpow(m,P-2);
for(int i=0;i<=std::min(n,k);i++){
(ans+=1LL*S[k][i]%P*Cni%P*qpow(inv,i)%P)%=P;
Cni=1LL*Cni*(n-i)%P;
}
printf("%d\n",ans);
return 0;
}
P6620 [省选联考 2020 A 卷] 组合数问题
zhx yyds!!
\(b_i\) 是 \(f(k)\) 转成下降幂多项式后的系数:
于是 \(b\) 就可以 \(\mathcal O(m^2)\) 预处理了。
Code
#include<algorithm>
#include<cstdio>
const int M=1000;
int n,x,p,m,a[M+10],b[M+10];
int S[M+10][M+10];
void init(){
S[0][0]=1;
for(int i=1;i<=M;i++)
for(int j=1;j<=M;j++)
S[i][j]=(S[i-1][j-1]+1LL*S[i-1][j]*j%p)%p;
}
int qpow(int a,int b){
int res=1;
for(;b;a=1LL*a*a%p,b>>=1)
if(b&1)res=1LL*res*a%p;
return res;
}
int main(){
scanf("%d%d%d%d",&n,&x,&p,&m);
for(int i=0;i<=m;i++)
scanf("%d",a+i);
init();
for(int i=0;i<=m;i++)
for(int j=i;j<=m;j++)
(b[i]+=1LL*a[j]*S[j][i]%p)%=p;
int ans=0,fac=1;
for(int i=0;i<=m;i++){
(ans+=1LL*b[i]*fac%p*qpow(x,i)%p*qpow(x+1,n-i)%p)%=p;
fac=1LL*fac*(n-i)%p;
}
printf("%d\n",ans);
return 0;
}

浙公网安备 33010602011771号