斯特林数
1 上升幂和下降幂
1.1 定义
我们定义上升幂
需要注意这里的
1.2 性质
首先我们先来看上升幂和下降幂对于指数求和的展开形式,显然有:
这个性质十分良好,因为它启示我们利用倍增去求解一些多项式,比如说
然后考虑上升下降幂之间的转化,显然有:
接下来是下降幂和组合数的一些性质。实际上不难看出,
于是我们可以推出下面的式子,通过这个我们可以换掉组合数的底数而保持值不变:
接下来进一步的,我们将组合数与下降幂相乘:
如此操作后我们便将
不难看出,这一部分的难点就在于推式子并化简,上面是几个常见的化简形式,做题时需要注意。
2 第二类斯特林数
斯特林数是一个组合数学概念,分为第一类斯特林数和第二类斯特林数,是一种广泛运用于解决组合问题的利器。
由于第二类斯特林数更加常见,所以先介绍第二类斯特林数。
2.1 定义
第二类斯特林数写作
接下来我们容易写出其递推式,如下:
边界是
- 将球放到一个空的盒子里,方案数
。 - 将球放到一个现有的非空盒子里,方案数
。
显然递推求解的复杂度是
2.2 通项公式
第二类斯特林数有实用的通项公式,如下:
接下来我们考虑证明这个公式,需要用到二项式反演。
令
接下来根据二项式反演得到:
然后可以得到:
由于
2.3 同一行第二类斯特林数的计算
回到上面的通项公式,不难发现对于相同的
模板题:第二类斯特林数·行,代码如下:
#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 定义
第一类斯特林数写作
接下来我们容易写出其递推式,如下:
边界依旧是
- 将这个人放到已经有的圆桌中,那么需要考虑它坐在那一个人旁边,方案数为
。 - 将这个人放到一个新的空圆桌中,方案数为
。
显然递推求解的复杂度是
第一类斯特林数没有实用的通项公式,在此不做介绍。
3.2 同一行第一类斯特林数的计算
我们构造出同一行第一类斯特林数的生成函数如下:
然后接下来根据递推式写出生成函数递推式:
于是有:
于是实际上同一行第一类斯特林数的生成函数就是
首先明确
最后面的和式显然就是一个差卷积的形式,多项式乘起来即可。然后这两个括号相乘还是多项式乘法,再乘一次即可得出
的复杂度内求出
模板题:第一类斯特林数·行,代码如下:
#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 小节内我们已经给出了上升幂转化成普通幂的形式:
对其作斯特林反演即可得到普通幂转化成上升幂的形式:
让我们来考虑普通幂转化成下降幂的形式,我们有:
考虑组合意义,
对其作斯特林反演即可得到下降幂转化成普通幂的形式:
不过实际上斯特林反演的证明需要运用到这个公式,所以我们需要用另外的方法证明它。容易发现:
至此我们就可以实现普通幂、上升幂、下降幂之间的灵活转化了。
4.2 例题
例 1 [联合省选 2020 A 卷] 组合数问题
暴力推式子即可。首先化开
现在看后面的和式,对其进行如下变换:
于是我们就可以在
例 2 [国家集训队] Crash 的文明世界
我们要求距离的
考虑将
于是我们只需要求出所有的
例 3 [TJOI/HEOI2016] 求和
暴力拆式子即可:
发现后面的和式是一个朴素和卷积的形式,因此直接 NTT 求出卷积即可。复杂度
例 4 [BZOJ5093] 图的价值
题意:定义一个带标号简单无向图的价值为每个点度数的
不难发现每个点的贡献是独立的,因此我们可以枚举每个点的度数并计算其对应方案数。那么对于一个点,其贡献如下:
对
所以我们只需要求出
例 5 [FJOI2016] 建筑师
考虑 dp,设
不难发现
复杂度是
如此预处理出第一类斯特林数即可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律