从球盒问题到第二类斯特林数

斯特林数是组合数学中一类特殊的数,有着广泛的应用,本文主要讨论第二类斯特林数的推导,性质与应用。

从球盒问题说起

组合问题最基础的模型就是球盒问题了。球盒问题即为 n n 个球放入 m 个盒子的方案数。众所周知,球盒问题有 2×2×2=8 2 × 2 × 2 = 8 种类型,分别为:

  1. 球相同,盒子相同,不可以有空盒子。
  2. 球相同,盒子相同,可以有空盒子。
  3. 球相同,盒子不同,不可以有空盒子。
  4. 球相同,盒子不同,可以有空盒子。
  5. 球不同,盒子相同,不可以有空盒子。
  6. 球不同,盒子相同,可以有空盒子。
  7. 球不同,盒子不同,不可以有空盒子。
  8. 球不同,盒子不同,可以有空盒子。

大量组合数学问题都源于这 8 8 种基础的模型,让我们一种一种讨论。

1. 球相同,盒子相同,不可以有空盒子。

这个问题可以转化为整数的 m 拆分数,即把 n n 分解成 m 个数相加的方案数,记为 pk(n) p k ( n ) 。 有如下递推式成立:

pk(n)=pk1(n1)+pk(nk) p k ( n ) = p k − 1 ( n − 1 ) + p k ( n − k )

其意义为:分别考虑含有 1 1 的拆分和不含有 1 的拆分。
边界情况: pn(n)=p1(n)=1 p n ( n ) = p 1 ( n ) = 1

2. 球相同,盒子相同,可以有空盒子。

类似上一种情况,我们也可以用整数拆分数来表示。
我们枚举拆分出数的个数,可以得到下式:

p(n,m)=i=1min(n,m)pi(n) p ( n , m ) = ∑ i = 1 min ( n , m ) p i ( n )

特别的,对于 mn m ≥ n 的情况,我们可以用整数拆分数 p(n) p ( n ) 来表示答案,即把 n n 分解成若干个数相加的方案数。
p(n)=i=1npi(n)

对于整数拆分数,我们还可以用生成函数的方法来计算。我们枚举每一个数出现的次数,即可得到整数拆分数的生成函数:
n=0p(n)xn=i=1(1+xi+x2i+x3i+) ∑ n = 0 ∞ p ( n ) x n = ∏ i = 1 ∞ ( 1 + x i + x 2 i + x 3 i + … )

n=0p(n)xn=i=111xi ∑ n = 0 ∞ p ( n ) x n = ∏ i = 1 ∞ 1 1 − x i

3. 球相同,盒子不同,不可以有空盒子。

这是一种比较简单的情况。我们可以用隔板法解决,转化为在 n n 个球中间 n1 个空隙插入 m1 m − 1 个隔板。方案数为:

(n1m1) ( n − 1 m − 1 )

4. 球相同,盒子不同,可以有空盒子。

类似上一种情况,我们也可以用隔板法,不同的是,这种情况隔板可以相邻。我们有两种思路,一种是预先在每个盒子种放入一个球(即球数增加 m m 个)转化为不可以有空盒子的情况,另一种方案是考虑一个 01 序列, 0 0 代表球,1 代表隔板,隔板共 m1 m − 1 个。即在长 n+m1 n + m − 1 01 01 序列选出 m1 m − 1 个位置放 1 1 , 其余放 0 的方案数。两种思路都可以得到下式:

(n+m1m1) ( n + m − 1 m − 1 )

5. 球不同,盒子相同,不可以有空盒子。

这个问题即为 n n 个元素划分为 k 个等价集合的方案数,这就是我们今天的重点——第二类斯特林数的定义。
第二类斯特林数通常记作 S(n,m) S ( n , m ) {nm} { n m }
其递推式为:

{nm}={n1m1}+m{n1m} { n m } = { n − 1 m − 1 } + m { n − 1 m }

