在组合计数的世界里,如果说卡特兰数是刻画「栈式结构」的代名词,那么斯特林数便是定义「划分问题」的基石。同样源于递归思想,同样拥有丰富的组合意义,第一类与第二类斯特林数分别解决了轮换划分 与集合划分两大核心问题,是算法竞赛与组合数学中不可或缺的工具。
本文将从组合意义出发,推导递推公式,讲解典型应用,并给出可直接复用的代码实现,带你系统掌握两类斯特林数。
一、第一类斯特林数:轮换的计数
第一类斯特林数(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,c、b,c,ab,c,ab,c,a、c,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 个元素的归属,分为两种互斥的情况:
- 单独成轮换 :第 nnn 个元素自己构成一个轮换,剩下的 n−1n-1n−1 个元素划分为 k−1k-1k−1 个轮换,方案数为 s(n−1,k−1)s(n-1, k-1)s(n−1,k−1)。
- 插入已有轮换 :第 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 典型应用场景
-
排列的轮换分解
任意一个 nnn 元排列都可以唯一分解为若干个互不相交的轮换的乘积。恰好包含 kkk 个轮换的 nnn 元排列的个数,就等于无符号第一类斯特林数 s(n,k)s(n,k)s(n,k)。
-
圆桌排列问题
nnn 个人围坐在 kkk 张完全相同的圆桌旁,每张桌子至少坐1人,旋转后相同视为同一种坐法,总方案数为 s(n,k)s(n,k)s(n,k)。
-
上升阶乘展开
上升阶乘 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 个元素的归属,分为两种情况:
- 单独成集合 :第 nnn 个元素自己作为一个集合,剩下 n−1n-1n−1 个元素划分为 k−1k-1k−1 个集合,方案数为 S(n−1,k−1)S(n-1, k-1)S(n−1,k−1)。
- 加入已有集合 :第 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 典型应用场景
- 经典放球模型
| 球的属性 | 盒子属性 | 盒子非空 | 方案数 |
|---|---|---|---|
| 不同 | 相同 | 是 | 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) |
这是第二类斯特林数最直接的应用。
-
贝尔数(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)
-
集合划分与子集问题
各类涉及"将元素分组、每组非空"的计数问题,大多可以转化为第二类斯特林数求解,例如任务分配、用户分组、状态划分等。
-
斯特林反演
与二项式反演、莫比乌斯反演类似,斯特林反演是组合数学中的重要变换工具,常用于复杂计数问题的推导。
三、代码实现
以下代码均以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(klogn)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个。
五、结语
和卡特兰数一样,斯特林数的核心魅力在于其递归思想------通过分类讨论"最后一个元素的归属",将大问题拆解为规模更小的子问题,最终得到简洁而优美的递推公式。
无论是第一类的轮换划分,还是第二类的集合划分,都是组合计数的基础模型。掌握了斯特林数,你就拥有了解决一大类分组、划分、排列计数问题的利器。