介绍
斯特林数是组合数学中的一个重要内容,有许多有用的性质.它由十八世纪的苏格兰数学家James Stirling首先发现并说明了它们的重要性.
斯特林数主要处理的是把N个不同的元素分成k个集合或环的个数问题.现在我们说的斯特林数可以指两类数,分为第一类斯特林数和第二类斯特林数,其中第一类斯特林数还分成有符号和无符号两种.
第一类斯特林数
Problem
将 n 个不同的元素全部放入 m 个环中,不允许有空环出现,求方案数.
定义:
\[s(i,j)\]
为 i 个人分成 j 组做环排列的方法数目.
对于每一个s( i , j )我们考虑第 i 个物品,有两种情况:
1. i 可以单独构成一个非空循环排列,这样前 i-1 种物品构成 j-1 个非空循环排列,方法数为s(i-1,j-1);
2.也可以前 i-1 种物品构成 j 个非空循环排列,而第 i 个物品插入第 i 个物品的左边,这有( i-1)*s( i-1 , j )种方法;
于是我们可以得到s(i,j)的递推公式:
\[ s(i,j)=(i-1)*s(i-1,j)+s( i-1 , j-1 ) ,1<=i<=j-1\]
边界条件:\[s(i,0)=0 ,p>=1\]
\[ s(i,i)=1 ,p>=0\]
时间复杂度为 O(n^2).
第二类斯特林数
Problem
将 n 个不同的元素全部放入 m 个集合中,不允许有空集出现,求方案数.
定义:\[S(n,m)\]
为把n个元素划分成m个无序集合的方案数.
1)递推式
根据这个定义我们不难写出递推式
设状态S(i,j),对于第i个元素我们则有两种情况:
1.若单独一个集合,则方案数等价于S(i-1,j-1).
2.若不是单独一个集合,则他可以在之前任意j个集合里,方案为j x S(i-1,j)
于是可以得到递推公式:
\[S(i,j)=S(i-1,j-1)+S(i-1,j)*j\]
边界条件:\[S(p,p)=1,(p>=0)\]
\[S(p,0)=0,(p>0)\]
时间复杂度为 O(n^2).
2)通项公式
假设集合没有非空的限制,则答案显然是m^n
之后我们可以利用容斥原理,枚举至少有几个集合是空的,在套上容斥系数我们有(注意集合是无序的,所以最后要消序)
\[S(n,m)=\frac{1}{m!}*\sum^{m}_{k=0} (-1)^k*C_m^k)*(m-k)^n)\]
直接根据这个式子暴力求解某一行的斯特林数的复杂度是O(n^2)的
这是一个卷积形式,利用FFT我们可以将时间复杂度优化成O(nlogn).
然后这里有一道 二类斯特林的裸题 .给出第二类斯特林数的递推部分代码,add 函数为高精度加法.(我不想打高精度,贴了别人的哈..)
void add(int u,int v)
{
for(int i=1;i<=max(a[u-1][v-1][0],a[u-1][v][0]);i++)
a[u][v][i]+=a[u-1][v-1][i],
a[u][v][i]+=v*a[u-1][v][i];
a[u][v][0]=max(a[u-1][v-1][0],a[u-1][v][0]);
for(int i=1;i<=a[u][v][0];i++)
a[u][v][i+1]+=a[u][v][i]/10,
a[u][v][i]%=10;
while(a[u][v][a[u][v][0]+1])
{
a[u][v][0]++,
a[u][v][a[u][v][0]+1]+=a[u][v][a[u][v][0]]/10;
a[u][v][a[u][v][0]]%=10;
}
}
{
cin>>n>>m;
a[1][1][0]=1;
a[1][1][1]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
add(i,j);
if(a[n][m][0]==0)
{cout<<"0"<<endl;return 0;}
for(int i=a[n][m][0];i>=1;i--)
cout<<a[n][m][i];
return 0;
}