西雅图
18:14发布
西雅图
18:14发布
8°
东南风 4级
空气质量 无
相对湿度 90%
今天
中雨
6°/11°
周四
中雨
3°/10°
周五
2°/10°

斯特林数及其相关知识和应用

引言

在组合数学,Stirling数可指两类数,第一类Stirling数和第二类Stirling数,都是由18世纪数学家James Stirling提出的。
Stirling数有两种,第一类Stirling数和第二类Stirling数,它们自18世纪以来一直吸引许多数学家的兴趣,如欧拉、柯西、西尔沃斯特和凯莱等。后来哥本哈根(Copenhagen)大学的尼尔森(Niels Nielsen,1865-1931)提出了"Stirlingschen Zahlen erster Art" (第一类Stirling数)和"Stirlingschen Zahlen zweiter Art" (第二类Stirling数),首次把这两类数冠以「Stirling数」之名 。因为苏格兰数学家斯特林(J. Stirling, 1692-1770)首次发现这些数并说明了它们的重要性。
——来自于百度百科

此外,由于拉赫数与斯特林数关系密切,有时候也将拉赫数(Lah number)称为第三类斯特林数。

注:第二类斯特林数却在斯特林的相关著作和具体数学中被首先描述,同时也比第一类斯特林数常用得多。

自然幂,上升幂和下降幂

在介绍斯特林数之前,我先介绍一下自然幂,上升幂和下降幂,因为斯特林数的性质和这三种幂紧密相关。

自然幂符号及公式

mn=i=0n1m

上升幂符号及公式

mn¯=i=0n1(m+i)

下降幂符号及其公式

mn_=i=0n1(mi)

上升幂和下降幂的转换

(m)n¯=(1)nmn_

(m)n¯=(1)nmn_

另外

其实自然幂和上升幂,自然幂和下降幂之间是可以相互转换的,在之后的前两类斯特林数的性质中会讲述。

第一类斯特林数

介绍

第一类斯特林数(斯特林轮换数)[nm],表示将 n 个两两不同的元素,划分成 m 个互不区分的非空置换的方案数。

注:一个轮换是一个收尾相接的环形排列。两个可以通过旋转而互相得到的轮换是等价的(我们不认为两个可以通过翻转而相互得到的轮换等价)。

递推式

[nm]=[n1m1]+(n1)[n1m]

边界为:[n0]=[n=0]

运用组合意义证明:

  • 新加入的元素独立作为一个置换,方案数为 [n1m1]

  • 新加入的元素加入现有的置换当中,因为对任意长度为 l 的置换插入该置换的方案数为 l ,当前置换总长为 n1 ,所以方案数为 (n1)[n1m]

根据加法原理,将两式相加即可得到递推式。

性质

  1. n!=i=0n[ni]

理解:对于一个排列 P 我们可以将所有的 (pi,i) 连边,一个环则为一组轮换,每种排列一一每种置换。

证明:数学归纳法,请读者自行证明。

  1. mn¯=i=0n[ni]mi

证明:

  • 显然,在 n=0 的时候左右两边相等。
  • n=k 的时候两边相等,即 mk¯=i=0k[ki]mi
  • 则在 n=k+1 时:

=mk+1¯=(m+k)mk¯=(m+k)i=0k[ki]mi

=i=0k+1[k+1i]mi=i=0k+1([ki1]+k[ki])mi=i=0k+1[ki1]mi+ki=0k+1[ki]mi=i=0k[ki]mi+1+ki=0k[ki]mi=(m+k)i=0k[ki]mi=

  • mk+1¯=i=0k+1[k+1i]mi

  • 改命题由此得证。

  1. mn_=i=0n(1)ni[ni]mi

证明:

  • 由上式可得:

mn_=(1)n(1)nmn_=(1)n(m)n¯=i=0n[ni](m)i=(1)ni=0n[ni](m)i=(1)ni=0n(1)i[ni]mi=i=0n(1)ni[ni]mi

通项公式

第一类斯特林数目前没有实用的通项公式。

同一行第一类斯特林数的计算