其意义为:考虑第 n n 个物品,n可以单独构成一个非空集合,此时前 n1 n − 1 个物品构成 m1 m − 1 个非空的不可辨别的集合,有 {n1m1} { n − 1 m − 1 } 种方法;也可以前 n1 n − 1 种物品构成 m m 个非空的不可辨别的集合,第 n 个物品放入任意一个中,这样有 m{n1m} m { n − 1 m } 种方法。

6. 球不同,盒子相同,可以有空盒子。

对于这一种情况,我们可以枚举几个盒子非空,即为:

i=1min(n,m){ni} ∑ i = 1 min ( n , m ) { n i }

7. 球不同,盒子不同,不可以有空盒子。

对于这一种情况,我们发现,在第 5 5 种情况里的每一个方案,我们把每一个盒子标号,就对应着 m! 种方案。所以这种情况的答案为:

{nm}m! { n m } m !

8. 球不同,盒子不同,可以有空盒子。

终于到了最后一种情况,也是最简单的一种。
对于这种情况,我们可以枚举每一个球放在哪个盒子里,就得到了答案:

mn m n

当然我们也可以枚举哪一些盒子非空,就得到了一个重要的等式:
mn=i=0m(mi){ni}i! m n = ∑ i = 0 m ( m i ) { n i } i !

关于这个等式的应用将在之后介绍。

以上我们解决了球盒问题的所有 8 8 种情况,现在看来,是不是很 naive 呢?

第二类斯特林数

第二类斯特林数是 n n 个元素划分为 k 个等价集合的方案数。也就是之前介绍的球盒问题的第 5 5 种类型。
第二类斯特林数除了解决组合问题,还有一些神奇的性质。
第二类斯特林数可以使用下降幂来表示数的幂,即满足下式:

xn=k=0n{nk}xk_

其中 xk=x(x1)(x2)(xk+1)=x!(xk)! x k _ = x ( x − 1 ) ( x − 2 ) … ( x − k + 1 ) = x ! ( x − k ) !
这个式子可以使用数学归纳法证明,但是我们惊奇的发现,他所表示的意义即为 球不同,盒子不同,可以有空盒子 中我们发现的等式!
把下降幂用组合数替代更直观一些:

xn=k=0n{nk}x!k!(xk)!k!=k=0n{nk}(xk)k! x n = ∑ k = 0 n { n k } x ! k ! ( x − k ) ! k ! = ∑ k = 0 n { n k } ( x k ) k !

我们还可以利用这个性质,再次得到第二类斯特林数的递推公式:

转自riteme的博客

np1np=nnp1=k=0p1{p1k}nk=nk=0p1{p1k}nk=k=0p1{p1k}nnk=k=0p1{p1k}(nk+k)nk=k=0p1{p1k}nk+1+k=0p1k{p1k}nk=k=1p{p1k1}nk+k=0p1k{p1k}nk n p − 1 = ∑ k = 0 p − 1 { p − 1 k } n k _ n p = n ⋅ n p − 1 = n ∑ k = 0 p − 1 { p − 1 k } n k _ = ∑ k = 0 p − 1 { p − 1 k } ⋅ n ⋅ n k _ = ∑ k = 0 p − 1 { p − 1 k } ⋅ ( n − k + k ) ⋅ n k _ = ∑ k = 0 p − 1 { p − 1 k } n k + 1 _ + ∑ k = 0 p − 1 k { p − 1 k } n k _ = ∑ k = 1 p { p − 1 k − 1 } n k _ + ∑ k = 0 p − 1 k { p − 1 k } n k _

我们就再次得到了第二类斯特林数的递推式:
{pk}=k{p1k}+{p1k1} { p k } = k { p − 1 k } + { p − 1 k − 1 }

现在,你应该对第二类斯特林数有了深入的理解了。

斯特林反演

