斯特林数
1 上升幂和下降幂
1.1 定义
我们定义上升幂 \(n^{\overline{m}}\) 和下降幂 \(n^{\underline{m}}\) 如下:
需要注意这里的 \(n,m\) 同样可以取到负数。
1.2 性质
首先我们先来看上升幂和下降幂对于指数求和的展开形式,显然有:
这个性质十分良好,因为它启示我们利用倍增去求解一些多项式,比如说 \(f(x)=x^{\overline{n}}\) 展开后的系数。在下文中会重新提到这一部分。
然后考虑上升下降幂之间的转化,显然有:
接下来是下降幂和组合数的一些性质。实际上不难看出,\(n^{\underline{m}}\) 实际上就是 \(A_n^m\),于是我们可以将组合数转化如下:
于是我们可以推出下面的式子,通过这个我们可以换掉组合数的底数而保持值不变:
接下来进一步的,我们将组合数与下降幂相乘:
如此操作后我们便将 \(k^{\underline{i}}\) 这个变量改成了 \(n^{\underline{i}}\) 这个定值,然后就可以进一步求解了。
不难看出,这一部分的难点就在于推式子并化简,上面是几个常见的化简形式,做题时需要注意。
2 第二类斯特林数
斯特林数是一个组合数学概念,分为第一类斯特林数和第二类斯特林数,是一种广泛运用于解决组合问题的利器。
由于第二类斯特林数更加常见,所以先介绍第二类斯特林数。
2.1 定义
第二类斯特林数写作 \(n\brace m\),表示将 \(n\) 个不同元素划分为 \(m\) 个互不区分的非空子集方案数。更通俗的来讲就是将 \(n\) 个不同的球放入 \(m\) 个相同的盒子,每个盒子至少放一个的方案数。
接下来我们容易写出其递推式,如下:
边界是 \({n\brace 0}=[n=0]\)。考虑用组合意义证明其正确性。当我们考虑放一个新球的时候,有两种方案:
- 将球放到一个空的盒子里,方案数 \({n-1\brace m-1}\)。
- 将球放到一个现有的非空盒子里,方案数 \(m{n-1\brace m}\)。
显然递推求解的复杂度是 \(O(nm)\) 的。
2.2 通项公式
第二类斯特林数有实用的通项公式,如下:
接下来我们考虑证明这个公式,需要用到二项式反演。
令 \(F_i\) 表示将 \(n\) 个不同的球放到 \(i\) 个不同的盒子里,每个盒子至少放一个的方案数。令 \(G_i\) 表示将 \(n\) 个不同的球放到 \(i\) 个不同的盒子里的方案数。显然有:
接下来根据二项式反演得到:
然后可以得到:
由于 \(F_i\) 求得是盒子不同的方案数,而斯特林数求得是盒子相同的方案数,所以最后除以 \(i!\) 即 可得到斯特林数的通项公式:
2.3 同一行第二类斯特林数的计算
回到上面的通项公式,不难发现对于相同的 \(n\),后面的和式正好构成了一个和卷积的形式,于是我们直接使用 FFT / NTT 对其进行求解即可。
模板题:第二类斯特林数·行,代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = (1 << 19) + 5;
const int Inf = 2e9;
const int Mod = 167772161;
const int YG = 3;
const int InvG = 55924054;
int n;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) res = res * a % Mod;
a = a * a % Mod; b >>= 1;
}
return res;
}
int f[Maxn], g[Maxn];
void init() {
f[0] = 1;
for(int i = 1; i <= n; i++) f[i] = f[i - 1] * i % Mod;
g[n] = qpow(f[n], Mod - 2);
for(int i = n - 1; i >= 0; i--) g[i] = g[i + 1] * (i + 1) % Mod;
}
int r[Maxn];
struct Poly {
int n; vector <int> a;
void reset(int len) {n = len; a.resize(len + 1);}
int& operator [](int x) {return a[x];}
void NTT(int len, int typ) {
reset(len - 1);
for(int i = 0; i < len; i++) if(i < r[i]) swap(a[i], a[r[i]]);
for(int h = 1; h < len; h <<= 1) {
int cur = qpow(typ == 1 ? YG : InvG, (Mod - 1) / (h << 1));
for(int i = 0; i < len; i += (h << 1)) {
int w = 1;
for(int j = 0; j < h; j++, w = w * cur % Mod) {
int x = a[i + j], y = a[i + j + h] * w % Mod;
a[i + j] = (x + y) % Mod;
a[i + j + h] = (x - y + Mod) % Mod;
}
}
}
if(typ == -1) {
int iv = qpow(len, Mod - 2);
for(int i = 0; i < len; i++) a[i] = a[i] * iv % Mod;
}
}
Poly operator * (Poly y) {
Poly x = *this, z;
int n = x.n + y.n, len = 1;
while(len <= n) len <<= 1;
for(int i = 0; i < len; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) * (len >> 1));
x.NTT(len, 1), y.NTT(len, 1);
z.reset(len - 1);
for(int i = 0; i < len; i++) z[i] = x[i] * y[i] % Mod;
z.NTT(len, -1);
z.reset(n);
return z;
}
}F, G;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
init();
F.reset(n), G.reset(n);
for(int i = 0; i <= n; i++) {
F[i] = g[i] * ((i & 1) ? -1 : 1);
G[i] = g[i] * qpow(i, n) % Mod;
}
F = F * G;
for(int i = 0; i <= n; i++) cout << F[i] << " ";
return 0;
}
同一列第二类斯特林数的计算较为困难,以后有机会再写。
3 第一类斯特林数
3.1 定义
第一类斯特林数写作 \(n\brack m\),表示将 \(n\) 个两两不同的元素划分成 \(m\) 个互不区分的非空轮换的方案数。通俗来讲就是 \(n\) 个不同的人坐在 \(m\) 张相同的圆桌上,每一张圆桌至少坐一个人的方案数。
接下来我们容易写出其递推式,如下:
边界依旧是 \({n\brack 0}=[n=0]\)。考虑用组合意义证明其正确性。当我们考虑放一个新人的时候,有两种方案:
- 将这个人放到已经有的圆桌中,那么需要考虑它坐在那一个人旁边,方案数为 \((n-1){n-1\brack m}\)。
- 将这个人放到一个新的空圆桌中,方案数为 \({n-1\brack m-1}\)。
显然递推求解的复杂度是 \(O(nm)\) 的。
第一类斯特林数没有实用的通项公式,在此不做介绍。
3.2 同一行第一类斯特林数的计算
我们构造出同一行第一类斯特林数的生成函数如下:
然后接下来根据递推式写出生成函数递推式:
于是有:
于是实际上同一行第一类斯特林数的生成函数就是 \(x^{\overline{n}}\)。在 1.2 小节中我们提到过可以使用倍增来求出其各项系数,现在我们来看怎样求。\(O(n\log^2 n)\) 直接分治的做法是显然的,不过我们有单 \(\log\) 做法:
首先明确 \(F_n(x)= x^{\overline{n}}\),设 \(F_n(x)=\sum\limits_{i=0}^n a_ix^i\),于是会有:
最后面的和式显然就是一个差卷积的形式,多项式乘起来即可。然后这两个括号相乘还是多项式乘法,再乘一次即可得出 \(F_{2n}(x)\)。于是我们可以在:
的复杂度内求出 \(x^{\overline{n}}\) 的展开形式,即同一行第一类斯特林数的值。当然如果 \(n\) 是奇数的话直接求会漏掉 \(x+n\) 这一项,暴力乘上去就行。
模板题:第一类斯特林数·行,代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = (1 << 20) + 5;
const int Inf = 2e9;
const int Mod = 167772161;
const int YG = 3;
const int InvG = 55924054;
int n;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) res = res * a % Mod;
a = a * a % Mod, b >>= 1;
}
return res;
}
int f[Maxn], g[Maxn];
void init() {
f[0] = 1;
for(int i = 1; i <= n; i++) f[i] = f[i - 1] * i % Mod;
g[n] = qpow(f[n], Mod - 2);
for(int i = n - 1; i >= 0; i--) g[i] = g[i + 1] * (i + 1) % Mod;
}
int r[Maxn];
struct Poly {
int n; vector <int> a;
void reset(int len) {n = len, a.resize(len + 1);}
int& operator [](int x) {return a[x];}
void NTT(int len, int typ) {
reset(len - 1);
for(int i = 0; i < len; i++) if(i < r[i]) swap(a[i], a[r[i]]);
for(int h = 1; h < len; h <<= 1) {
int cur = qpow(typ == 1 ? YG : InvG, (Mod - 1) / (h << 1));
for(int i = 0; i < len; i += (h << 1)) {
for(int j = 0, w = 1; j < h; j++, w = w * cur % Mod) {
int x = a[i + j], y = a[i + j + h] * w % Mod;
a[i + j] = (x + y) % Mod;
a[i + j + h] = (x - y + Mod) % Mod;
}
}
}
if(typ == -1) {
int iv = qpow(len, Mod - 2);
for(int i = 0; i < len; i++) a[i] = a[i] * iv % Mod;
}
}
Poly operator * (Poly y) {
Poly x = *this, z;
int n = x.n + y.n, len = 1;
while(len <= n) len <<= 1;
for(int i = 0; i < len; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) * (len >> 1));
x.NTT(len, 1), y.NTT(len, 1);
z.reset(len - 1);
for(int i = 0; i < len; i++) z[i] = x[i] * y[i] % Mod;
z.NTT(len, -1);
z.reset(n);
return z;
}
};
Poly solve(int n) {
if(n == 1) {
Poly F; F.reset(n);
F[1] = 1; return F;
}
int m = n >> 1;
Poly F = solve(m);
Poly G, H; G.reset(m), H.reset(m);
for(int i = 0; i <= m; i++) G[i] = qpow(m, i) * g[i] % Mod, H[m - i] = F[i] * f[i] % Mod;
H = G * H; G.reset(m);
for(int i = 0; i <= m; i++) G[i] = H[m - i] * g[i] % Mod;
G = F * G; F.reset(n);
for(int i = 0; i <= n; i++) {
if(n & 1) F[i] = (G[i] * (n - 1) % Mod + (i ? G[i - 1] : 0)) % Mod;
else F[i] = G[i];
}
return F;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
init();
Poly F = solve(n);
for(int i = 0; i <= n; i++) {
cout << F[i] << " ";
}
return 0;
}
同一列第一类斯特林数的计算也较为困难,以后再写。
4 应用
4.1 幂的互化
首先我们回到上升幂和下降幂,考虑上升幂、下降幂与普通幂之间的互相转化。
在 3.2 小节内我们已经给出了上升幂转化成普通幂的形式:
对其作斯特林反演即可得到普通幂转化成上升幂的形式:
让我们来考虑普通幂转化成下降幂的形式,我们有:
考虑组合意义,\(x^n\) 即表示将 \(n\) 个不同的球放到 \(x\) 个不同的盒子里,盒子可以为空的方案数;那么不妨先枚举有球的盒子的个数 \(i\),然后我们要将 \(n\) 个球放到这 \(i\) 个不同的盒子里,盒子不可以为空,显然这个方案数就是 \({n\brace i}i!\)。所以上式成立。
对其作斯特林反演即可得到下降幂转化成普通幂的形式:
不过实际上斯特林反演的证明需要运用到这个公式,所以我们需要用另外的方法证明它。容易发现:
至此我们就可以实现普通幂、上升幂、下降幂之间的灵活转化了。
4.2 例题
例 1 [联合省选 2020 A 卷] 组合数问题
暴力推式子即可。首先化开 \(f(k)\):
现在看后面的和式,对其进行如下变换:
于是我们就可以在 \(O(m^2)\) 的复杂度内得到原式结果了。
例 2 [国家集训队] Crash 的文明世界
我们要求距离的 \(k\) 次方和,首先想到的肯定是运用二项式定理对距离进行合并或拆分。于是想到换根 dp,设 \(f(i,j)\) 表示 \(i\) 子树内到 \(i\) 的距离的 \(j\) 次方和,那么可以在 \(O(nk^2)\) 的复杂度内求出答案,无法通过。
考虑将 \(k\) 次方做斯特林展开:
于是我们只需要求出所有的 \(\binom{dis(i,j)}{m}\) 即可,设 \(f(i,j)\) 表示 \(i\) 子树内到 \(i\) 距离的 \(\binom{dis}{j}\) 之和,由于我们有 \(\binom{n+1}{m}=\binom{n}{m}+\binom{n}{m-1}\),所以转移方程为 \(f(i,j)=f(to,j)+f(to,j-1)\)。然后做一遍换根即可得出正确答案。复杂度 \(O(k^2+ nk)\),可以通过。
例 3 [TJOI/HEOI2016] 求和
暴力拆式子即可:
发现后面的和式是一个朴素和卷积的形式,因此直接 NTT 求出卷积即可。复杂度 \(O(n\log n)\)。
例 4 [BZOJ5093] 图的价值
题意:定义一个带标号简单无向图的价值为每个点度数的 \(k\) 次方之和,求所有 \(n\) 个点的带标号简单无向图价值之和。
不难发现每个点的贡献是独立的,因此我们可以枚举每个点的度数并计算其对应方案数。那么对于一个点,其贡献如下:
对 \(i^k\) 做斯特林展开可得:
所以我们只需要求出 \(k\) 这一行上的斯特林数即可 \(O(k)\) 求出答案,求一行上第二类斯特林数直接 NTT 即可,复杂度 \(O(k\log k)\)。
例 5 [FJOI2016] 建筑师
考虑 dp,设 \(dp(i,j)\) 表示前 \(i\) 个建筑可以看到 \(j\) 个的方案数,则有转移:
不难发现 \(dp(i,j)={i\brack j}\)。然后考虑答案,枚举最大值所在位置,答案即为:
复杂度是 \(O(n)\) 的,不能接受。考虑组合意义优化,我们认为每个数及其后面被他遮住的建筑为一块,那么我们直接求出 \({n-1\brack A+B-2}\),这样会构成 \(A+B-2\) 个块,从这些块里选出 \(B-1\) 个块放到后面即可构成一个合法序列,所以答案实际上就是:
如此预处理出第一类斯特林数即可 \(O(1)\) 求解答案。

浙公网安备 33010602011771号