I - Infinity - 题解
标签与难度
标签: 组合数学, 生成函数, 多项式NTT, 多项式对数函数, 多项式指数函数, 群论 难度: 2900
题目大意喵~
哈喵~!各位算法大师们,今天本喵要带大家攻略一道超级有趣的题目!
题目是这样哒:
首先,我们有一个集合 ,它包含了所有 个元素的排列,也就是我们熟悉的对称群,喵~ 对于 中的每一个排列 ,我们定义一个值 ,它表示集合 中不同元素的数量。
然后,题目会给我们一个固定的整数 ,和好多好多个查询 。对每一个 ,我们都要计算下面这个式子的值,并对 取模:
简单来说,就是对所有 -阶排列 ,计算出它的 值的 次方,然后把它们全部加起来。是不是听起来就很有挑战性呀?喵~
解题思路分析
这道题看起来披着一层群论的神秘外衣,可能会吓到一些小伙伴,但别怕,本喵会像解开毛线球一样,把思路一点点理清楚的!
步骤一:理解 的真面目
首先,我们来研究一下 是什么。在群论中,集合 被称为排列 的共轭类(Conjugacy Class)。而 就是这个共轭类的大小。
对称群 有一个非常美妙的性质:两个排列是共轭的,当且仅当它们有相同的轮换分解结构(Cycle Type)。
什么叫轮换分解结构呢?就是一个排列分解成不相交的循环置换后,这些循环的长度组成的集合。比如说,在 中,排列 有一个长度为2的循环和一个长度为3的循环,它的轮换结构就是 。所有轮换结构是 的排列(比如 )都和它在同一个共轭类里。
步骤二:转化求和式
知道了这一点,我们就可以把原来的求和式进行一个漂亮的变身!原来的和是枚举所有的排列 ,但我们可以换个角度,按共轭类来分组计算。
设 的所有共轭类为 。
因为在同一个共轭类 中,所有排列 的 值都是相等的,都等于这个类的大小 。所以,内层的和可以简化:
于是,我们的总和就变成了:
这可真是个不得了的发现!我们只需要知道所有共轭类的大小,然后求它们的 次方和就可以啦!
步骤三:共轭类大小的公式
一个排列的轮换结构可以用一个序列 来表示,其中 是长度为 的循环的个数。这个结构必须满足 。
对于一个给定的轮换结构 ,它对应的共轭类的大小是:
一个更有趣的事实是,具有这个轮换结构的排列的个数,也恰好是这个公式!这说明,对于一个共轭类里的任何一个排列 ,(共轭类大小)就等于与它同类型的排列的数量。
所以,我们的目标是计算:
这个和是枚举所有满足 的轮换结构。为了方便,我们令 。
步骤四:请出我们的魔法道具——生成函数!
这个公式看起来非常复杂,直接计算所有整数分拆是不可能的。这种组合计数问题,正是生成函数大显身手的时候!
我们把公式稍微变形一下:
令 。我们来构造 的生成函数 。
观察 的结构,它是对所有满足 的 求和,这种形式的卷积可以表示为多个多项式的乘积。我们为每个循环长度 定义一个多项式:
这里的 就相当于原来的 。那么 就是所有这些 的乘积!
就会自动地从每个 中选出一项 ,使得指数和 ,这正好就是 的定义!
步骤五:对数-指数魔法(ln-exp 技巧)
无限乘积太难处理了,喵~ 我们可以用对数把它变成加法:
现在我们来仔细看看 。 。 如果我们定义一个基础多项式 ,那么 。 令 ,则 。
再令 。那么:
把所有 的贡献加起来,我们就能得到 的系数 :
最终的算法流程
好耶!我们已经推导出了一条清晰的计算路径,喵~
- 预处理: 计算 。因为模数是质数 ,根据费马小定理,指数需要对 取模。同时预计算阶乘和阶乘逆元。
- 构造 : 构造多项式 ,其中 是所有查询中最大的 。
- 计算 : 使用多项式对数函数算法 (NTT实现),得到 的系数 。
- 计算 : 根据公式 ,计算出 的系数 。这可以通过枚举每个 和它的倍数 来高效完成,复杂度是 级别。
- 计算 : 使用多项式指数函数算法 (NTT实现),得到 的系数 。
- 计算答案: 对于每个查询 ,最终答案就是 。我们可以预先计算出所有 的答案。
这套流程下来,我们就可以在 的时间复杂度内解决所有查询啦!是不是感觉充满了智慧的光芒,喵~
代码实现
下面就是本喵根据上面的思路,精心为大家准备的代码实现啦!注释很详细的哦,希望能帮到大家,喵~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
// MOD is a prime number: 998244353
const int MOD = 998244353;
const int NTT_PRIMITIVE_ROOT = 3;
const int MAXN = 200005;
// Fast power function
long long power(long long base, long long exp) {
long long res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % MOD;
base = (base * base) % MOD;
exp /= 2;
}
return res;
}
// Modular inverse
long long modInverse(long long n) {
return power(n, MOD - 2);
}
namespace Poly {
std::vector<int> rev;
std::vector<long long> roots;
int ntt_limit = 1;
void ensure_ntt_limit(int limit) {
if (ntt_limit >= limit) return;
int old_limit = ntt_limit;
ntt_limit = 1;
while (ntt_limit < limit) ntt_limit <<= 1;
rev.resize(ntt_limit);
for (int i = 0; i < ntt_limit; ++i) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (ntt_limit >> 1) : 0);
}
roots.resize(ntt_limit);
for (int len = 2; len <= ntt_limit; len <<= 1) {
long long w_len = power(NTT_PRIMITIVE_ROOT, (MOD - 1) / len);
roots[len / 2] = 1;
for (int i = len / 2 + 1; i < len; ++i) {
roots[i] = (roots[i - 1] * w_len) % MOD;
}
}
}
void ntt(std::vector<long long>& a, bool invert) {
int n = a.size();
for (int i = 0; i < n; ++i) {
if (i < rev[i]) std::swap(a[i], a[rev[i]]);
}
for (int len = 2; len <= n; len <<= 1) {
for (int i = 0; i < n; i += len) {
for (int j = 0; j < len / 2; ++j) {
long long u = a[i + j];
long long v = (a[i + j + len / 2] * roots[len / 2 + j]) % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
}
}
}
if (invert) {
std::reverse(a.begin() + 1, a.end());
long long inv_n = modInverse(n);
for (long long& x : a) {
x = (x * inv_n) % MOD;
}
}
}
std::vector<long long> multiply(std::vector<long long> a, std::vector<long long> b) {
int sz = a.size() + b.size() - 1;
int limit = 1;
while (limit < sz) limit <<= 1;
ensure_ntt_limit(limit);
a.resize(limit);
b.resize(limit);
ntt(a, false);
ntt(b, false);
for (int i = 0; i < limit; ++i) a[i] = (a[i] * b[i]) % MOD;
ntt(a, true);
a.resize(sz);
return a;
}
std::vector<long long> inverse(const std::vector<long long>& a, int deg) {
if (deg == 0) return {};
std::vector<long long> b = {modInverse(a[0])};
int current_deg = 1;
while (current_deg < deg) {
current_deg <<= 1;
std::vector<long long> a_slice(current_deg);
for(int i = 0; i < std::min((int)a.size(), current_deg); ++i) a_slice[i] = a[i];
int limit = current_deg * 2;
ensure_ntt_limit(limit);
std::vector<long long> b_ntt = b;
b_ntt.resize(limit);
ntt(b_ntt, false);
std::vector<long long> a_ntt = a_slice;
a_ntt.resize(limit);
ntt(a_ntt, false);
for (int i = 0; i < limit; ++i) {
b_ntt[i] = (2 - (a_ntt[i] * b_ntt[i]) % MOD + MOD) % MOD * b_ntt[i] % MOD;
}
ntt(b_ntt, true);
b.resize(current_deg);
for(int i = 0; i < current_deg; ++i) b[i] = b_ntt[i];
}
b.resize(deg);
return b;
}
std::vector<long long> derivative(const std::vector<long long>& a) {
if (a.empty()) return {};
std::vector<long long> res(a.size() - 1);
for (size_t i = 1; i < a.size(); ++i) {
res[i - 1] = (a[i] * i) % MOD;
}
return res;
}
std::vector<long long> integral(const std::vector<long long>& a) {
std::vector<long long> res(a.size() + 1, 0);
for (size_t i = 0; i < a.size(); ++i) {
res[i + 1] = (a[i] * modInverse(i + 1)) % MOD;
}
return res;
}
std::vector<long long> logarithm(const std::vector<long long>& a, int deg) {
if (deg == 0) return {};
auto deriv_a = derivative(a);
auto inv_a = inverse(a, deg);
auto res = multiply(deriv_a, inv_a);
res.resize(deg - 1);
return integral(res);
}
std::vector<long long> exponentiation(const std::vector<long long>& a, int deg) {
if (deg == 0) return {};
std::vector<long long> b = {1};
int current_deg = 1;
while (current_deg < deg) {
current_deg <<= 1;
auto log_b = logarithm(b, current_deg);
std::vector<long long> next_b(current_deg);
for (int i = 0; i < current_deg; ++i) {
next_b[i] = (a[i] - log_b[i] + MOD) % MOD;
}
next_b[0] = (next_b[0] + 1) % MOD;
b = multiply(b, next_b);
b.resize(current_deg);
}
b.resize(deg);
return b;
}
} // namespace Poly
long long fact[MAXN], invFact[MAXN];
void precompute_factorials(int n) {
fact[0] = 1;
invFact[0] = 1;
for (int i = 1; i <= n; ++i) {
fact[i] = (fact[i - 1] * i) % MOD;
invFact[i] = modInverse(fact[i]);
}
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
long long k;
std::cin >> t >> k;
std::vector<int> queries(t);
int max_n = 0;
for (int i = 0; i < t; ++i) {
std::cin >> queries[i];
if (queries[i] > max_n) {
max_n = queries[i];
}
}
precompute_factorials(max_n);
long long K_exp = (k + 1) % (MOD - 1);
if (K_exp == 0) K_exp = MOD - 1;
// Step 2: Construct F(x)
std::vector<long long> poly_F(max_n + 1);
for (int i = 0; i <= max_n; ++i) {
poly_F[i] = power(invFact[i], K_exp);
}
// Step 3: Compute H(x) = ln(F(x))
auto poly_H = Poly::logarithm(poly_F, max_n + 1);
// Step 4: Compute D(x)
std::vector<long long> poly_D(max_n + 1, 0);
for (int r = 1; r <= max_n; ++r) {
if (poly_H[r] == 0) continue;
for (int i = 1; r * i <= max_n; ++i) {
long long n_over_r_K_r = power(modInverse(i), (K_exp * (long long)r) % (MOD - 1));
long long term = (poly_H[r] * n_over_r_K_r) % MOD;
poly_D[r * i] = (poly_D[r * i] + term) % MOD;
}
}
// Step 5: Compute B(x) = exp(D(x))
auto poly_B = Poly::exponentiation(poly_D, max_n + 1);
// Step 6: Precompute all answers
std::vector<long long> answers(max_n + 1);
for (int n = 1; n <= max_n; ++n) {
long long n_fact_K = power(fact[n], K_exp);
answers[n] = (n_fact_K * poly_B[n]) % MOD;
}
for (int n : queries) {
std::cout << answers[n] << "\n";
}
return 0;
}复杂度分析
时间复杂度: ,其中 是查询中最大的 。
- 预计算阶乘和逆元需要 。
- 构造多项式 需要 。
- 计算多项式对数 需要 。
- 计算多项式 的系数需要 次运算,每次运算包含一个快速幂,所以总的是 。
- 计算多项式指数 需要 。
- 计算最终答案需要 。
- 整个算法的瓶颈在于计算 的系数和多项式运算,所以总时间复杂度是 。但由于 很大,我们对 取模,所以可以看作 。
空间复杂度: 。
- 我们需要存储几个大小为 的多项式()以及阶乘等预处理数组。NTT 过程中需要临时空间,但也在 范围内。
知识点总结
这道题是群论、组合数学和多项式算法的完美结合,做出来之后一定会让你成就感满满的,喵~
- 群论与组合: 理解对称群 的共轭类和轮换结构的关系是解题的第一步。将问题从对排列求和转化为对共轭类(或整数分拆)求和是关键的简化。
- 生成函数: 面对复杂的组合求和式,生成函数是我们的最强武器。特别是将乘积关系转化为卷积,再通过 技巧处理乘积形式,是解决这类问题的经典思路。
- 多项式全家桶: 本题的核心计算依赖于一系列多项式操作,包括 NTT(快速数论变换)、多项式求逆、求对数、求指数。熟练掌握这些板子是解决高难度组合计数问题的基础。
- 算法优化: 通过 和 的关系,我们将计算许多个 的复杂问题,转化为了计算一次 和一次 的求和,大大提高了效率。
希望这篇题解能帮助你更好地理解这道题的精髓!下次再遇到难题,也要像小猫一样,充满好奇心和勇气去挑战哦!喵~