根据第二类斯特林数的递推式,我们已经可以以 O(n2) O ( n 2 ) 的复杂度求第二类斯特林数了。那么对于 n,m n , m 比较大的情况,我们应该怎么办呢?这里介绍一个技巧——斯特林反演。

斯特林反演的公式如下:

{nj}=1j!k=0j(1)jk(jk)kn { n j } = 1 j ! ∑ k = 0 j ( − 1 ) j − k ( j k ) k n

看起来十分玄学,我们需要想办法证明这个式子。
我们首先对于等号两边乘以阶乘:
{nj}j!=k=0j(1)jk(jk)kn { n j } j ! = ∑ k = 0 j ( − 1 ) j − k ( j k ) k n

考虑左式的组合意义: {nm}m! { n m } m ! 表示球盒问题中 球不同,盒子不同,不可以有空盒子 的方案数。
我们考虑用另一种思路求解这个组合问题:
考虑容斥原理,没有空盒子的方案数 = 总方案数 - 至少一个空盒子的方案数 + 至少两个个空盒子的方案数 - 至少三个空盒子的方案数……

k=0m(1)mk(mk)kn ∑ k = 0 m ( − 1 ) m − k ( m k ) k n

恰好与等号右边相同!
于是我们证明了斯特林反演。
我们把斯特林反演的式子展开:
{nj}=1j!k=0j(1)jkj!k!(jk)!kn { n j } = 1 j ! ∑ k = 0 j ( − 1 ) j − k j ! k ! ( j − k ) ! k n

{nj}=k=0j(1)jk(jk)!knk! { n j } = ∑ k = 0 j ( − 1 ) j − k ( j − k ) ! k n k !

恰好是一个卷积的形式。于是我们可以用 FFT(NTT)以 O(nlog2n) O ( n log 2 ⁡ n ) 的复杂度算出一行的第二类斯特林数。

例题

BZOJ 2159 Crash 的文明世界
题意:

给定一棵 n n 个点的树和一个常数 k , 对于每个 i i , 求

S(i)=j=1ndist(i,j)k

n50000,k150 n ≤ 50000 , k ≤ 150

题解:

常见套路:先大力把幂转为下降幂,再转为组合数:

S(i)=j=1nl=0k{kl}dist(i,j)l=l=0k{kl}j=1ndist(i,j)l=l=0k{kl}l!j=1n(dist(i,j)l)(1)(2)(3) (1) S ( i ) = ∑ j = 1 n ∑ l = 0 k { k l } d i s t ( i , j ) l _ (2) = ∑ l = 0 k { k l } ∑ j = 1 n d i s t ( i , j ) l _ (3) = ∑ l = 0 k { k l } l ! ∑ j = 1 n ( d i s t ( i , j ) l )

然后根据组合数的递推公式
(nm)=(n1m1)+(n1m) ( n m ) = ( n − 1 m − 1 ) + ( n − 1 m )

就可以 O(k) O ( k ) 从一个点转移到另一个点了。
总时间复杂度 O(nk) O ( n k )

My Code:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <vector>
#define inf 0x3f3f3f3f

using namespace std;
typedef long long ll;
const ll mod = 10007;

struct edge{
    int to, nxt;
}e[100005];

int h[50005], cnt;

void addedge(int x, int y){
    cnt++; e[cnt].to = y; e[cnt].nxt = h[x]; h[x] = cnt;
    cnt++; e[cnt].to = x; e[cnt].nxt = h[y]; h[y] = cnt;    
}
ll f[50005][151];
int n, k, L;

void dfs(int x, int fa){
    f[x][0] = 1;
    for(int i = h[x]; i; i = e[i].nxt){
        if(e[i].to == fa) continue;
        dfs(e[i].to, x);
        f[x][0] = (f[x][0] + f[e[i].to][0]) % mod;
        for(int j = 1; j <= k; j ++){
            f[x][j] = (f[x][j] + f[e[i].to][j] + f[e[i].to][j - 1]) % mod;
        }
    }
}

