lonely - 题解
标签与难度
标签: 快速沃尔什变换(FWT), 子集卷积, 生成函数, 动态规划, 组合数学 难度: 2500
题目大意喵~
你好呀,指挥官!这道题是要我们计算一些和集合划分有关的奇妙数值,喵~
我们有 个点,从 到 编号。对于 个非空点集 ,我们有两个给定的数组:权值 和贡献值 。
接着,题目定义了一个函数 ,它表示将集合 划分成 个非空、互不相交的子集的所有方案中,每个方案的权值之积的总和。 举个例子,如果把 划分成 (其中 , , ),那么这个划分方案的权值就是 。 就是所有这些不同划分方案的权值加起来的总和。
我们的最终任务是,对于每个 (从 到 ),计算出下面这个式子的值:
其中 是包含所有 个点的全集。所有计算都要在模 下进行。
解题思路分析
这道题看起来好复杂,又是集合划分又是求和的,但别怕,我我来带你一步步解开它的神秘面纱,喵~
核心问题:集合划分与子集卷积
首先,我们来仔细看看 的定义。它是在对集合 进行划分。这种对集合进行划分并求权值乘积和的形式,是一个非常典型的组合结构,通常和子集卷积以及生成函数有关。
让我们定义一种运算,叫做子集卷积,记作 *。对于两个关于子集的函数 和 ,它们的子集卷积 定义为:
这个式子计算了所有将集合 分成两部分 和 的方案,并将两部分的函数值乘起来再求和。
现在回到 。
- 当 时,只有一种划分方法,就是 本身。所以 。
- 当 时,我们需要把 分成两个非空子集 和 。这和子集卷积很像! 是所有 的和。如果我们计算 ,会发现每个无序对 被计算了两次(一次是 ,一次是 )。所以 。
- 推广开来,如果我们定义 为 与自身进行 次子集卷积,那么 就是 中 对应的系数,再除以 (因为 个集合的划分是无序的)。
所以,我们可以把 (表示所有 构成的数组)看作是 数组的某种 "指数" 运算的结果。
引入快速沃尔什变换 (FWT)
直接计算子集卷积非常慢,复杂度高达 。而要计算 次方,总复杂度会是 或者使用快速幂优化到 ,对于 来说是绝对无法接受的。
但是,对于一类特殊的卷积——子集和卷积(OR卷积)、子集交卷积(AND卷积)和子集异或卷积(XOR卷积),我们可以使用一种叫快速沃尔什变换 (FWT) 的算法,在 的时间内完成。
虽然子集卷积本身不能直接用 FWT 加速,但它和 FWT 有着深刻的联系。一个关键的性质是,我们可以利用 FWT(这里特指子集和变换,也叫 Zeta 变换或 SOS DP)来简化问题。
我们定义一个数组 的 FWT(子集和变换)为 ,其中:
这个变换是线性的,并且有一个神奇的性质:它能将一些复杂的集合运算转化为简单的逐点运算。对于集合划分问题,有这样一个美妙的结论:
令 和 。那么它们之间满足:
比较等式两边 的系数,我们得到一个超级重要的关系!
这意味着,只要我们对 数组做一次 FWT 得到 ,我们就可以轻松地在变换域中得到 ,而不需要进行任何昂贵的子集卷积!
求解最终答案
我们的目标是计算 。这是一个点积的形式。我们已经知道了 ,但如何利用它来求和呢?直接对 做逆 FWT 得到 再求和,复杂度又回去了。
这里需要一个关于 FWT 的求和恒等式。对于两个数组 和 ,它们的点积满足:
其中 是全集, 是 的子集和变换(Sum over Subsets),而 是 的超集和变换(Sum over Supermasks)。
这两种变换都可以用 FWT 在 的时间内计算出来。
把这个恒等式应用到我们的问题上,令 ,:
现在的问题是, 是什么? 奇迹再次发生!超集和变换与子集划分也有着类似的美妙关系:
其中 。
把这个代入我们的答案公式中,就得到了最终的计算方法!
这个公式看起来吓人,但它告诉我们一个清晰的算法步骤:
- 对数组 进行子集和 FWT,得到 。
- 对数组 进行超集和 FWT,得到 。
- 预计算阶乘的逆元 。
- 对于每个 ,遍历所有子集 ,累加
(-1)^|S| * hat_b[S] * (check_a[U \ S])^i。 - 最后将累加和乘以 就是 。
整个算法的瓶颈在于两次 FWT 和最后的求和循环,总时间复杂度为 ,对于 来说完全可以接受,喵~
代码实现
下面就是我根据上面的思路,精心为你准备的代码啦!注释写得很详细,希望能帮助你理解哦~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
// 定义模数
const int MOD = 998244353;
const int MAXN = 20;
// 快速幂,用于计算逆元
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;
}
// 快速沃尔什变换 - 子集和 (SOS DP)
// in-place 实现,op=1 为正变换,op=-1 为逆变换
void FWT_SOS(vector<long long>& a, int n, int op) {
int size = 1 << n;
for (int i = 0; i < n; ++i) {
for (int mask = 0; mask < size; ++mask) {
if (mask & (1 << i)) {
long long term = a[mask ^ (1 << i)];
if (op == 1) {
a[mask] = (a[mask] + term) % MOD;
} else {
a[mask] = (a[mask] - term + MOD) % MOD;
}
}
}
}
}
// 快速沃尔什变换 - 超集和
// in-place 实现, op=1 为正变换, op=-1 为逆变换
void FWT_Supermask(vector<long long>& a, int n, int op) {
int size = 1 << n;
for (int i = 0; i < n; ++i) {
for (int mask = size - 1; mask >= 0; --mask) {
if (mask & (1 << i)) {
long long term = a[mask ^ (1 << i)];
if (op == 1) {
a[mask] = (a[mask] + term) % MOD;
} else {
a[mask] = (a[mask] - term + MOD) % MOD;
}
}
}
}
}
int main() {
// 加速输入输出,喵~
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
int size = 1 << n;
vector<long long> a(size, 0), b(size, 0);
// 读入 a_S,下标 S > 0
for (int i = 1; i < size; ++i) {
cin >> a[i];
}
// 读入 b_S,下标 S > 0
for (int i = 1; i < size; ++i) {
cin >> b[i];
}
// 预计算阶乘和它们的逆元
vector<long long> inv_fact(n + 1);
vector<long long> fact(n + 1);
fact[0] = 1;
inv_fact[0] = 1;
for (int i = 1; i <= n; ++i) {
fact[i] = (fact[i - 1] * i) % MOD;
inv_fact[i] = power(fact[i], MOD - 2);
}
// 1. 对 b 进行子集和 FWT
vector<long long> hat_b = b;
FWT_SOS(hat_b, n, 1);
// 2. 对 a 进行超集和 FWT
vector<long long> check_a = a;
// 注意!超集和变换等价于对 bit-reversed 的数组做子集和变换
// 或者直接写超集和的循环,这里为了清晰就直接写了
FWT_Supermask(check_a, n, 1);
// 3. 计算最终答案
vector<long long> ans(n + 1, 0);
int full_mask = size - 1;
// 为了效率,我们交换内外循环
// 外层循环遍历 S,内层循环计算对所有 Ans_i 的贡献
for (int s = 0; s < size; ++s) {
long long b_term = hat_b[s];
if (__builtin_popcount(s) % 2 != 0) {
b_term = (MOD - b_term) % MOD;
}
long long a_term_base = check_a[full_mask ^ s];
long long a_power = 1;
for (int i = 1; i <= n; ++i) {
a_power = (a_power * a_term_base) % MOD;
long long contribution = (b_term * a_power) % MOD;
ans[i] = (ans[i] + contribution) % MOD;
}
}
// 4. 乘以 1/i!
for (int i = 1; i <= n; ++i) {
ans[i] = (ans[i] * inv_fact[i]) % MOD;
}
// 5. 输出结果
for (int i = 1; i <= n; ++i) {
cout << ans[i] << (i == n ? "" : " ");
}
cout << endl;
return 0;
}复杂度分析
时间复杂度: ,其中 是点的数量。
- 两次 FWT (子集和与超集和) 的复杂度都是 。
- 预计算阶乘逆元是 。
- 最后计算答案的循环,外层是 次,内层是 次,所以也是 。
- 因此总时间复杂度由 FWT 和求和部分主导,为 。
空间复杂度: 。
- 我们需要存储
a,b数组以及它们的变换结果,每个数组的大小都是 。所以空间复杂度是 。
- 我们需要存储
知识点总结
这道题是集合动态规划问题中的一个经典模型,融合了多个重要的算法思想,很值得好好回味一下,喵~
- 集合划分与生成函数: 问题的核心是集合划分,这种组合结构通常可以和指数型生成函数联系起来。
f_{S,i}的形式就是多项式指数函数的系数。 - 子集卷积: 集合划分的递推关系天然地形成了子集卷积的形式。虽然我们最终没有直接计算它,但理解这个概念是找到正确方向的第一步。
- 快速沃尔什变换 (FWT): FWT 是解决各类子集相关卷积问题的利器。本题中,我们利用了 FWT 的两个变种:
- 子集和变换 (Zeta Transform / SOS DP):
- 超集和变换 (Dual Zeta Transform):
- FWT 与集合运算的关系: 最关键的知识点!FWT 可以将复杂的集合运算(如子集卷积的指数运算)转化为变换域中简单的逐点运算。我们利用了 和 这两个核心性质。
- 求和恒等式: 为了在变换域中计算点积,我们使用了一个优美的恒等式 ,它将两个不同类型的 FWT 巧妙地联系在了一起,避免了代价高昂的逆变换。
希望这篇题解能帮到你,如果还有不明白的地方,随时可以再来问我哦,喵~!