由于 O(nlog2n) 的分治FFT的做法已经烂大街。

所以我们讲讲时间复杂度为 O(nlogn) 的做法(虽然还是逃不过多项式)。

考虑构造生成函数 F(x)n=i=0n1(x+i)=i=0naixi ,可以根据 mn¯=i=0n[ni]mi 得到其中的 ai=[ni]

显然 F(x)2n=F(x)nF(x+n)n

F(x+n)n=i=0nai(x+n)i=i=0naij=0n(ij)xjnij=i=0nj=in(ji)xinjiaj=i=0nj=inj!i!(ji)!xinjiaj=i=0n1i!(j=inajj!nji(ji)!)xi

我们可以通过左半部分系数得到右半部分系数由此得到全部的系数。

时间复杂度: T(n)=T(n2)+O(nlogn)=O(nlogn)

/**
 *    unicode: UTF-8
 *    name:    同一行第一类斯特林数的计算
 *    author:  wangjunrui
 *    created: 2022.07.03 周日 21:23:22 (Asia/Shanghai)
 **/
#include <algorithm>
#include <cstdio>
#define ll long long
#define lll __int128
#define ull unsigned ll
#define lowbit(x) (x & (-x))
template <typename T>
inline void read(T &x)
{
    x = 0;
    char s = (char)getchar();
    bool f = false;
    while (!(s >= '0' && s <= '9'))
    {
        if (s == '-')
            f = true;
        s = (char)getchar();
    }
    while (s >= '0' && s <= '9')
    {
        x = (x << 1) + (x << 3) + s - '0';
        s = (char)getchar();
    }
    if (f)
        x = (~x) + 1;
}
template <typename T, typename... T1>
inline void read(T &x, T1 &...x1)
{
    read(x);
    read(x1...);
}
template <typename T>
inline void ckmin(T &x, T y)
{
    if (x > y)
        x = y;
}
template <typename T>
inline void ckmax(T &x, T y)
{
    if (x < y)
        x = y;
}
using namespace std;
constexpr int N = 8e5 + 5;
constexpr int mod = 167772161;
constexpr int inv3 = 55924054;
inline ll quickpow(ll a, int b)
{
    ll res = 1;
    while (b)
    {
        if (b & 1)
            (res *= a) %= mod;
        (a *= a) %= mod;
        b >>= 1;
    }
    return res;
}
ll fac[N], ifac[N];
inline ll C(int n, int m)
{
    return fac[n] * fac[m] % mod * fac[n - m] % mod;
}
ll A[N], B[N];
int len, limit, rk[N];
inline void init(int all)
{
    len = 0, limit = 1;
    while (limit <= all)
    {
        limit <<= 1;
        ++len;
    }
    for (int i = 0; i < limit; ++i)
        rk[i] = (rk[i >> 1] >> 1) | ((i & 1) << (len - 1));
}
inline void NTT(ll *dp)
{
    for (int i = 0; i < limit; ++i)
        if (i < rk[i])
            swap(dp[i], dp[rk[i]]);
    for (int mid = 1; mid < limit; mid <<= 1)
    {
        ll gn = quickpow(3, (mod - 1) / (mid << 1));
        for (int j = 0; j < limit; j += mid << 1)
        {
            ll g = 1;
            for (int i = 0; i < mid; ++i, (g *= gn) %= mod)
            {
                ll x = dp[i + j], y = dp[i + j + mid] * g % mod;
                dp[i + j] = (x + y) % mod;
                dp[i + j + mid] = (x - y) % mod;
            }
        }
    }
}
inline void INTT(ll *dp)
{
    for (int i = 0; i < limit; ++i)
        if (i < rk[i])
            swap(dp[i], dp[rk[i]]);
    for (int mid = 1; mid < limit; mid <<= 1)
    {
        ll gn = quickpow(inv3, (mod - 1) / (mid << 1));
        for (int j = 0; j < limit; j += mid << 1)
        {
            ll g = 1;
            for (int i = 0; i < mid; ++i, (g *= gn) %= mod)
            {
                ll x = dp[i + j], y = dp[i + j + mid] * g % mod;
                dp[i + j] = (x + y) % mod;
                dp[i + j + mid] = (x - y) % mod;
            }
        }
    }
    ll inv = quickpow(limit, mod - 2);
    for (int i = 0; i < limit; ++i)
        (dp[i] *= inv) %= mod;
}
ll answer[N];
inline void solve(int n)
{
    if (n == 1)
    {
        answer[1] = 1;
        return;
    }
    int las = n / 2;
    solve(las);
    for (int i = 0; i <= las; ++i)
        A[las - i] = answer[i] * fac[i] % mod;
    ll power = 1;
    for (int i = 0; i <= las; ++i, (power *= las) %= mod)
        B[i] = power * ifac[i] % mod;
    init(las * 2);
    NTT(A), NTT(B);
    for (int i = 0; i < limit; ++i)
        (A[i] *= B[i]) %= mod;
    INTT(A);
    reverse(A, A + las + 1);
    for (int i = 0; i <= las; ++i)
        (A[i] *= ifac[i]) %= mod;
    fill(A + las + 1, A + limit, 0);
    fill(B + 0, B + limit, 0);
    init(n);
    NTT(answer), NTT(A);
    for (int i = 0; i < limit; ++i)
        (answer[i] *= A[i]) %= mod;
    INTT(answer);
    fill(A + 0, A + limit, 0);
    if (n & 1)
        for (int i = n; i >= 1; --i)
            answer[i] = (answer[i - 1] + (n - 1) * answer[i]) % mod;
}
signed main()
{
    int n;
    read(n);
    fac[0] = 1;
    for (int i = 1; i <= n; ++i)
        fac[i] = fac[i - 1] * i % mod;
    ifac[n] = quickpow(fac[n], mod - 2);
    for (int i = n; i >= 1; --i)
        ifac[i - 1] = ifac[i] * i % mod;
    if(n)
        solve(n);
    else
        answer[0] = 1;
    for (int i = 0; i <= n; ++i)
        printf("%lld ", (answer[i] + mod) % mod);
    putchar('\n');
    return 0;
}

