斯特林数与斯特林反演
(Stirling Number)
斯特林数
第二类斯特林数
比第一类斯特林数常见得多,且似乎简单一点(?
又名为斯特林子集数。\(\begin{Bmatrix}n\\k\end{Bmatrix}\),表示将 \(n\) 个两两不同的元素,划分为 \(k\) 个互不区分的非空子集的方案数。
递推式
\(\begin{Bmatrix}n\\k\end{Bmatrix} =\begin{Bmatrix}n-1\\k-1\end{Bmatrix}+k\begin{Bmatrix}n-1\\k\end{Bmatrix}\)
边界是 \(\begin{Bmatrix}n\\0\end{Bmatrix}=[n=0]\)
对递推式的理解就是加入一个新元素,要么单独放入一个子集,要么放入一个现有的非空子集。
通项公式
考虑用二项式反演证明。
设将 \(n\) 个两两不同的元素,划分到 \(i\) 个两两不同的可空集合的方案数为 \(G_i\);将 \(n\) 个两两不同的元素,划分到 \(i\) 个两两不同的非空集合的方案数为 \(F_i\)。
则 \(G_i=i^n\),\(G_i=\sum_{j=0}^i \binom{i}{j}F_j\)
反演得到 \(F_i=\sum_{j=0}^i(-1)^{i-j}\binom{i}{j}G_j=\sum_{j=0}^i\frac{i!(-1)^{i-j}j^n}{j!(i-j)!}\)
但第二类斯特林数要求集合间互不区分,因此
同一行第二类斯特林数
意思就是求出 \(n\) 相同,\(m\) 不同的所有斯特林数。我们发现这个式子实际上可以卷积,用 NTT 计算就行。
码
//第二类斯特林数
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=167772161,g=3,invg=55924054,maxn=8e5+5;
int n,mx=1,lg,r[maxn],fac[maxn],inv[maxn],ans;
int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int C(int n,int m){
if(n<m) return 0;
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
struct polyn{
int a[maxn];
void ntt(int typ){
for(int i=0;i<mx;i++){
if(i<r[i]) swap(a[r[i]],a[i]);
}
for(int len=1;len<mx;len<<=1){
int now=qpow((typ==1)?g:invg,(mod-1)/(len<<1));
for(int i=0;i<mx;i+=(len<<1)){
int w=1;
for(int j=0;j<len;j++,w=w*now%mod){
int x=a[i+j],y=w*a[i+j+len]%mod;
a[i+j]=(x+y)%mod,a[i+j+len]=(x-y+mod)%mod;
}
}
}
if(typ==-1){
int iv=qpow(mx,mod-2);
for(int i=0;i<mx;i++) a[i]=a[i]*iv%mod;
}
}
}A,B;
signed main(){
scanf("%lld",&n);
fac[0]=inv[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
inv[n]=qpow(fac[n],mod-2);
for(int i=n;i>1;i--) inv[i-1]=inv[i]*i%mod;
while(mx<=n+n) mx<<=1,lg++;
for(int i=0;i<mx;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(lg-1));
int fl=1;
for(int i=0;i<=n;i++) A.a[i]=(fl*inv[i]+mod)%mod,fl*=-1;
for(int i=0;i<=n;i++) B.a[i]=qpow(i,n)*inv[i]%mod;
A.ntt(1),B.ntt(1);
for(int i=0;i<mx;i++) A.a[i]=A.a[i]*B.a[i]%mod;
A.ntt(-1);
for(int i=0;i<=n;i++) printf("%lld ",A.a[i]);
return 0;
}
同一列第二类斯特林数需要用到指数型生成函数等等,较为麻烦。
第一类斯特林数
写作 \(\begin{bmatrix}n\\ k\end{bmatrix}\) ,表示将 \(n\) 个两两不同的元素,划分成 \(k\) 个互不区分的非空轮换的方案数。也可以理解为将 \(n\) 个人,安排到 \(k\) 个相同圆桌上吃饭的方案数。
注意到,一个轮换就是一个圆形排列,两个可以通过旋转相互得到的轮换是等价的。(闲话:在我们推圆排列的式子时我们会发现,要对排列数除以 \(n\) ,其原因就在于一个圆排列可以转 \(n\) 次,在对应的序列上不同。但由于圆排列相当于没有首尾,所以它们是等价的。)
递推式
边界是 \(\begin{bmatrix}n\\ 0\end{bmatrix}=[n=0]\)。
证明依然考虑组合意义。对于一个新加入的元素,要么把它放在一个单独的轮换中,要么把他放到现有的轮换中(可以放到已有的任何一个元素的后面,因为我们是圆排列,所以钦定放到已有元素的后面是不重不漏的)。
通项公式
第一类斯特林数没有实用的通项公式没想到吧hhh
同一行第一类斯特林数
我们考虑构造同行第一类斯特林数的生成函数:
然后根据其递推公式
理解上就是,要注意一下,后面的那一项是 \(k-1\) 给 \(k\) 贡献,所以要乘上一个 \(x\) 。
得到
其中 \(x^{\overline{n}}\) 就是 \(x\) 的 \(n\) 次上升阶乘幂。
那我们求同行斯特林数就相当于求 \(x^{\overline{n}}\) 的展开形式。我们先学习上升幂下降幂再来求解这个问题。
上升幂与下降幂
上升(阶乘)幂 \(x^{\overline{n}}=\frac{(x+n-1)!}{(x-1)!}=\prod_{k=0}^{n-1}(x+k)\)
我们可以用这个恒等式将上升幂转化为普通幂
将普通幂转化为上升幂
下降(阶乘)幂\(x^{\underline{n}}=\frac{x!}{(x-n)!}=\prod_{k=0}^{n-1}(x-k)\)
将普通幂转化为下降幂
下降幂转化为普通幂
如果我们把上升幂写成 \(n^{\overline{m}}=\prod_{i=n}^{n+m-1} i\) ,下降幂写成 \(n^{\underline{m}}=\prod_{i=n-m+1}^n i\)。我们可以发现,这里的 \(n,m\) 是可以取到负数的。
上升幂和下降幂对于指数求和的展开形式,有:
这样的性质十分良好,它启示我们用倍增法求解一些问题。
考虑上升幂和下降幂之间的一些转化
考虑下降幂和组合数的一些性质。不难发现 \(n^{\underline{m}}\) 实际上就是 \(A_n^m\) ,所以有:
听说有一些推式子的化简,我们来看一下 UKE_Automation dalao 的博客。

所有以上的这些上升下降幂的东西在推式子时尤为关键。
同一行第一类斯特林数
还记得我们遗留下来的问题吗(
我们求同行斯特林数就相当于求 \(x^{\overline{n}}\) 的展开形式。
有了对上面上升幂性质的了解,我们可以直接 分治+NTT \(O(nlog^2n)\) 来做。
我们仍有单 \(log\) 做法:设 \(F_n(x)=x^{\overline{n}}=\sum_{i=0}^n a_ix^i\),就有,

然后后面的部分是卷积的形式,前后多项式又是可以相乘的,因此我们的时间复杂度为 \(T(n)=T(\frac{n}{2})+O(nlogn)=O(nlogn)\)
码
//第二类斯特林数
//这个要是再不在封装里面写多项式乘法就真的很难受了、、
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=167772161,g=3,invg=55924054,maxn=8e5+5;
int n,r[maxn],fac[maxn],inv[maxn];
int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
struct polyn{
int n; vector<int>a;
void reset(int m){ n=m,a.resize(m+1);}
void ntt(int mx,int typ){
reset(mx-1);
for(int i=0;i<mx;i++){
if(i<r[i]) swap(a[r[i]],a[i]);
}
for(int len=1;len<mx;len<<=1){
int now=qpow((typ==1)?g:invg,(mod-1)/(len<<1));
for(int i=0;i<mx;i+=(len<<1)){
for(int j=0,w=1;j<len;j++,w=w*now%mod){
int x=a[i+j],y=w*a[i+j+len]%mod;
a[i+j]=(x+y)%mod,a[i+j+len]=(x-y+mod)%mod;
}
}
}
if(typ==-1){
int iv=qpow(mx,mod-2);
for(int i=0;i<mx;i++) a[i]=a[i]*iv%mod;
}
}
polyn operator *(polyn y){
polyn x=*this,z;//this是指向自己的指针
int n=x.n+y.n,mx=1;
while(mx<=n) mx<<=1;
for(int i=0;i<mx;i++) r[i]=(r[i>>1]>>1)|((i&1)*(mx>>1));
x.ntt(mx,1),y.ntt(mx,1);
z.reset(mx-1);
for(int i=0;i<mx;i++) z.a[i]=x.a[i]*y.a[i]%mod;
z.ntt(mx,-1),z.reset(n);
return z;
}
};
polyn solve(int n){
if(n==1){
polyn f; f.reset(n);
f.a[1]=1; return f;
}
int m=n>>1;
polyn f=solve(m);
polyn g,h; g.reset(m),h.reset(m);
for(int i=0;i<=m;i++) g.a[i]=qpow(m,i)*inv[i]%mod,h.a[m-i]=f.a[i]*fac[i]%mod;
h=h*g,g.reset(m);
for(int i=0;i<=m;i++) g.a[i]=h.a[m-i]*inv[i]%mod;//因为这里是我们化简的式子,只能算偶数的情况
g=g*f,f.reset(n);
for(int i=0;i<=n;i++){
if(n&1) f.a[i]=(g.a[i]*(n-1)%mod+(i?g.a[i-1]:0))%mod;//要特判奇数的情况,这就是递推式
else f.a[i]=g.a[i];
}
return f;
}
signed main(){
scanf("%lld",&n);
fac[0]=inv[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
inv[n]=qpow(fac[n],mod-2);
for(int i=n;i>1;i--) inv[i-1]=inv[i]*i%mod;
polyn F=solve(n);
// printf("%lld\n",F.n);
for(int i=0;i<=n;i++) printf("%lld ",F.a[i]);
return 0;
}
例题
[省选联考 2020 A 卷] 组合数问题
只是又被数学题震撼住了。。首先看到数据范围就想到往 \(m\) 上转化。
看到一个有幂次的东西乘上一个组合数,考虑把这个幂次转化为下降幂,因为下降幂乘组合数会有一个很漂亮的东西(就是能把在幂次底数上的变量搞掉)。转化完之后把下降幂提出来,会发现后面成了一个很漂亮的二项式定理(其实还不太容易看出来。。但看到一个变量在指数上的东西乘一个组合数应该更加敏感)
总结一下:形如 \(x^a\) 的东西乘上一个组合数,若 \(x\) 为变量且 \(a\) 在组合数的式子中出现过,考虑转化为下降幂;若 \(a\) 为变量,考虑二项式定理。
[hdu4625] JZPTREE / [国家集训队] Crash 的文明世界
我们又是注意到n和k的范围,考虑直接把要求的式子的某一维转移到k上。
还是把幂次用斯特林数换掉,然后非常巧妙的是运用组合数和下降幂的关系,直接用递推公式拆开,f[x][i]表示x子树中 \(\sum_{i=0}^k \binom{dis}{i}\) 的值。换根的时候注意,必须是两个点各自的子树且相距1才能转移。
[TJOI / HEOI2016] 求和
暴力化简式子,瓶颈就是一个函数里面多余出来了别的参量,没法卷积了
真没想到就是直接在最开始把范围改掉,因为斯特林数j>i的情况反正都是0,所以直接算到n是不会影响的。。居然对通项公式也适用,手法需要积累
写的时候有i=0,i=1时不能直接用等比数列求和公式,特判一下。这个递推式有一个隐含的条件 \(0^0=1\),而对于 \(i>0,0^i=0\) 。
[bzoj5093] 图的价值
想过每个点单独算,但觉得不严谨。其实是严谨的,因为每个点都是有编号的,所以他们都是彼此独立的。
\(ans=n\sum_{i=0}^{n-1}\binom{n-1}{i}i^k 2^{\frac{n(n-1)}{2}-(n-1)}\)
意义是 \(n\) 个点独立计算,考虑枚举从每个点发出 \(i\) 条边,计算贡献,然后边的方案数。
推式子注意,下降幂可以直接写成组合数乘以阶乘的形式!!然后拆拆看。太棒了,抄式子多抄了一项,直接无法战胜。
有一个推式子的小技巧,可以用来推那个组合数和下降幂结合的东西(实在是记不住那个式子)。我们先考虑下降幂拆成组合数,然后使用 \(\binom{n}{k}\binom{k}{m}=\binom{n}{m}\binom{n-m}{k-m}\),就能推出来一个能用二项式定理的东西了。(最后化简出来就是需要求一个同行第二类斯特林数,再乘上依托就完了
[FJOI2016] 建筑师
我还以为我这辈子做不出来数学题了呢。。。
但我估计要是不在斯特林数题单里面我就很难想到了。考虑最终排列的形态,一定是形如中间有一个最高的,旁边依次是较低的几个但它们都高于自己到下一个比自己高的中间的这些部分。我们一定是要把最高的提出来的,那么就是圆排列,划分方案也就是第一类斯特林数。然后用组合数乘一下就行。
二项式反演与斯特林反演
二项式反演
形式零
形式一
形式二
其精髓就在于“至多/至少(钦定)”和“恰好”的转化。不知道用哪个式子的时候,从实际出发,考虑一下它应该从哪里开始容斥,即哪一头一定是正的。(在直接容斥和二项式反演之间犹豫的时候,考虑一下每种情况会在钦定处被多算几次,如果被多算组合数次,那就是二项式反演)
斯特林反演
引理:反转公式
形式一
形式二
- 挂个链接,二项式反演与斯特林反演
人家的证明还是写的太清楚了,本来想自己写,现在觉得没必要了
例题
[bzoj4665] 小 w 的喜糖
直接算钦定种类不同的很难算,难以避开要维护种类还剩的数量。必须考虑正难则反!!!也就是设 \(f_{i,j}\) 表示,到第 \(i\) 种糖,其中有 \(j\) 种钦定和原来相同,然后跑dp。然后我们用二项式反演算出 \(g(0)\) 表示恰好没有糖和原来相同即可。
P4859 已经没有什么好害怕的了
这个题从数据范围,到限制条件都和上一题很像。算是一个举一反三了。思路仍是用dp算出钦定答案的贡献,再二项式反演。
[bzoj4671] 异或图
跟前两道依然比较像,直接求解连通图很难求,但不妨考虑钦定有若干连通块,求出这个方案数然后再反演出恰好有1个连通块的情况。发现一次“钦定”的 \(f(m)\) 会把“恰好”的 \(g(i)\) 计算的次数就是把 \(i\) 划分成 \(m\) 个非空子集的方案数,即第二类斯特林数。
我们就有
我们计算钦定划分连通块的方案数时,由于数据范围小,我们直接暴搜。对于某些我们一定不能连的边,符合要求的方案一定满足这些边异或和为0。考虑线性基,当n个数插入线性基只成功了k个数时,异或和为0的方案数为 \(2^{n-k}\) 。

浙公网安备 33010602011771号