void dfs2(int x, int fa){
    for(int i = h[x]; i; i = e[i].nxt){
        if(e[i].to == fa) continue;
        for(int j = k; j >= 2; j --){
            f[e[i].to][j] = (f[x][j] + f[x][j - 1] - 2 * f[e[i].to][j - 1] - f[e[i].to][j - 2] + mod * 3) % mod;
        }
        f[e[i].to][1] = (f[x][1] + f[x][0] - 2 * f[e[i].to][0] + mod + mod) % mod;
        f[e[i].to][0] = f[x][0];
        dfs2(e[i].to, x);
    }
}
ll S[205][205];
ll fac[205];
int main(){
    scanf("%d%d%d", &n, &k, &L);
    int now, a, b, q;
    scanf("%d%d%d%d", &now, &a, &b, &q);
    for(int i = 1; i < n; i ++){
        now = (now * a + b) % q;
        int tmp=((i<L)?i:L);
        int u=i-now%tmp; int v=i+1;
        addedge(u, v);
    }
    dfs(1, 0);
    dfs2(1, 0);
    S[0][0] = 1;
    for(int i = 1; i <= k; i ++){
        S[i][0] = 0;
        for(int j = 1; j <= i; j ++){
            S[i][j] = (S[i - 1][j - 1] + j * S[i - 1][j]) % mod;
        }
    }
    fac[0] =1;
    for(int i = 1; i <= k; i ++){
        fac[i] = fac[i - 1] * i % mod;
    }
    for(int i = 1; i <= n; i ++){
        ll ans = 0;
        for(int j = 0; j <= k; j ++){
            ans = (ans + S[k][j] * fac[j] % mod * f[i][j])% mod;
        }
        printf("%lld\n", ans);
    }
    return 0;
}
HNOI2015 省队集训 原题的价值
题意:

定义一个无向图的权值为所有节点的度数的 k k 次方之和. 求所有 n个点的简单无向图(无重边无自环)的权值之和, 对 998244353 998244353 取模。
n109,k2×105 n ≤ 10 9 , k ≤ 2 × 10 5

题解:

由于每个点是等价的,我们不妨考虑一个点对答案的贡献,最后乘 n n 即为答案。
我们枚举这个点连出去的 n1 条边是否存在,其他的部分(一个 n1 n − 1 个点的无向图)与这个点的度数无关,所以再乘上 n1 n − 1 个点的简单无向图的数量。所以答案为:

(i=0n1(n1i)ik)2(n1)(n2)2n ( ∑ i = 0 n − 1 ( n − 1 i ) i k ) 2 ( n − 1 ) ( n − 2 ) 2 n

问题转化为求
i=0n(ni)ik ∑ i = 0 n ( n i ) i k

i=0n(ni)ik=i=0n(ni)j=0k{kj}ij=i=0n(ni)j=0k{kj}(ij)j!=j=0k{kj}j!i=0n(ni)(ij)=j=0k{kj}j!i=0nn!i!(ni)!i!j!(ij)!=j=0k{kj}j!i=0nn!j!(nj)!(nj)!(ni)!(ij)!=j=0k{kj}j!(nj)i=jn(njij)=j=0k{kj}nj2nj(4)(5)(6)(7)(8)(9)(10) (4) ∑ i = 0 n ( n i ) i k = ∑ i = 0 n ( n i ) ∑ j = 0 k { k j } i j _ (5) = ∑ i = 0 n ( n i ) ∑ j = 0 k { k j } ( i j ) j ! (6) = ∑ j = 0 k { k j } j ! ∑ i = 0 n ( n i ) ( i j ) (7) = ∑ j = 0 k { k j } j ! ∑ i = 0 n n ! i ! ( n − i ) ! i ! j ! ( i − j ) ! (8) = ∑ j = 0 k { k j } j ! ∑ i = 0 n n ! j ! ( n − j ) ! ( n − j ) ! ( n − i ) ! ( i − j ) ! (9) = ∑ j = 0 k { k j } j ! ( n j ) ∑ i = j n ( n − j i − j ) (10) = ∑ j = 0 k { k j } n j _ 2 n − j