同一列第一类斯特林数的计算

在路上了

第二类斯特林数

介绍

第二类斯特林数(斯特林子集数){nm} ,表示将 n 个两两不同的元素,划分为 m 个互不区分的非空子集的方案数。

递推式

{nm}={n1m1}+m{n1m}

边界为 {n0}=[n=0]

运用组合意义证明:

  • 将新元素单独放入一个子集,方案数为 {n1m1}

  • 新将新元素放入一个现有的非空子集,方案数为 m{n1m}

根据加法原理,将两式相加即可得到递推式。

性质

  1. mn=i=0n{ni}mi_

证明:

  • 显然,在 n=0 的时候左右两边相等。
  • n=k 的时候两边相等,即 mk=i=0k{ki}mi_
  • 则在 n=k+1 时:

=mk+1=mmk=mi=0k{ki}mi_

=i=0k+1{k+1i}mi_=i=0k+1({ki1}+i{ki})mi_=i=0k+1{ki1}mi_+i=0k+1i{ki}mi_=i=0k{ki}mi+1_+i=0ki{ki}mi_=i=0k(mi){ki}mi_+i=0ki{ki}mi_=mi=0k{ki}mi_=

  • mk+1¯=i=0k+1[k+1i]mi

  • 改命题由此得证。

  1. mn=i=0n(1)ni{ni}mi¯

证明:

  • 由上式可得:

mn=(1)n(m)n=(1)ni=0n{ni}(m)i_=(1)ni=0n{ni}(1)imi¯=i=0n(1)ni{ni}mi¯

通项公式

{nm}=i=0m(1)miini!(mi)!

证明:

我们用二项式反演。

fi 为表示将 n 个两两不同的元素,划分为 i 个互不相同的 非空子集 的方案数。

显然 fi=i!{ni}

考虑令 gi 为表示将 n 个两两不同的元素,划分为 i 个互不相同的子集 (可非空) 的方案数。

显然 gi=ni

