前言
在 【6】组合计数学习笔记 当作一类具体的问题提到过,但是这个东西好像使用范围比较广,查了一下这个知识点在大纲中,单独开一篇博客记录一下。
统一了斯特林数的记号,但有一些高级的内容涉及生成函数,并没有记载。
第二类斯特林数
基础定义
\(n\) 个有区别的球放到 \(m\) 个相同的盒子中,要求无一空盒,其不同的方案数用 \(n\brace{m}\) 表示,称为第二类斯特林数。
设有 \(n\) 个不同的球,分别用 \(b_1,b_2,\dots b_n\) 表示。 从 \(1\) 到 \(n\) 依次考虑每个球的放法,从中取出一个球 \(b_n\),\(b_n\)的放法有以下两种。
\(1\) :\(b_n\) 独自占一个盒子。
那么剩下的球只能放在 \(m-1\) 个盒子中,方案数为 \({n-1}\brace{m-1}\)。
\(2\) :\(b_n\) 与别的球共占一个盒子。
那么可以事先将 \(b_1,b_2,\dots b_{n-1}\) 这 \(n-1\) 个球放入 \(m\) 个盒子中,然后再将球 \(b_n\) 可以放入其中一个盒子中,方案数为 \(m{n-1\brace m}\)。
根据加法原理,得出第二类斯特林数的递推式。
\[{n\brace{m}}={n-1\brace m-1}+m{n-1\brace m} \]
边界情况为 \({0\brace0}=1\)。
使用递推法计算第二类斯特林数的时间复杂度为 \(O(mn)\),但可以一次求出范围内所有第二类斯特林数的值。
通项公式
\[{n\brace m}=\sum_{i=0}^m\frac{(-1)^{m-i}i^n}{i!(m-i)!} \]
如果 \(n\) 个不同的球放进 \(m\) 个不相同的盒子,允许有空盒,那方案数为 \(m^n\)。
我们很容易把不相同的盒子转化相同的盒子,考虑编号的排列,除掉 \(m!\) 即可。
考虑容斥原理。枚举钦定空盒数量 \(k\),如果剩余随便放,则答案为 \((m-i)^n\)。根据容斥原理,如果 \(k\) 为偶数,就加上这种情况的贡献;如果 \(k\) 为奇数,就减去这种情况的贡献。又由于需要选定钦定哪 \(k\) 个空盒,还需乘上 \(\binom{m}{k}\),推出以下式子。
\[{n\brace m}=\frac{1}{m!}\sum_{k=0}^m(-1)^k\binom{m}{k}(m-k)^n \]
换元,用 \(i\) 代换 \(m-k\)。
\[{n\brace m}=\frac{1}{m!}\sum_{i=0}^m(-1)^{m-i}\binom{m}{m-i}i^n \]
用阶乘展开组合数,把 \(\frac{1}{m!}\) 乘进去,就得到了最后的式子。
\[{n\brace m}=\frac{1}{m!}\sum_{i=0}^m\frac{(-1)^{m-i}i^nm!}{i!(m-i)!}=\sum_{i=0}^m\frac{(-1)^{m-i}i^n}{i!(m-i)!} \]
使用通项公式计算第二类斯特林数的时间复杂度为 \(O(m\log n)\),需要快速幂和预处理阶乘逆。
斯特林反演
\[x^n=\sum_{i=0}^n{n\brace i}{\binom{x}{i}i!} \]
考虑组合意义。组合意义为有 \(n\) 个有区别的球装到 \(x\) 个有区别的盒子里。
左边式子就是考虑每一个球可以放到 \(x\) 个盒子中,有 \(n\) 中放法,根据乘法原理有 \(x^n\) 种放法。
右边式子就是考虑选择多少个盒子放球,枚举放球的盒子的数量 \(i\),首先有 \(\binom{x}{i}\) 种选盒子编号的法,然后假设这些盒子全部相同,\(n\) 个有区别球放进 \(i\) 个相同的盒子里,方案数为 \(n\brace i\)。因为盒子互不相同,最后再给盒子分配编号,有 \(i!\) 种选法。根据乘法原理和加法原理,就是右边的式子。
这个式子可以实现对高次项进行转化,将指数化为枚举,是一个降次的方式。同时,这个式子也与上升幂和下降幂以及其转化有关,这里暂不展开。
第一类斯特林数
如果对于一个排列,我们把这个排列的循环移位视为与此排列是同一种方案,我们把这种排列称为圆排列。可以想象圆排列是在圆桌上排座位。
长度为 \(n\) 的圆排列数量为 \(\frac{n!}{n}\),因为每一个不同的排列都被计算了 \(n\) 次。
\(n\) 个有区别的球放到 \(m\) 个相同的圆排列中,要求无一空圆排列,其不同的方案数用 \({n\brack m}\) 表示,称为第一类斯特林数。
设有 \(n\) 个不同的球,分别用 \(b_1,b_2,\dots b_n\) 表示。 从 \(1\) 到 \(n\) 依次考虑每个球的放法,从中取出一个球 \(b_n\),\(b_n\)的放法有以下两种。
\(1\) :\(b_n\) 独自占一个圆排列。
那么剩下的球只能放在 \(m-1\) 个圆排列中,方案数为 \({n-1\brack m-1}\)。
\(2\) :\(b_n\) 与别的球共占一个圆排列。
那么可以事先将 \(b_1,b_2,\dots b_{n-1}\) 这 \(n-1\) 个球放入 \(m\) 个圆排列中。因为圆排列可以看作一个环,我们发现每一个大小为 \(x\) 的圆排列中有 \(x\) 个插入的位置,总共就有 \(n-1\) 个插入的位置,方案数为 \((n-1){n-1\brack m}\)。
根据加法原理,得出第一类斯特林数的递推式。
\[{n\brack{m}}={n-1\brack m-1}+(n-1){n-1\brack m} \]
边界情况为 \({0\brack0}=1\)。
使用递推法计算第一类斯特林数的时间复杂度为 \(O(mn)\),但可以一次求出范围内所有第二类斯特林数的值。第一类斯特林数没有实用的通项公式。
例题
例题 \(1\) :
第二类斯特林数模板题,需要高精度。
cpp
#include <bits/stdc++.h>
using namespace std;
int n,m;
int f[101][101][101];
void huge_int(int na,int nb,int a,int b,int c,int d,int m)
{
int flag=0;
for(int i=1;i<=100;i++)
f[na][nb][i]=f[a][b][i];
for(int i=100;i>0;i--)
{
f[na][nb][i]+=f[c][d][i]*m+flag;
flag=f[na][nb][i]/10;
f[na][nb][i]%=10;
}
}
void print(int n,int m)
{
int now=1;
for(now=1;now<=100;now++)
if(f[n][m][now]!=0)break;
for(int i=now;i<=100;i++)
printf("%d",f[n][m][i]);
}
int main()
{
for(int i=1;i<=100;i++)f[i][1][100]=f[i][i][100]=1;
for(int i=1;i<=100;i++)
for(int j=1;j<=100;j++)
if(!(i==j||j==1))huge_int(i,j,i-1,j-1,i-1,j,j);
while(scanf("%d%d",&n,&m)!=-1)
{
if(n<m)
{
printf("0\n");
continue;
}
print(n,m);
putchar('\n');
}
return 0;
}
例题 \(2\) :
首先答案满足每行有一个棋子或每列有一个棋子,否则假设某一行没有棋子,则为了覆盖这一行需要每一列都有一个棋子,矛盾。某一列没有棋子同理。因此,最多只能把棋子摆成一排,产生 \(k-1\) 对碰撞。
行和列等价,我们先考虑每行有一个棋子的情况。假设每一列都只有一颗棋子,如果需要产生 \(k\) 对棋子可以相互攻击,则需要将 \(k\) 列的棋子移动到别的列,最后总共有 \(n-k\) 列有棋子。
首先选出 \(n-k\) 列的编号,方案数为 \(\binom{n}{n-k}\)。然后假设每列相同,由于棋子所在的行不同所以本质不同,相当于有区别的求放进无区别的盒子里,方案数为 \(n\brace n-k\)。最后由于每列不同,分配编号,方案数为 \((n-k)!\)。最终的式子为 \(\binom{n}{n-k}{n\brace n-k}(n-k)!\)。第二类斯特林数可以直接使用通项公式计算。
注意 \(k\ne0\) 时还需要考虑每列有一个棋子的情况,与每行有一个棋子的情况相同,乘以 \(2\) 即可。时间复杂度 \(O(m\log n)\)。
cpp
#include <bits/stdc++.h>
using namespace std;
long long n,k,jc[300000],inv[300000],ans=0;
const long long mod=998244353;
long long power(long long a,long long p)
{
long long x=a,ans=1;
while(p)
{
if(p&1)ans=ans*x%mod;
p>>=1;
x=x*x%mod;
}
return ans;
}
long long c(long long n,long long k)
{
return jc[n]*inv[n-k]%mod*inv[k]%mod;
}
long long strl(long long n,long long m)
{
long long ans=0;
for(int i=0;i<=m;i++)ans=(ans+power(-1,m-i)*power(i,n)%mod*inv[i]%mod*inv[m-i]%mod)%mod;
return (ans%mod+mod)%mod;
}
int main()
{
jc[0]=1;
for(int i=1;i<=200000;i++)jc[i]=jc[i-1]*i%mod;
inv[200000]=power(jc[200000],mod-2)%mod;
for(int i=199999;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
scanf("%lld%lld",&n,&k);
if(k>=n)printf("0\n");
else if(k==0)printf("%lld\n",c(n,n-k)%mod*strl(n,n-k)%mod*jc[n-k]%mod);
else printf("%lld\n",2*c(n,n-k)%mod*strl(n,n-k)%mod*jc[n-k]%mod);
return 0;
}
例题 \(3\) :
我们发现,最高的建筑无论从左边还是从右边都可以被看到,那我们先不管它。
考虑左边的建筑的形式,发现一定是 \(A-1\) 组建筑,每组建筑最左边的建筑比这个组左边的组的建筑都高,而这组内其他建筑都比最左边的建筑矮。右边的建筑的形式也是同理的 \(B-1\) 组建筑,只不过最高的在最右边。
我们考虑组内怎么分配。显然,\(n\) 个元素组内除了最高的元素必须在最左边或者最右边,其余元素的排布没有限制,因此方案数为 \((n-1)!=\frac{n!}{n}\)。观察式子,发现其实就是圆排列。
接下来考虑把除了最高的元素分配到这 \(A+B-2\) 个组中。我们发现,无论怎么分配,只要不存在空的组,一定可以按照组内最大的元素把每个组排好序,且两种方案必不相同。因此,相当于 \(n-1\) 个有区别的球放进 \(A+B-2\) 个圆排列中,方案数为 \(n-1\brack A+B-2\)。
最后还需要选择 \(A-1\) 个组丢到左边去,所以还需要乘上 \(\binom{A+B-2}{A-1}\)。由于 \(A,B\) 很小,预处理之后可以做到 \(O(1)\) 查询。
cpp
#include <bits/stdc++.h>
using namespace std;
int t,n,a,b,strl[50001][201],c[201][201];
const int mod=1e9+7;
int main()
{
strl[0][0]=1;
for(int i=1;i<=50000;i++)
for(int j=1;j<=200;j++)
strl[i][j]=(strl[i-1][j-1]+1ll*strl[i-1][j]*(i-1)%mod)%mod;
for(int i=0;i<=200;i++)c[i][0]=c[i][i]=1;
for(int i=1;i<=200;i++)
for(int j=1;j<i;j++)
c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
scanf("%d",&t);
while(t--)
{
scanf("%d%d%d",&n,&a,&b);
printf("%lld\n",1ll*strl[n-1][a+b-2]*c[a+b-2][a-1]%mod);
}
return 0;
}
例题 \(4\) :
多项式显然是在诈骗,考虑展开后求每一项,最后每一项乘上系数累加。先写出每一项的式子。
\[\sum_{k=0}^{n}k^mx^k\binom{n}{k} \]
\(n\) 非常大,而 \(m\) 比较小。而 \(m\) 是指数,考虑经典的斯特林反演技巧,带入斯特林反演公式把 \(m\) 变成枚举量。
\[=\sum_{k=0}^{n}x^k\binom{n}{k}\sum_{i=0}^m{m\brace i}\binom{k}{i}i! \]
交换枚举顺序,把对 \(m\) 的枚举提到前面去。
\[=\sum_{i=0}^m{m\brace i}i!\sum_{k=0}^{n}x^k\binom{n}{k}\binom{k}{i} \]
对 \(n\) 求和后面有两项组合数相乘有点麻烦,考虑把一个提出去。根据组合意义,\(\binom{n}{k}\binom{k}{i}\) 表示先在 \(n\) 个中选 \(k\) 个,再在 \(k\) 中选 \(i\) 个,等价的做法是先在 \(n\) 个中选 \(i\) 个,再在 \(n-i\) 中选 \(k-i\) 个,即 \(\binom{n}{i}\binom{n-i}{k-i}\)。这样 \(\binom{n}{i}\) 就可以提出去了。
\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}\sum_{k=0}^{n}x^k\binom{n-i}{k-i} \]
考虑把对 \(n\) 求和后面配成二项式定理的形式消去求和。由于单组合数不好变形,考虑对指数进行变形。二项式定理要求指数和组合数下指标相同,所以考虑提一个 \(x^i\) 来配凑。由于 \(k\lt i\) 时 \(\binom{n-i}{k-i}=0\),所以可以直接忽略 \(k\lt i\) 的项。
\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i\sum_{k=i}^{n}x^{k-i}\binom{n-i}{k-i} \]
换元,用 \(p\) 代 \(k-i\)。注意换元后变量的范围。
\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i\sum_{p=0}^{n-i}x^{p}\binom{n-i}{p} \]
配凑一个 \(1^{n-i-p}\) 项,就可以愉快地二项式定理了。
\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i(x+1)^{n-i} \]
由于 \(n\) 很大,目前 \(\binom{n}{i}\) 求不出来。考虑 \(i!\binom{n}{i}=\frac{n!}{(n-i)!}=\prod_{j=n-i+1}^nj\),考虑维护变化,在枚举 \(i\) 的这一轮计算完成之后乘上一个 \(n-i\),就维护好了这两项。递推求第二类斯特林数加上快速幂就做完了。时间复杂度 \(O(m^2\log m)\)。
cpp
#include <bits/stdc++.h>
using namespace std;
long long n,x,mod,k,m,a,s2[2000][2000],ans=0;
long long power(long long a,long long p)
{
long long x=a,ans=1;
while(p)
{
if(p&1)ans=ans*x%mod;
p>>=1;
x=x*x%mod;
}
return ans;
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&x,&mod,&k);
s2[0][0]=1;
for(int i=1;i<=1000;i++)
for(int j=1;j<=1000;j++)
s2[i][j]=(s2[i-1][j]*j%mod+s2[i-1][j-1])%mod;
for(int m=0;m<=k;m++)
{
long long sum=0,ac=1;
scanf("%lld",&a);
for(int i=0;i<=m;i++)sum=(sum+s2[m][i]*ac%mod*power(x,i)%mod*power(x+1,n-i)%mod)%mod,ac=ac*(n-i)%mod;
ans=(ans+sum*a%mod)%mod;
}
printf("%lld\n",ans);
return 0;
}
后记
涉及到上升幂和下降幂的内容 可以直接转化为斯特林反演的式子给出的形式,除非题目里直接出现上升幂和下降幂,否则应该不会用到。
世味年来薄似纱,谁令骑马客京华。
小楼一夜听春雨,深巷明朝卖杏花。
矮纸斜行闲作草,晴窗细乳戏分茶。
素衣莫起风尘叹,犹及清明可到家。