这样已经可以 O(k2) O ( k 2 ) 的递推斯特林数求解了。 弱化版提交地址
观察到我们只需要一行斯特林数,我们可以用斯特林反演 + NTT 在 O(nlog2n) O ( n log 2 ⁡ n ) 的时间求出答案。

My Code:
#include <bits/stdc++.h>
#define MAXN 524288
using namespace std;
typedef long long ll;
const ll mod = 998244353;

ll qpow(ll a, ll b){
    ll ret = 1;
    for(; b; b >>= 1, a = a * a % mod){
        if(b & 1) ret = ret * a % mod;
    }
    return ret;
}

ll inv3, pw3[MAXN + 1], ipw3[MAXN + 1];
ll fac[MAXN], ifac[MAXN];
int N;
void pre(){
    for(int i = 1; i <= N; i <<= 1) pw3[i] = qpow(3, (mod - 1) / i);
    inv3 = qpow(3, mod - 2);
    for(int i = 1; i <= N; i <<= 1) ipw3[i] = qpow(inv3, (mod - 1) / i);
    fac[0] = 1;
    for(int i = 1; i < N; i ++) fac[i] = fac[i - 1] * i % mod;
    ifac[N - 1] = qpow(fac[N - 1], mod - 2);
    for(int i = N - 1; i >= 1; i --) ifac[i - 1] = ifac[i] * i % mod;
}

void bit_reverse(ll *r, int N){
    for(int i = 0, j = 0; i < N; i ++){
        if(i < j) swap(r[i], r[j]);
        for(int l = N >> 1; (j ^= l) < l; l >>= 1);
    }
}

void NTT(ll *r, int N, int flag){
    bit_reverse(r, N);
    for(int i = 2; i <= N; i <<= 1){
        int m = i >> 1;
        for(int j = 0; j < N; j += i){
            ll w = 1, wn = pw3[i];
            if(flag == -1) wn = ipw3[i];
            for(int k = 0; k < m; k ++){
                ll z = w * r[j + k + m] % mod;
                r[j + k + m] = (r[j + k] - z + mod) % mod;
                r[j + k] = (r[j + k] + z) % mod;
                w = w * wn % mod;
            }
        }
    }
    if(flag == -1){
        ll invn = qpow(N, mod - 2);
        for(int i = 0; i < N; i ++){
            r[i] = r[i] * invn % mod;
        }
    }
}

ll a[MAXN], b[MAXN];
int n, k;
int main(){
    scanf("%d%d", &n, &k);
    N = 1;
    while(N < 2 * k) N <<= 1;
    pre();
    for(int i = 0; i <= k; i ++){
        if(i & 1) a[i] = mod - 1;
        else a[i] = 1;
        a[i] = a[i] * ifac[i] % mod;
        b[i] = qpow(i, k) * ifac[i] % mod;
    }
    NTT(a, N, 1);
    NTT(b, N, 1);
    for(int i = 0; i < N; i ++) a[i] = a[i] * b[i] % mod;
    NTT(a, N, -1);
    n--;
    ll ans = 0, tmp = qpow(2, n) % mod, inv2 = qpow(2, mod - 2);
    for(int i = 0; i <= k; i ++){
        ans = (ans + tmp * a[i]) % mod;
        tmp = tmp * (n - i) % mod * inv2 % mod;
    }
    n++;
    ans = ans * qpow(2, 1ll * (n - 1) * (n - 2) / 2) % mod * n % mod;
    printf("%lld\n", ans);
    return 0;
}


参考资料:

斯特灵数 - 维基百科
差分序列与Stirling数 - riteme的博客

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值