斯特林数

1 上升幂和下降幂

1.1 定义

我们定义上升幂 \(n^{\overline{m}}\) 和下降幂 \(n^{\underline{m}}\) 如下:

\[\begin{aligned} n^{\overline{m}}=\prod _{i=n}^{n+m-1}i=\dfrac {(n+m-1)!}{(n-1)!}\\ n^{\underline{m}}=\prod_{i=n-m+1}^{n}i=\dfrac{n!}{(n-m)!} \end{aligned} \]

需要注意这里的 \(n,m\) 同样可以取到负数。

1.2 性质

首先我们先来看上升幂和下降幂对于指数求和的展开形式,显然有:

\[\begin{aligned} n^{\overline{a+b}}=n^{\overline{a}}\times (n+a)^{\overline{b}}\\ n^{\underline{a+b}}=n^{\underline{a}}\times (n-a)^{\underline{b}}\\ \end{aligned} \]

这个性质十分良好,因为它启示我们利用倍增去求解一些多项式,比如说 \(f(x)=x^{\overline{n}}\) 展开后的系数。在下文中会重新提到这一部分。

然后考虑上升下降幂之间的转化,显然有:

\[\begin{aligned} n^{\overline{m}}=(-1)^m(-n)^{\underline{m}}=(n+m-1)^{\underline{m}}\\ n^{\underline{m}}=(-1)^m(-n)^{\overline{m}}=(n-m+1)^{\overline{m}} \end{aligned} \]

接下来是下降幂和组合数的一些性质。实际上不难看出,\(n^{\underline{m}}\) 实际上就是 \(A_n^m\),于是我们可以将组合数转化如下:

\[\binom{n}{m}=\dfrac{n^{\underline{m}}}{m!} \]

于是我们可以推出下面的式子,通过这个我们可以换掉组合数的底数而保持值不变:

\[\binom{n}{m}=\dfrac{n^{\underline{m}}}{m!}=\dfrac{(-1)^m (-n)^{\overline{m}}}{m!}=(-1)^m\dfrac{(m-n-1)^{\underline{m}}}{m!}=(-1)^m\binom{m-n-1}{m} \]

接下来进一步的,我们将组合数与下降幂相乘:

\[\binom{n}{k}k^{\underline{i}}=\dfrac{n!}{k!(n-k)!}\dfrac {k!}{(k-i)!}=\dfrac{(n-i)!}{(k-i)!(n-k)!}\dfrac{n!}{(n-i)!}=\binom{n-i}{k-i}n^{\underline{i}} \]

如此操作后我们便将 \(k^{\underline{i}}\) 这个变量改成了 \(n^{\underline{i}}\) 这个定值,然后就可以进一步求解了。

不难看出,这一部分的难点就在于推式子并化简,上面是几个常见的化简形式,做题时需要注意。

2 第二类斯特林数

斯特林数是一个组合数学概念,分为第一类斯特林数和第二类斯特林数,是一种广泛运用于解决组合问题的利器。

由于第二类斯特林数更加常见,所以先介绍第二类斯特林数。

2.1 定义

第二类斯特林数写作 \(n\brace m\),表示将 \(n\) 个不同元素划分为 \(m\) 个互不区分的非空子集方案数。更通俗的来讲就是将 \(n\) 个不同的球放入 \(m\) 个相同的盒子,每个盒子至少放一个的方案数。

接下来我们容易写出其递推式,如下:

\[{n\brace m}={n-1\brace m-1}+m{n-1\brace m} \]

边界是 \({n\brace 0}=[n=0]\)。考虑用组合意义证明其正确性。当我们考虑放一个新球的时候,有两种方案:

  • 将球放到一个空的盒子里,方案数 \({n-1\brace m-1}\)
  • 将球放到一个现有的非空盒子里,方案数 \(m{n-1\brace m}\)

显然递推求解的复杂度是 \(O(nm)\) 的。

2.2 通项公式

第二类斯特林数有实用的通项公式,如下:

\[{n\brace m}=\sum_{i=0}^m \dfrac {(-1)^{m-i}}{(m-i)!}\dfrac{i^n}{i!} \]

接下来我们考虑证明这个公式,需要用到二项式反演。

