斯特林数:组合划分的递归经典,一二两类全解

在组合计数的世界里,如果说卡特兰数是刻画「栈式结构」的代名词,那么斯特林数便是定义「划分问题」的基石。同样源于递归思想,同样拥有丰富的组合意义,第一类与第二类斯特林数分别解决了轮换划分集合划分两大核心问题,是算法竞赛与组合数学中不可或缺的工具。

本文将从组合意义出发,推导递推公式,讲解典型应用,并给出可直接复用的代码实现,带你系统掌握两类斯特林数。

一、第一类斯特林数:轮换的计数

第一类斯特林数(Stirling numbers of the first kind)分为无符号有符号两种,其中无符号第一类斯特林数的组合意义最直观,也是算法题中的常用形式。

1.1 组合意义与定义

无符号第一类斯特林数通常记作 nk\begin{bmatrix}n\\k\end{bmatrix}nk 或 s(n,k)s(n,k)s(n,k),表示:

将 nnn 个不同的元素,划分为 kkk 个非空**轮换(环排列)**的方案数。

所谓轮换,即环形排列:旋转后完全重合的排列视为同一种。例如元素 {a,b,c}\{a,b,c\}{a,b,c} 构成的轮换中,a,b,ca,b,ca,b,cb,c,ab,c,ab,c,ac,a,bc,a,bc,a,b 是同一个轮换,而 a,c,ba,c,ba,c,b 是另一个不同的轮换。

有符号第一类斯特林数则为:

ss(n,k)=(−1)n−k⋅s(n,k)s_s(n,k) = (-1)^{n-k} \cdot s(n,k)ss(n,k)=(−1)n−k⋅s(n,k)

主要用于下降阶乘的多项式展开,算法中较少直接使用。

1.2 递推公式与推导

第一类斯特林数满足如下递推关系:

s(n,k)=s(n−1,k−1)+(n−1)⋅s(n−1,k)s(n, k) = s(n-1, k-1) + (n-1) \cdot s(n-1, k)s(n,k)=s(n−1,k−1)+(n−1)⋅s(n−1,k)

组合意义推导

考虑第 nnn 个元素的归属,分为两种互斥的情况:

  1. 单独成轮换 :第 nnn 个元素自己构成一个轮换,剩下的 n−1n-1n−1 个元素划分为 k−1k-1k−1 个轮换,方案数为 s(n−1,k−1)s(n-1, k-1)s(n−1,k−1)。
  2. 插入已有轮换 :第 nnn 个元素插入到前 n−1n-1n−1 个元素形成的 kkk 个轮换中。对于任意一个包含 mmm 个元素的轮换,有 mmm 个不同的插入位置(每个元素后面都可以插入),因此 n−1n-1n−1 个元素共有 n−1n-1n−1 个插入位置,方案数为 (n−1)⋅s(n−1,k)(n-1) \cdot s(n-1, k)(n−1)⋅s(n−1,k)。

两种情况相加即得到递推公式。

边界条件

  • s(0,0)=1s(0, 0) = 1s(0,0)=1(0个元素划分为0个轮换,空方案)
  • s(n,0)=0(n>0)s(n, 0) = 0 \quad (n > 0)s(n,0)=0(n>0)
  • s(0,k)=0(k>0)s(0, k) = 0 \quad (k > 0)s(0,k)=0(k>0)
  • s(n,n)=1s(n, n) = 1s(n,n)=1(每个元素单独成轮换)
  • s(n,1)=(n−1)!s(n, 1) = (n-1)!s(n,1)=(n−1)!(nnn 个元素构成1个轮换,即环排列数)

例如:s(4,2)=s(3,1)+3⋅s(3,2)=2+3×3=11s(4,2) = s(3,1) + 3\cdot s(3,2) = 2 + 3\times 3 = 11s(4,2)=s(3,1)+3⋅s(3,2)=2+3×3=11。

