集合幂级数板子

我说这其实不难,我做不明白可能是因为我太蠢了。

一些不严谨的定义

我们知道有个东西叫多项式,多项式 \(f(x) = \sum\limits_{i = 0}^{n} a_i x^{i}\)

先引出一个新的概念:形式幂级数。这个东西与多项式类似,区别在于它的系数数量是无限的,即,形式幂级数 \(F(x) = \sum\limits_{i = 0}^{\infty} a_i x^i\)。同时,为了表示幂级数的每一项,我们定义 \([x^i] F(x) = a_i\)

关于形式幂级数有啥用?我也不到啊

接下来,仿照形式幂级数,我们定义集合幂级数:\(F(x) = \sum\limits_{i = 0}^{\infty} a_i x^i\)。其与形式幂级数的区别是,形式幂级数的乘法与多项式乘法类似,而集合幂级数的乘法则类似于子集卷积(将第 \(i\) 项视为 \(i\) 的二进制位所表示的集合)。算法竞赛中,我们一般认为集合幂级数存在最高项,即集合幂级数满足 \(\forall i > n, [x^i] F(x) = 0\)。通过引入集合幂级数,我们做到了重定义多项式乘法为自己卷积的事情。

幂级数的平方,加减,\(\text{exp}\)\(\text{ln}\) 运算得到的均为幂级数。

集合幂级数半在线子集卷积

考虑如下问题:给定集合幂级数 \(g\),要求集合幂级数 \(f\),其中 \(f\) 的递推式为 \(f_S = \varphi(S) \sum\limits_{T \in S} f_T g_{S - T}\),其中 \(\varphi(S)\) 是一个可以快速计算的函数。显然该递推过程与子集卷积的区别是,我们要一定程度上在线地做它,故将其称为半在线子集卷积。

做法:考虑仿照子集卷积,引入辅助元。定义 \(F_{i, S} = \sum [|S| = i] f_S\)\(G_{i, S} = \sum [|S| = i] g_S\)。引入辅助幂级数 \(H_{i, S}\),我们会将计算的贡献存入 \(H\) 当中。

考虑从小到大枚举 \(i\),并考虑 \(F_i\) 的贡献。对于当前处理的 \(i\),假定 \(F_i\) 是已经处理好的真实值,先将其 \(\text{fwt}\) 一遍,再对于所有的 \(G_i\),将新的 \(F_i\)\(\text{fwt}(G_i)\) 相乘,计算贡献 \(H_{i + j, s} \leftarrow F_{i, S} G_{j, S}\)(贡献式中的 \(F\)\(G\) 均为 \(fwt\) 后的)。贡献计算结束之后,考虑当前 \(H_{i + 1}\) 的值必定是真实值,将其 \(ifwt\) 一遍并计算真实的 \(F_{i + 1}\) 即可。

关于复杂度,设集合大小不超过 \(n\),则你要枚举一遍大小,每次枚举的内层的时间复杂度是 \(O(2^n n)\) 的,因此总时间复杂度为 \(O(2^n n^2)\)

P12232 【模板】集合幂级数求逆

半在线子集卷积模板题,将式子化为半在线子集卷积的通用格式并计算即可。

P12230 【模板】集合幂级数 exp / P13843 【模板】集合幂级数 exp(非素数模数)

题意:给定集合幂级数 \(f(x)\),保证 \([x^0]f(x) = 0\),求 \(e^{f(x)}\)

考虑泰勒展开,我们有 \(e^{f(x)} = \sum\limits_{n \ge 0} \frac{f^n(x)}{n!}\)

考虑这个式子的组合意义,将 \(x\) 视为集合,即可将式子理解为:有 \(n\) 个元素,每个集合 \(S\) 有权值 \(f_S\),其中空集的权值为 \(0\)。定义一个集合的划分方案的贡献为,划分的所有子集的权值之积。对全集的每个子集,求它的所有划分方案的贡献之和,划分出的集合是无序的,即改变划分出的集合的顺序,并不会形成新的划分方案。

考虑这个咋转移,设 \(F_S\) 表示集合 \(S\) 的答案,因为集合是无序的,因此考虑钦定转移顺序。我们钦定最后枚举的子集是包含当前集合的 \(\text{lowbit}\),则有转移:

\[F_S = \sum\limits_{\varnothing \neq T \nsubseteq S, \text{lowbit(S)} \in T} f_T F_{S - T} \]

考虑这个转移与半在线子集卷积的区别:我们有一个 \(\text{lowbit}(S) \in T\) 的限制,于是考虑魔改对 \(f\) 做的 \(\text{fwt}\),改成只有 \(\text{lowbit}(i + j) = \text{lowbit}(i + j + k)\) 的时候才转移。我们知道 \(\text{fwt}_S(w)\) 表示的是,\(\sum\limits_{T \subseteq S} w_T\),而在魔改之后,新的 \(\text{fwt}_S(w)\) 存的显然就是 \(\sum\limits_{T \subseteq S, \text{lowbit}(T) = \text{lowbit}(S)} w_T\),而这就是我们所需要的 \(\text{fwt}(f)\)。同样可以发现对 \(F\)\(\text{ifwt}\) 也需要改变,就是新的 \(\text{fwt}\) 做逆变换。