\(F_i\) 表示将 \(n\) 个不同的球放到 \(i\) 个不同的盒子里,每个盒子至少放一个的方案数。令 \(G_i\) 表示将 \(n\) 个不同的球放到 \(i\) 个不同的盒子里的方案数。显然有:

\[G_i=i^n \]

接下来根据二项式反演得到:

\[G_i=\sum_{j=0}^i\binom ij F_j \iff F_i=\sum_{j=0}^i(-1)^{i-j} \binom ij G_j \]

然后可以得到:

\[\begin{aligned} F_i&=\sum_{j=0}^i(-1)^{i-j} \binom ij G_j\\ &= \sum_{j=0}^i(-1)^{i-j} \binom ij j^n\\ &=\sum_{j=0}^i\dfrac{(-1)^{i-j}\times i!\times j^n}{(i-j)!\times j!} \end{aligned} \]

由于 \(F_i\) 求得是盒子不同的方案数,而斯特林数求得是盒子相同的方案数,所以最后除以 \(i!\) 即 可得到斯特林数的通项公式:

\[{n\brace m}= \dfrac{F_m}{m!} =\sum_{i=0}^m \dfrac {(-1)^{m-i}}{(m-i)!}\dfrac{i^n}{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 m}={n-1\brack m-1}+(n-1){n-1\brack m} \]

边界依旧是 \({n\brack 0}=[n=0]\)。考虑用组合意义证明其正确性。当我们考虑放一个新人的时候,有两种方案:

  • 将这个人放到已经有的圆桌中,那么需要考虑它坐在那一个人旁边,方案数为 \((n-1){n-1\brack m}\)
  • 将这个人放到一个新的空圆桌中,方案数为 \({n-1\brack m-1}\)

显然递推求解的复杂度是 \(O(nm)\) 的。

第一类斯特林数没有实用的通项公式,在此不做介绍。

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

我们构造出同一行第一类斯特林数的生成函数如下:

\[F_n(x)=\sum_{i=0}^n {n\brack i} x^i \]

然后接下来根据递推式写出生成函数递推式:

\[F_n(x)=xF_{n-1}(x)+(n-1)F_{n-1}(x) = (x+n-1)F_{n-1}(x) \]

于是有:

\[F_n(x)=\prod_{i=0}^{n-1} (x+i) = x^{\overline{n}} \]

于是实际上同一行第一类斯特林数的生成函数就是 \(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\),于是会有:

\[\begin{aligned} F_{2n}(x)&=x^{\overline{2n}}\\ &= x^{\overline n}(x+n)^{\overline n}\\ &= (\sum_{i=0}^n a_ix^i)(\sum_{i=0}^n a_i(x+n)^i)\\ &= (\sum_{i=0}^n a_ix^i)(\sum_{i=0}^n a_i\sum_{j=0}^i\binom ij x^jn^{i-j})\\ &= (\sum_{i=0}^n a_ix^i)(\sum_{j=0}^nx^j\sum_{i=j}^n a_i\binom ij n^{i-j})\\ &= (\sum_{i=0}^n a_ix^i)(\sum_{j=0}^nx^j\sum_{i=j}^n a_in^{i-j}\dfrac{i!}{j!(i-j)!})\\ &= (\sum_{i=0}^n a_ix^i)(\sum_{i=0}^n\dfrac{x^i}{i!}\sum_{j=i}^n (a_jj!)\dfrac{n^{j-i}}{(j-i)!})\\ \end{aligned} \]

最后面的和式显然就是一个差卷积的形式,多项式乘起来即可。然后这两个括号相乘还是多项式乘法,再乘一次即可得出 \(F_{2n}(x)\)。于是我们可以在:

\[T(n)=T(\dfrac n2) +O(n\log n) =O(n\log n) \]

的复杂度内求出 \(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^{\overline{n}}=\sum_{i=0}^n {n\brack i} x^i \]

对其作斯特林反演即可得到普通幂转化成上升幂的形式:

\[x^n=\sum_{i=0}^n {n\brace i}(-1)^{n-i}x^{\overline{i}} \]

让我们来考虑普通幂转化成下降幂的形式,我们有:

\[x^n=\sum_{i=0}^n {n\brace i}i! \binom xi =\sum_{i=0}^n {n\brace i}x^{\underline{i}} \]