1.3 典型应用场景

  1. 排列的轮换分解

    任意一个 nnn 元排列都可以唯一分解为若干个互不相交的轮换的乘积。恰好包含 kkk 个轮换的 nnn 元排列的个数,就等于无符号第一类斯特林数 s(n,k)s(n,k)s(n,k)。

  2. 圆桌排列问题

    nnn 个人围坐在 kkk 张完全相同的圆桌旁,每张桌子至少坐1人,旋转后相同视为同一种坐法,总方案数为 s(n,k)s(n,k)s(n,k)。

  3. 上升阶乘展开

    上升阶乘 xn‾=x(x+1)(x+2)⋯(x+n−1)x^{\overline{n}} = x(x+1)(x+2)\cdots(x+n-1)xn=x(x+1)(x+2)⋯(x+n−1) 可以展开为第一类斯特林数的形式:

    xn‾=∑k=0ns(n,k)⋅xkx^{\overline{n}} = \sum_{k=0}^n s(n,k) \cdot x^kxn=k=0∑ns(n,k)⋅xk

二、第二类斯特林数:集合的划分

第二类斯特林数(Stirling numbers of the second kind)是组合计数中更常用的一类,记作 {nk}\begin{Bmatrix}n\\k\end{Bmatrix}{nk} 或 S(n,k)S(n,k)S(n,k),核心解决集合划分问题。

2.1 组合意义与定义

第二类斯特林数表示:

将 nnn 个不同的元素,划分为 kkk 个非空、无序的集合的方案数。

注意两个关键点:

  • 集合是无序的:交换两个集合的顺序不算新的方案;
  • 集合内部元素无顺序。

如果集合是有标号的(例如不同的盒子),则结果需要乘以 k!k!k!。

2.2 递推公式与推导

第二类斯特林数满足如下递推关系:

S(n,k)=S(n−1,k−1)+k⋅S(n−1,k)S(n, k) = S(n-1, k-1) + k \cdot S(n-1, k)S(n,k)=S(n−1,k−1)+k⋅S(n−1,k)

组合意义推导

同样考虑第 nnn 个元素的归属,分为两种情况:

  1. 单独成集合 :第 nnn 个元素自己作为一个集合,剩下 n−1n-1n−1 个元素划分为 k−1k-1k−1 个集合,方案数为 S(n−1,k−1)S(n-1, k-1)S(n−1,k−1)。
  2. 加入已有集合 :第 nnn 个元素放入已有的 kkk 个集合中的任意一个,共 kkk 种选择,方案数为 k⋅S(n−1,k)k \cdot S(n-1, k)k⋅S(n−1,k)。

两种情况相加即得到递推公式。

边界条件

  • S(0,0)=1S(0, 0) = 1S(0,0)=1
  • S(n,0)=0(n>0)S(n, 0) = 0 \quad (n > 0)S(n,0)=0(n>0)
  • S(0,k)=0(k>0)S(0, k) = 0 \quad (k > 0)S(0,k)=0(k>0)
  • S(n,n)=1S(n, n) = 1S(n,n)=1
  • S(n,2)=2n−1−1S(n, 2) = 2^{n-1} - 1S(n,2)=2n−1−1
  • S(n,n−1)=(n2)S(n, n-1) = \binom{n}{2}S(n,n−1)=(2n)

例如:S(4,2)=S(3,1)+2⋅S(3,2)=1+2×3=7S(4,2) = S(3,1) + 2\cdot S(3,2) = 1 + 2\times 3 = 7S(4,2)=S(3,1)+2⋅S(3,2)=1+2×3=7。

2.3 通项公式(容斥原理)

第二类斯特林数存在显式的通项公式,可通过容斥原理推导得出:

S(n,k)=1k!∑i=0k(−1)i⋅(ki)⋅(k−i)nS(n,k) = \frac{1}{k!} \sum_{i=0}^k (-1)^i \cdot \binom{k}{i} \cdot (k-i)^nS(n,k)=k!1i=0∑k(−1)i⋅(ik)⋅(k−i)n

推导思路

先考虑「kkk 个有标号的盒子,每个盒子可以为空」的放球方案数为 knk^nkn。