我们考虑 fi 其实就是 gi 去掉所有的空子集,则我们只要钦定 gi 中那些是非空子集就可以写出 figi 的关系了:

gi=j=0i(ij)fj

根据二项式反演:

fi=j=0i(1)ij(ij)gj=j=0i(1)ij(ij)jn=j=0ii!(i1)ijjnj!(ij)!

{nm}=fmm!=i=0m(1)miini!(mi)!

同一行第二类斯特林数的计算

根据通项公式,可以发现这是卷积的形式,直接卷积计算即可。

/**
 *    unicode: UTF-8
 *    name:    同一行第二类斯特林数的计算
 *    author:  wangjunrui
 *    created: 2022.07.04 周一 23:35:00 (Asia/Shanghai)
 **/
#include <algorithm>
#include <cstdio>
#include <cstring>
#define ll long long
#define lll __int128
#define ull unsigned ll
#define lowbit(x) (x & (-x))
template <typename T>
inline void read(T &x)
{
    x = 0;
    char s = (char)getchar();
    bool f = false;
    while (!(s >= '0' && s <= '9'))
    {
        if (s == '-')
            f = true;
        s = (char)getchar();
    }
    while (s >= '0' && s <= '9')
    {
        x = (x << 1) + (x << 3) + s - '0';
        s = (char)getchar();
    }
    if (f)
        x = (~x) + 1;
}
template <typename T, typename... T1>
inline void read(T &x, T1 &...x1)
{
    read(x);
    read(x1...);
}
template <typename T>
inline void ckmin(T &x, T y)
{
    if (x > y)
        x = y;
}
template <typename T>
inline void ckmax(T &x, T y)
{
    if (x < y)
        x = y;
}
using namespace std;
constexpr int N = 8e5 + 5;
constexpr int mod = 167772161;
constexpr int inv3 = 55924054;
inline ll quickpow(ll a, int b)
{
    ll res = 1;
    while (b)
    {
        if (b & 1)
            (res *= a) %= mod;
        (a *= a) %= mod;
        b >>= 1;
    }
    return res;
}
int n;
ll fac[N], ifac[N];
int limit, len, rk[N];
ll A[N], B[N];
inline void init(int all)
{
    len = 0, limit = 1;
    while (limit <= all)
    {
        limit <<= 1;
        ++len;
    }
    for (int i = 0; i < limit; ++i)
        rk[i] = (rk[i >> 1] >> 1) | ((i & 1) << (len - 1));
}
inline void NTT(ll *dp)
{
    for (int i = 0; i < limit; ++i)
        if (i < rk[i])
            swap(dp[i], dp[rk[i]]);
    for (int mid = 1; mid < limit; mid <<= 1)
    {
        ll gn = quickpow(3, (mod - 1) / (mid << 1));
        for (int j = 0; j < limit; j += mid << 1)
        {
            ll g = 1;
            for (int i = 0; i < mid; ++i, (g *= gn) %= mod)
            {
                ll x = dp[i + j], y = dp[i + j + mid] * g % mod;
                dp[i + j] = (x + y) % mod;
                dp[i + j + mid] = (x - y) % mod;
            }
        }
    }
}
inline void INTT(ll *dp)
{
    for (int i = 0; i < limit; ++i)
        if (i < rk[i])
            swap(dp[i], dp[rk[i]]);
    for (int mid = 1; mid < limit; mid <<= 1)
    {
        ll gn = quickpow(inv3, (mod - 1) / (mid << 1));
        for (int j = 0; j < limit; j += mid << 1)
        {
            ll g = 1;
            for (int i = 0; i < mid; ++i, (g *= gn) %= mod)
            {
                ll x = dp[i + j], y = dp[i + j + mid] * g % mod;
                dp[i + j] = (x + y) % mod;
                dp[i + j + mid] = (x - y) % mod;
            }
        }
    }
    ll inv = quickpow(limit, mod - 2);
    for (int i = 0; i < limit; ++i)
        (dp[i] *= inv) %= mod;
}
signed main()
{
    read(n);
    fac[0] = 1;
    for (int i = 1; i <= n; ++i)
        fac[i] = fac[i - 1] * i % mod;
    ifac[n] = quickpow(fac[n], mod - 2);
    for (int i = n; i >= 1; --i)
        ifac[i - 1] = ifac[i] * i % mod;
    for (int i = 0; i <= n; ++i)
    {
        A[i] = quickpow(i, n) * ifac[i] % mod;
        B[i] = (i & 1 ? -1 : 1) * ifac[i];
    }
    init(n * 2);
    NTT(A), NTT(B);
    for (int i = 0; i < limit; ++i)
        (A[i] *= B[i]) %= mod;
    INTT(A);
    for (int i = 0; i <= n; ++i)
        printf("%lld ", (A[i] + mod) % mod);
    putchar('\n');
    return 0;
}