考虑组合意义,\(x^n\) 即表示将 \(n\) 个不同的球放到 \(x\) 个不同的盒子里,盒子可以为空的方案数;那么不妨先枚举有球的盒子的个数 \(i\),然后我们要将 \(n\) 个球放到这 \(i\) 个不同的盒子里,盒子不可以为空,显然这个方案数就是 \({n\brace i}i!\)。所以上式成立。

对其作斯特林反演即可得到下降幂转化成普通幂的形式:

\[x^{\underline{n}}=\sum_{i=0}^n {n\brack i} (-1)^{n-i} x^i \]

不过实际上斯特林反演的证明需要运用到这个公式,所以我们需要用另外的方法证明它。容易发现:

\[\begin{aligned} x^{\underline{n}}&= (-1)^n (-x)^{\overline{n}}\\ &=(-1)^n \sum_{i=0}^n {n\brack i} (-x)^i\\ &=\sum_{i=0}^n (-1)^{n+i} {n\brack i} x^i\\ &=\sum_{i=0}^n (-1)^{n-i} {n\brack i} x^i\\ \end{aligned} \]

至此我们就可以实现普通幂、上升幂、下降幂之间的灵活转化了。

4.2 例题

例 1 [联合省选 2020 A 卷] 组合数问题

\(\text{Link}\)

暴力推式子即可。首先化开 \(f(k)\)

\[\begin{aligned} &\sum_{k=0}^n f(k)\times x^k\times \binom{n}{k}\\ =& \sum_{k=0}^n \sum_{i=0}^m a_ik^i\times x^k\times \binom{n}{k}\\ =& \sum_{i=0}^m a_i\sum_{k=0}^n k^i\times x^k\times \binom{n}{k}\\ \end{aligned} \]

现在看后面的和式,对其进行如下变换:

\[\begin{aligned} & \sum_{k=0}^n k^i\times x^k\times \binom{n}{k}\\ =&\sum_{k=0}^n \sum_{j=0}^i{i\brace j} k^{\underline{j}} \times x^k\times \binom{n}{k}\\ =&\sum_{k=0}^n \sum_{j=0}^i{i\brace j} n^{\underline{j}} \times x^k\times \binom{n-j}{k-j}\\ =&\sum_{j=0}^i{i\brace j} n^{\underline{j}}\sum_{k=0}^n x^k\times \binom{n-j}{k-j}\\ =&\sum_{j=0}^i{i\brace j} n^{\underline{j}}\sum_{k=0}^{n-j} \binom{n-j}{k}\times x^{k+j}\\ =&\sum_{j=0}^i{i\brace j} n^{\underline{j}}x^j\sum_{k=0}^{n-j} \binom{n-j}{k}\times x^{k}\\ =&\sum_{j=0}^i{i\brace j} n^{\underline{j}}x^j(x+1)^{n-j} \end{aligned} \]

于是我们就可以在 \(O(m^2)\) 的复杂度内得到原式结果了。

例 2 [国家集训队] Crash 的文明世界

\(\text{Link}\)

我们要求距离的 \(k\) 次方和,首先想到的肯定是运用二项式定理对距离进行合并或拆分。于是想到换根 dp,设 \(f(i,j)\) 表示 \(i\) 子树内到 \(i\) 的距离的 \(j\) 次方和,那么可以在 \(O(nk^2)\) 的复杂度内求出答案,无法通过。

考虑将 \(k\) 次方做斯特林展开:

\[\begin{aligned} S(i)&=\sum_{j=1}^n dis(i,j)^k\\ &=\sum_{j=1}^n \sum_{m=1}^k {k\brace m} m!\binom{dis(i,j)}{m}\\ &=\sum_{m=1}^k {k\brace m} m!\sum_{j=1}^n \binom{dis(i,j)}{m}\\ \end{aligned} \]

于是我们只需要求出所有的 \(\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] 求和

\(\text{Link}\)

暴力拆式子即可:

\[\begin{aligned} f(n)&=\sum_{i=0}^n\sum_{j=0}^i {i\brace j}\times 2^j\times j!\\ &=\sum_{i=0}^n\sum_{j=0}^n {i\brace j}\times 2^j\times j!\\ &=\sum_{j=0}^n 2^j\times j!\sum_{i=0}^n{i\brace j}\\ &=\sum_{j=0}^n 2^j\times j!\sum_{i=0}^n\sum_{k=0}^{j}\dfrac{(-1)^{j-k}}{(j-k)!}\dfrac{k^i}{k!}\\ &=\sum_{j=0}^n 2^j\times j!\sum_{k=0}^{j}\dfrac{(-1)^{j-k}}{(j-k)!}\dfrac{\sum_{i=0}^nk^i}{k!}\\ \end{aligned} \]

发现后面的和式是一个朴素和卷积的形式,因此直接 NTT 求出卷积即可。复杂度 \(O(n\log n)\)

例 4 [BZOJ5093] 图的价值

题意:定义一个带标号简单无向图的价值为每个点度数的 \(k\) 次方之和,求所有 \(n\) 个点的带标号简单无向图价值之和。

不难发现每个点的贡献是独立的,因此我们可以枚举每个点的度数并计算其对应方案数。那么对于一个点,其贡献如下:

\[\sum\limits_{i=0}^{n-1} \binom{n-1}{i}\times 2^{\tfrac{n(n-1)}{2}-n+1}\times i^k \]

\(i^k\) 做斯特林展开可得:

\[\begin{aligned} &\sum\limits_{i=0}^{n-1} \binom{n-1}{i}\times 2^{\tfrac{n(n-1)}{2}-n+1}\times \sum_{j=0}^k {k\brace j} j!\binom{i}{j}\\ =&\sum_{j=0}^k {k\brace j} j!\sum\limits_{i=0}^{n-1} \binom{n-1}{i}\times 2^{\tfrac{n(n-1)}{2}-n+1}\times \binom{i}{j}\\ =&\sum_{i=0}^k {k\brace i} i!\sum\limits_{j=0}^{n-1} \binom{n-1}{j}\times 2^{\tfrac{n(n-1)}{2}-n+1}\times \binom{j}{i}\\ =&\sum_{i=0}^k {k\brace i}\sum\limits_{j=0}^{n-1} 2^{\tfrac{n(n-1)}{2}-n+1}\times i!\times \binom{n-1}{j}\times \binom{j}{i}\\ =&\sum_{i=0}^k {k\brace i}\sum\limits_{j=0}^{n-1} 2^{\tfrac{n(n-1)}{2}-n+1}\times \binom{n-1-i}{j-i}\times (n-1)^{\underline{i}}\\ =&\sum_{i=0}^k 2^{\tfrac{n(n-1)}{2}-n+1}{k\brace i} (n-1)^{\underline{i}}\sum\limits_{j=0}^{n-1} \binom{n-1-i}{j-i}\\ =&\sum_{i=0}^k 2^{\tfrac{n(n-1)}{2}-n+1}{k\brace i} (n-1)^{\underline{i}}2^{n-1-i}\\ \end{aligned} \]

所以我们只需要求出 \(k\) 这一行上的斯特林数即可 \(O(k)\) 求出答案,求一行上第二类斯特林数直接 NTT 即可,复杂度 \(O(k\log k)\)

例 5 [FJOI2016] 建筑师

\(\text{Link}\)

考虑 dp,设 \(dp(i,j)\) 表示前 \(i\) 个建筑可以看到 \(j\) 个的方案数,则有转移:

\[dp(i,j)=dp(i-1,j-1)+(i-1)dp(i-1,j) \]

不难发现 \(dp(i,j)={i\brack j}\)。然后考虑答案,枚举最大值所在位置,答案即为:

\[\sum_{i=1}^n {i-1\brack A-1} {n-i\brack B-1} \binom{n-1}{i-1} \]

复杂度是 \(O(n)\) 的,不能接受。考虑组合意义优化,我们认为每个数及其后面被他遮住的建筑为一块,那么我们直接求出 \({n-1\brack A+B-2}\),这样会构成 \(A+B-2\) 个块,从这些块里选出 \(B-1\) 个块放到后面即可构成一个合法序列,所以答案实际上就是:

\[{n-1\brack A+B-2} \binom{A+B-2}{B-1} \]

如此预处理出第一类斯特林数即可 \(O(1)\) 求解答案。

posted @ 2025-01-23 10:48  UKE_Automation  阅读(178)  评论(0)    收藏  举报