初中生都看得懂的快速上手斯特林数指南——从盒放球问题说起
前置知识
- 基础的组合计数知识
- NTT(非必要)
- 看到一长串的式子不会晕过去的能力
盒放球
盒放球问题可以描述为:
有 n n n个相同/不同的球, k k k个相同/不同的盒子,把 n n n个球放到盒子里,盒子允许/不允许为空,有多少种方案
稍微有点智商的人都知道,这一共有 2 × 2 × 2 = 8 2\times2\times2=8 2×2×2=8个问题
我们一个个来看
球相同,盒不同,不允许为空
这个非常简单,只需要使用隔板法就可以了,在 n − 1 n-1 n−1个空中插入 k − 1 k-1 k−1个板,把球隔成 k k k段,每一段对应地放到盒子中,所以有 ( n − 1 k − 1 ) \binom{n-1}{k-1} (k−1n−1)种
球相同,盒不同,允许为空
这回变成了允许为空,怎么办?
很简单,先在每个盒子里都放一个球,这样就转化为了球相同,盒不同,不允许为空
的情况了!一共有 ( n + k − 1 k − 1 ) \binom{n+k-1}{k-1} (k−1n+k−1)
球相同,盒相同,不允许/允许为空
这个需要使用到母函数(生成函数),不是本篇文章的重点,所以先不讲
球不同,盒不同,允许为空
这个应该没人不会吧……当然是 k n k^n kn种了
球不同,盒相同,不允许为空
emmm……
这是一个很有难度的问题,我们先看看如何递推吧
假设有 S ( n , k ) S(n,k) S(n,k)种,那么可能是前 n − 1 n-1 n−1个球就用了 k k k个盒子,那么我们把第 n n n个球放在 k k k个盒子中的任意一个即可;如果前 n − 1 n-1 n−1个球用了 k − 1 k-1 k−1个盒子,那么我们就把第 n n n个球放在第 k k k个盒子中
所以 S ( n , k ) = S ( n − 1 , k ) × k + S ( n − 1 , k − 1 ) S(n,k)=S(n-1,k)\times k+S(n-1,k-1) S(n,k)=S(n−1,k)×k+S(n−1,k−1)
话说我们不是要斯特林数吗?怎么还没讲?
别急,看看这个问题所使用的的字母: S S S,好像跟斯特林的首字母一样
不会吧不会吧,难道这就是传说中的斯特林数,原来斯特林数这么简单!!!对,您太强了,所以觉得简单,可以爆踩我这个菜鸡,妈妈再也不用担心你的学习啦!(大雾
接下来,我们暂且放下剩下的两种盒放球问题,来看看我们刚刚计算的 S ( n , k ) S(n,k) S(n,k),也就是第二类斯特林数的定义
第二类斯特林数
定义
将 n n n个元素分成 m m m个无标号的集合 S ( n , m ) = { n m } S(n,m)=\begin{Bmatrix}n\\m\end{Bmatrix} S(n,m)={
nm}
等等,啥叫无标号集合啊?如果用上面的盒放球问题来解释,就是盒子是相同的,如果严谨一点说,就是如果一堆集合调换顺序后和另一堆集合完全相同(所有对应的集合都相等),那么这两堆集合就是等价的
比如 { { 1 } , { 2 , 3 } } \{\{1\},\{2,3\}\} {
{
1},{
2,3}}和 { { 2 , 3 } , { 1 } } \{\{2,3\},\{1\}\} {
{
2,3},{
1}}就是等价的
看到这里,相信有的人还是理解不了第二类斯特林数,所以,我们举一个例子: { 3 2 } = 3 \begin{Bmatrix}3\\2\end{Bmatrix}=3 {
32}=3
三种方法如下:
- { { 1 } , { 2 , 3 } } \{\{1\},\{2,3\}\} { { 1},{ 2,3}}
- { { 2 } , { 1 , 3 } } \{\{2\},\{1,3\}\} { { 2},{ 1,3}}
- { { 3 } , { 1 , 2 } } \{\{3\},\{1,2\}\} { { 3},{ 1,2}}
现在清楚多了吧?
通项
第二类斯特林数的通项为 { n m } = 1 m ! ∑ i = 0 m ( − 1 ) i ( m i ) ( m − i ) n \begin{Bmatrix}n\\m\end{Bmatrix}=\frac{1}{m!}\sum \limits_{i=0}^m(-1)^i\binom{m}{i}(m-i)^n {
nm}=m!1i=0∑m(−1)i(im)(m−i)n
可以通过盒放球问题,来感性理解这个公式:我们考虑容斥,假设有 i i i个空盒,那么,我们先从 m m m个盒子中,选出 i i i个,剩下的盒子随便塞
最后,不要忘记无标号这个问题,所以我们需要除以 m ! m! m!
这个公式非常重要,它可以帮助我们用NTT计算出一整行的第二类斯特林数,这个后面会讲到
特殊值
在开始讲NTT求第二类斯特林数之前,为了让大家放松心情,我们先来看看第二类斯特林数的特殊值:
{ n 0 } = 0 , { n 1 } = 1 , { n n − 1 } = ( n 2 ) , { n n } = 1 \begin{Bmatrix}n\\0\end{Bmatrix}=0,\begin{Bmatrix}n\\1\end{Bmatrix}=1,\begin{Bmatrix}n\\n-1\end{Bmatrix}=\binom{n}{2},\begin{Bmatrix}n\\n\end{Bmatrix}=1 {
n0}=0,{
n1}=1,{
nn−1}=(2n),{
nn}=1
自己想吧,太简单了,不证了
计算第二类斯特林数
Warning!Warning!Warning!前方高能,请没学过NTT迅速跳过这一节
好的,我们开始推式子吧!
{ n m } = 1 m ! ∑ i = 0 m ( − 1 ) i ( m i ) ( m − i ) n = 1 m ! ∑ i = 0 m ( − 1 ) i m ! i ! ( m − i ) ! ( m − i ) n = ∑ i = 0 m ( − 1 ) i i ! ( m − i ) n ( m − i ) ! \begin{Bmatrix}n\\m\end{Bmatrix}=\frac{1}{m!}\sum \limits_{i=0}^m(-1)^i\binom{m}{i}(m-i)^n=\frac{1}{m!}\sum \limits_{i=0}^m(-1)^i\frac{m!}{i!(m-i)!}(m-i)^n=\sum \limits_{i=0}^m\frac{(-1)^i}{i!}\frac{(m-i)^n}{(m-i)!} {
nm}=m!1i=0∑m(−1)i(im)(m−i)n=m!1i=0∑m(−1)ii!(m−i)!m!(m−i)n=i=0∑mi!(−1)i(m−i)!(m−i)n
看,这是什么?是不是一个漂亮的卷积式?
还没看出来?设 f ( x ) = ∑ i ⩾ 0 ( − 1 ) i i ! x i , g ( x ) = ∑ i ⩾ 0 i n i ! x i f(x)=\sum \limits_{i\geqslant 0} \frac{(-1)^i}{i!}x^i,g(x)=\sum\limits_{i\geqslant 0}\frac{i^n}{i!}x^i f(x)=i⩾0∑i!(−1)ixi,g(x)=i⩾0∑i!inxi,则 { n m } \begin{Bmatrix}n\\m\end{Bmatrix} {
nm}就是 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x)的卷积
所以,我们就可以愉快地使用逆天塔(NTT)了!
模板题代码:
#include<bits/stdc++.h>
#define ljc 167772161
#define int long long
#define ll long long
using namespace std;
int n,m,k,r[800006],lim,w[800006],a[800006],b[800006],inv[800006];
inline ll fast_pow(ll a,ll b,ll p){
ll t=1;a%=p;
while (b){
if (b&1) t=t*a%p;
b>>=1;a=a*a%p;
}
return t;
}
inline void NTT(ll f[],int lim,int id){
for (int i=0;i<lim;i++){
if (i<r[i]) swap(f[r[i]],f[i]);
}
w[0]=1;
for (int len=1;len<lim;len<<=1){
ll gen=fast_pow(3,(ljc-1)/(len<<1)*id+ljc-1,ljc);
for (int i=1;i<len;i++) w[i]=w[i-1]*gen%ljc;
for (int i=0;i<lim;i+=len<<1){
ll *f1=f+i,*f2=f1+len;
for (int j=0;j<len;j++){
ll x=f1[j]%ljc,y=f2[j]*w[j]%ljc;
f1[j]=(x+y)%ljc;f2[j]=(x-y+ljc)%ljc;
}
}
}
if (id==1) return;
ll Inv=fast_pow(lim,ljc-2,ljc);
for (int i=0;i<lim;i++) f[i]=f[i]*Inv%ljc;
}
signed main(){
cin>>n;n++;
inv[0]=inv[1]=1;
for (int i=2;i<n;i++) inv[i]=(ljc-(ljc/i)*inv[ljc%i]%ljc)%ljc;
for (int i=1;i<n;i++) inv[i]=inv[i-1]*inv[i]%ljc;
for (int i=0,one=1;i<n;one=ljc-one,i++){
a[i]=one*inv[i]%ljc;b[i]=fast_pow(i,n-1,ljc)*inv[i]%ljc;
}
int lim=1,len=0;
while (lim<=(n<<1)) lim<<=1,len++;
for (int i=0;i<lim;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(len-1));
NTT(a,lim,1);NTT(b,lim,1);
for (int i=0;i<lim;i++) a[i]=a[i]*b[i]%ljc;
NTT(a,lim,-1);
for (int i=0;i<n;i++) printf("%lld ",a[i]);
}
自然数幂和
你有没有见过这样的题目:让你求 ∑ i = 1 n i k \sum\limits_{i=1}^ni^k i=1∑nik的题目?
你可能会想:这不简单吗,每次使用快速幂不就好了嘛
不!没有那么简单! n ⩽ 1 0 9 , k ⩽ 5000 n\leqslant 10^9,k\leqslant 5000 n⩽109,k⩽5000的时候你还能用快速幂吗?不能!不要告诉我你要分段打表
但是!第二类斯特林数可以!
这只能说明:斯特林数(停顿)行!快速幂(停顿)不行!我爱斯特林数!老伏拉夫了
那斯特林数到底怎么行了呢?我们要从一个假快速幂讲起
快速幂?
n m = ∑ i = 0 m { m i } n i ‾ n^m=\sum \limits_{i=0}^{m} \begin{Bmatrix}m\\i\end{Bmatrix} n^{\underline{i}} nm=i=0∑m{
mi}ni
其中 n i ‾ n^{\underline{i}} ni表示 n n n的 i i i次下降幂,即 n i ‾ = ∏ k = 0 i − 1 ( n − k ) n^{\underline{i}}=\prod \limits_{k=0}^{i-1}(n-k) ni=k=0∏i−1(n−k)
证明:
考虑归纳证明,当 m = 1 m=1 m=1时, ∑ i = 0 m { m i } n i ‾ = { 1 1 } n = n \sum \limits_{i=0}^{m} \begin{Bmatrix}m\\i\end{Bmatrix} n^{\underline{i}}=\begin{Bmatrix}1\\1\end{Bmatrix} n=n i=0∑m{
mi}ni={
11}n=n
假设 m = k m=k m=k时成立
当 m = k + 1 m=k+1 m=<