同一列第二类斯特林数的计算

在路上了

第三类斯特林数

介绍

第三类斯特林数(拉赫数)$L(n, m),通常指无符号拉赫数 L(n,m) ,表示将含有 n 个元素的集合拆分为 m 个非空线性有序子集的方法数目。

注: {(1,2,3)}{(2,3,1)}{(1,2),(3)}{(3),(1,2)}

拉赫数可以通过上升幂与下降幂之间的转化来定义:

无符号拉赫数

mn¯=i=0nL(n,i)mi_

mn_=i=0n(1)niL(n,i)mi¯

有符号拉赫数

mn¯=(1)ni=0nL(n,i)mi_

mn_=i=0n(1)iL(n,i)mi¯

通项公式

无符号拉赫数

L(n,m)=(n1m1)n!m!

有符号拉赫数

L(n,m)=(1)nL(n,m)=(1)n(n1m1)n!m!

递推式

由通项公式可得

L(n,m)=nm+1m(m1)L(n,m1)=(n+m1)L(n1,m)+L(n1,m1)

边界:
L(n,0)=0L(1,1)=1

性质

下面都是拉赫数的通项公式得出来的

L(n,m)=(n1m1)n!m!=(nm)(n1)!(m1)!=(nm)(n1m1)(nm)!

L(n,m)=(n1m1)n!m!=n!(n1)!m!(m1)!(nm)!=(n!m!)2mn(nm)!

三类斯特林数总结

斯特林反演

原来第一类斯特林数和第二类斯特林数之间还能进行反演

前置知识

  1. mn¯=i=0n[ni]mi

  2. mn_=i=0n(1)ni[ni]mi

  3. mn=i=0n{ni}mi_

  4. mn=i=0n(1)ni{ni}mi¯

具体证明见上面性质部分。

原理

mn=i=0n(1)ni{ni}mi¯=i=0n(1)ni{ni}j=0i[ij]mj=i=0nmij=in(1)nj{nj}[ji]

  • 由于这是一个恒等式,我们比对系数可得:

i=mn(1)ni{ni}[im]=[n=m]

  1. 和上面的推式子一样:

mn_=i=0n(1)ni[ni]mi=i=0n(1)ni[ni]j=0i{ij}mj_=i=0nmi_j=in(1)nj[nj]{ji}

  • 由于这是一个恒等式,我们比对系数可得:

i=mn(1)ni[ni]{im}=[n=m]

由此我们就有两个非常对称的恒等式:

i=mn(1)ni{ni}[im]=[n=m]

i=mn(1)ni[ni]{im}=[n=m]

第一种形式

fn=i=0n{ni}gign=i=0n(1)ni[ni]fi

证明:

gn=i=0ngi[n=i]=i=0ngij=in(1)nj[nj]{ji}=i=0n(1)ni[ni]j=0i{ij}gj=i=0n(1)ni[ni]fi

第二种形式

证明同上:

fn=i=0n[ni]gign=i=0n(1)ni{ni}fi

证明:

gn=i=0ngi[n=i]=i=0ngij=in(1)nj{nj}[ji]=i=0n(1)ni{ni}j=0i[ij]gj=i=0n(1)ni{ni}fi

应用

在路上了

作者:蒟蒻wjr
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

posted @   蒟蒻wjr  阅读(660)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?

点击右上角即可分享
微信分享提示