再用容斥原理减去至少一个盒子为空的情况,得到「kkk 个有标号的盒子,每个盒子非空」的方案数:

∑i=0k(−1)i⋅(ki)⋅(k−i)n\sum_{i=0}^k (-1)^i \cdot \binom{k}{i} \cdot (k-i)^ni=0∑k(−1)i⋅(ik)⋅(k−i)n

由于集合是无序的,除以 k!k!k! 即得到第二类斯特林数。

这个公式的优势在于:当 nnn 很大而 kkk 较小时,可以直接计算,无需递推整个二维表,时间复杂度更优。

2.4 典型应用场景

  1. 经典放球模型
球的属性 盒子属性 盒子非空 方案数
不同 相同 S(n,k)S(n,k)S(n,k)
不同 不同 k!⋅S(n,k)k! \cdot S(n,k)k!⋅S(n,k)
不同 相同 ∑i=0kS(n,i)\sum_{i=0}^k S(n,i)∑i=0kS(n,i)

这是第二类斯特林数最直接的应用。

  1. 贝尔数(Bell Number)

    nnn 个元素的所有集合划分方案总数称为贝尔数 BnB_nBn,即第二类斯特林数的前缀和:

    Bn=∑k=0nS(n,k)B_n = \sum_{k=0}^n S(n,k)Bn=k=0∑nS(n,k)

  2. 集合划分与子集问题

    各类涉及"将元素分组、每组非空"的计数问题,大多可以转化为第二类斯特林数求解,例如任务分配、用户分组、状态划分等。

  3. 斯特林反演

    与二项式反演、莫比乌斯反演类似,斯特林反演是组合数学中的重要变换工具,常用于复杂计数问题的推导。

三、代码实现

以下代码均以Python为例,默认对 109+710^9+7109+7 取模,可直接用于算法竞赛场景。

3.1 第二类斯特林数(递推版)

最通用的实现方式,动态规划求解,时间复杂度 O(nk)O(nk)O(nk),空间复杂度 O(nk)O(nk)O(nk),可通过滚动数组优化至 O(k)O(k)O(k)。

python 复制代码
def stirling2_dp(n, k, mod=10**9+7):
    """
    递推计算第二类斯特林数 S(n, k)
    """
    if k > n:
        return 0
    # dp[i][j] 表示 S(i, j)
    dp = [[0]*(k+1) for _ in range(n+1)]
    dp[0][0] = 1
    for i in range(1, n+1):
        # j 不超过 min(i, k)
        for j in range(1, min(i, k)+1):
            dp[i][j] = (dp[i-1][j-1] + j * dp[i-1][j]) % mod
    return dp[n][k]

空间优化版(滚动数组):

python 复制代码
def stirling2_dp_opt(n, k, mod=10**9+7):
    if k > n:
        return 0
    dp = [0]*(k+1)
    dp[0] = 1
    for i in range(1, n+1):
        # 逆序遍历,避免覆盖上一轮的值
        for j in range(min(i, k), 0, -1):
            dp[j] = (dp[j-1] + j * dp[j]) % mod
    return dp[k]

3.2 第二类斯特林数(通项公式版)

利用容斥通项公式计算,时间复杂度 O(klog⁡n)O(k \log n)O(klogn),适合 nnn 很大、kkk 较小的场景。

python 复制代码
def stirling2_formula(n, k, mod=10**9+7):
    """
    通项公式计算第二类斯特林数 S(n, k)
    """
    if k > n:
        return 0
    
    # 预处理阶乘与逆阶乘
    fact = [1]*(k+1)
    for i in range(1, k+1):
        fact[i] = fact[i-1] * i % mod
    inv_fact = [1]*(k+1)
    inv_fact[k] = pow(fact[k], mod-2, mod)
    for i in range(k-1, -1, -1):
        inv_fact[i] = inv_fact[i+1] * (i+1) % mod
    
    res = 0
    for i in range(0, k+1):
        # 组合数 C(k, i)
        c = fact[k] * inv_fact[i] % mod * inv_fact[k-i] % mod
        term = c * pow(k - i, n, mod) % mod
        if i % 2 == 1:
            res = (res - term) % mod
        else:
            res = (res + term) % mod
    # 除以 k!
    res = res * inv_fact[k] % mod
    return res