这个东西显然是支持任意模数的。

P13843 代码:

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define mid ((l + r) >> 1)
#define lowbit(x) ((x) & -(x))

using namespace std;

constexpr int N = 20, mod = 998244353;

#define Z unsigned long long

int n;
Z f[N + 1][1 << N], g[N + 1][1 << N], h[N + 1][1 << N];

void fwt(Z a[], Z f = 1) {
	for (int w = 2, k = 1; w <= (1 << n); w <<= 1, k <<= 1) {
		for (int i = 0; i < (1 << n); i += w) {
			for (int j = 0; j < k; ++j) {
				a[i + j + k] += a[i + j] * f;
			}
		}
	}
}

void fwt2(Z a[], Z f = 1) {
	for (int w = 2, k = 1; w <= (1 << n); w <<= 1, k <<= 1) {
		for (int i = 0; i < (1 << n); i += w) {
			for (int j = 0; j < k; ++j) {
				if (lowbit(i + j) == lowbit(i + j + k)) {
					a[i + j + k] += a[i + j] * f;
				}
			}
		}
	}
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	cin >> n;
	for (int i = 0; i < (1 << n); ++i) {
		cin >> f[__builtin_popcount(i)][i];
	}
	
	g[0][0] = 1;
	for (int i = 0; i <= n; ++i) {
		fwt2(f[i]);
	}
	for (int i = 0; i < n; ++i) {
		fwt(g[i]);
		for (int j = 1; i + j <= n; ++j) {
			for (int k = 0; k < (1 << n); ++k) {
				h[i + j][k] += f[j][k] * g[i][k];
			}
		}
		fwt(g[i], -1);
		fwt2(h[i + 1], -1);
		for (int j = 0; j < (1 << n); ++j) {
			g[i + 1][j] = h[i + 1][j];
		}
	}
	
	for (int i = 0; i < (1 << n); ++i) {
		cout << g[__builtin_popcount(i)][i] << " \n"[i == (1 << n) - 1];
	}
	
	return 0;
}

P12231 【模板】集合幂级数 ln / P13844 【模板】集合幂级数 ln(非素数模数)

考虑 \(\text{ln}\) 本质是 \(\text{exp}\) 的逆变换,组合意义就是,对于每个集合 \(S\) 告诉你它的所有划分方案的价值之和,求每个集合 \(S\) 的权值。

\(g_S\) 表示集合 \(S\) 的划分方案的价值和,\(f_S\) 表示集合 \(S\) 的权值,则容易得到递推式:

\[f_S = g_S - \sum\limits_{\varnothing \neq T \nsubseteq S, \text{lowbit}(T) = \text{lowbit}(S)} f_T g_{S - T} \]

类似集合幂级数 \(\text{exp}\) 地做即可。时间复杂度 \(O(2^n n^2)\),同样支持任意模数。

P13844 代码:

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define mid ((l + r) >> 1)
#define MID ((l + r + 1) >> 1)
#define lowbit(x) ((x) & -(x))

using namespace std;

constexpr int N = 20;

#define Z unsigned long long

int n;
Z f[N + 1][1 << N], g[N + 1][1 << N], h[N + 1][1 << N];

void fwt(Z a[], Z f = 1) {
	for (int w = 2, k = 1; w <= (1 << n); w <<= 1, k <<= 1) {
		for (int i = 0; i < (1 << n); i += w) {
			for (int j = 0; j < k; ++j) {
				a[i + j + k] += a[i + j] * f;
			}
		}
	}
}

void fwt2(Z a[], Z f = 1) {
	for (int w = 2, k = 1; w <= (1 << n); w <<= 1, k <<= 1) {
		for (int i = 0; i < (1 << n); i += w) {
			for (int j = 0; j < k; ++j) {
				if (lowbit(i + j) == lowbit(i + j + k)) {
					a[i + j + k] += a[i + j] * f;
				}
			}
		}
	}
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	cin >> n;
	for (int i = 0; i < (1 << n); ++i) {
		Z w;
		cin >> w;
		f[__builtin_popcount(i)][i] = w;
		g[__builtin_popcount(i)][i] = w;
	}
	
	g[0][0] = 0;
	for (int i = 0; i <= n; ++i) {
		fwt(f[i]);
	}
	for (int i = 0; i < n; ++i) {
		fwt2(g[i]);
		for (int j = 1; i + j <= n; ++j) {
			for (int k = 0; k < (1 << n); ++k) {
				h[i + j][k] += f[j][k] * g[i][k];
			}
		}
		fwt2(g[i], -1);
		fwt2(h[i + 1], -1);
		for (int j = 0; j < (1 << n); ++j) {
			g[i + 1][j] -= h[i + 1][j];
		}
	}
	
	for (int i = 0; i < (1 << n); ++i) {
		cout << g[__builtin_popcount(i)][i] << " \n"[i == (1 << n) - 1];
	}
	
	return 0;
}
posted @ 2026-04-10 21:34  zyb_txdy  阅读(20)  评论(0)    收藏  举报