3.3 第一类斯特林数(递推版)

无符号第一类斯特林数的递推实现,时间复杂度 O(nk)O(nk)O(nk)。

python 复制代码
def stirling1_dp(n, k, mod=10**9+7):
    """
    递推计算无符号第一类斯特林数 s(n, k)
    """
    if k > n:
        return 0
    dp = [[0]*(k+1) for _ in range(n+1)]
    dp[0][0] = 1
    for i in range(1, n+1):
        for j in range(1, min(i, k)+1):
            dp[i][j] = (dp[i-1][j-1] + (i-1) * dp[i-1][j]) % mod
    return dp[n][k]

3.4 贝尔数计算

基于第二类斯特林数求解贝尔数:

python 复制代码
def bell_number(n, mod=10**9+7):
    """计算第n个贝尔数"""
    dp = [0]*(n+1)
    dp[0] = 1
    for i in range(1, n+1):
        for j in range(i, 0, -1):
            dp[j] = (dp[j-1] + j * dp[j]) % mod
    return sum(dp) % mod

四、两类斯特林数对比总结

维度 第一类斯特林数(无符号) 第二类斯特林数
组合意义 n个元素划分为k个非空轮换 n个元素划分为k个非空集合
递推公式 s(n,k)=s(n−1,k−1)+(n−1)s(n−1,k)s(n,k) = s(n-1,k-1) + (n-1)s(n-1,k)s(n,k)=s(n−1,k−1)+(n−1)s(n−1,k) S(n,k)=S(n−1,k−1)+k⋅S(n−1,k)S(n,k) = S(n-1,k-1) + k\cdot S(n-1,k)S(n,k)=S(n−1,k−1)+k⋅S(n−1,k)
核心系数 n−1n-1n−1(插入位置数) kkk(集合个数)
边界特例 s(n,1)=(n−1)!s(n,1)=(n-1)!s(n,1)=(n−1)! S(n,1)=1S(n,1)=1S(n,1)=1
典型应用 排列轮换、圆桌排列 放球问题、集合划分、贝尔数

核心记忆点:一类分轮换,二类分集合;递推皆递归,讨论第n个

五、结语

和卡特兰数一样,斯特林数的核心魅力在于其递归思想------通过分类讨论"最后一个元素的归属",将大问题拆解为规模更小的子问题,最终得到简洁而优美的递推公式。

无论是第一类的轮换划分,还是第二类的集合划分,都是组合计数的基础模型。掌握了斯特林数,你就拥有了解决一大类分组、划分、排列计数问题的利器。

相关推荐
青春:一叶知秋1 小时前
【Python】python基本语法和使用
开发语言·python
不忘不弃1 小时前
计算pi的近似值
算法
码云骑士1 小时前
12-GIL不是性能杀手(下)-绕过GIL的三种方案与决策树
算法·决策树·机器学习
一只齐刘海的猫1 小时前
【Leetcode】无重复字符的最长子串
算法·leetcode·职场和发展
SilentSamsara1 小时前
向量数据库实战:Chroma/Milvus/Qdrant 选型与语义搜索应用
开发语言·数据库·人工智能·python·青少年编程·milvus
行智科技1 小时前
FAST-LIVO2 源码精读(二):环境搭建与编译避坑
算法·ubuntu·自动驾驶·slam
插件开发2 小时前
vs2015 cuda c++ cdpSimplePrint范例,递归功能实现演示
linux·c++·算法
Tisfy2 小时前
LeetCode 2130.链表最大孪生和:转数组 / 快慢指针+链表翻转(O(1))
算法·leetcode·链表·题解
沪漂阿龙2 小时前
Embedding:文本怎么变成向量?语义检索为什么能工作?
人工智能·python·embedding