集合幂级数(1):基础与图计数

本文都是比较简单基础的集合幂级数知识,难度不太高。

如果你下文听见我说“子集划分”就是说把 \(S\) 划分成若干个不交集合的并,这个集合族一般来说是无序的,但是计算起来不方便。常见的规避方法一种是每次找 lowbit 所在的那个位置,然后 dp 枚举子集算增量。一种是 exp-ln 我所有都枚举然后除以 \(k!\)。容易根据语境判断。

主要参考 chenxinyang2006 老师的 apio 课件,还有这篇文章

提前声明:本人学术水平为负数,如果有任何错误请速速踹我,恳请各位不吝赐教鸭 qwq。

集合幂级数

与形式幂级数类似,将集族构造为一个函数用于分析,不同的是形式幂级数中幂相乘指数运算是加法,而集合幂级数中指数运算为集合运算,常用的有与,或,异或与子集卷积。

FWT

FWT 一般是针对位运算的卷积。我们记这种运算为 \(*\)(它当然可以是与或异或之类的东西 qwq)。

FWT 希望我们对此卷积,对于给定的序列 \(A, B\) 要求出 \(C_i = \sum\limits_{j*k = i}A_jB_k\)。我们不妨令序列长度 \(n\)\(2\) 的整数次幂。(如果不够用 \(0\) 补位)。w

利用类似 FFT 之类的思想,我们将序列 \(A\)\(F\) 进行一个对应关系。将 \(A\) 对应到 \(FA\) 上,\(B\) 对应到 \(FB\) 上,\(FC_{i} = FA_{i}FB_{i}\),最后再把 \(FC\) 还原回 \(C\)

我们考察一下这个对应关系,假设这是一个线性关系,\(F_i = \sum\limits_{j = 0}^{n - 1}c(i, j)A_j\),这里的 \(c(i, j)\) 是我们自己规定的系数。接下来我们来分析与创造一下 \(c(i, j)\) 的性质。


第一条必须满足的性质:我们从 \(FC\) 的定义出发,得到 \(c\) 必须满足的数量关系。

\[FC_i = \sum\limits_{j = 0}^{n - 1}\sum\limits_{k = 0}^{n - 1}c(i, j*k)A_jB_k \\ = \sum\limits_{j = 0}^{n - 1}c(i, j)A_j\sum\limits_{k = 0}^{n - 1}c(i, k)B_k \\ = \sum\limits_{j = 0}^{n - 1}\sum\limits_{k = 0}^{n - 1}c(i, j)c(i, k)A_jB_k \]

第一行是 \(FC\) 的定义,第二行是利用 \(A,B\) 卷积得到的,经过比对我们发现应该要满足 \(c(i, j*k) = c(i, j)c(i, k)\)


如果我们人工构造的 \(c\) 满足上面的性质,那么它就可以这样来计算了。但是很严重的问题在于这样 \(c\)\(O(n^2)\) 太多了,我们希望我们自己规定的 \(c\) 尽量少。

加之这是位运算卷积,所以我们来人工创造一条性质:我们假设 \(x, y\) 中二进制最高位分别是 \(n, m\),去掉最高位后变成 \(x', y'\),那么有 \(c(x, y) = c(n, m)c(x', y')\)。等价的描述是:对于 \(x, y\) 我们令它二进制第 \(i\)\(x_i\)\(y_i\),那么有 \(c(x, y) = \prod c(x_i, y_i)\)。此时我们只需要构造出合适的 \(c(0, 0), c(0, 1), c(1, 0), c(1, 1)\) 即可。此时只需要满足 \(c(i, j)c(i, k) = c(i, j * k)\)

例:令 \(*\) 代表异或。那么要构造的 \(c\) 要满足:

  • \(c(0, 0)c(0, 0) = c(0, 0)\)
  • \(c(0, 0)c(0, 1) = c(0, 1)\)
  • \(c(0, 1)c(0, 1) = c(0, 0)\)
  • \(c(1, 0)c(1, 0) = c(1, 0)\)
  • \(c(1, 0)c(1, 1) = c(1, 1)\)
  • \(c(1, 1)c(1, 1) = c(1, 0)\)
  • 一个合法的解是 \(c(0, 0) = 1, c(0, 1) = 1, c(1, 0) = 1, c(1, 1) = -1\)

显然,满足上面两个条件的 \(c\) 有非常多,比如我全部都是 \(1\)。但是这其实是不行的,我们将引入 \(c\) 的第三条性质。这与还原有关。


假设我们拥有一个合法的 \(c\),现在我们运用魔法很快的将 \(A\) 变成了 \(F\),那么 \(F\) 怎么还原回 \(A\) 呢?

\(A\)\(F\) 之间显然也是线性关系。不妨令 \(A_i = \sum\limits_{j = 0}^{n - 1}d(i, j)F_j\)\(d\) 不满足 \(c\) 第一条性质,但是第二条性质是成立的。

这是一个反演关系,所以 \(c\) 构造成的矩阵和 \(d\) 构造成的矩阵互逆。既然如此,那么 \(c\)\(d\) 最左上角的 \(2\times 2\) 矩阵,也就是 \(\begin{pmatrix}c(0, 0)&c(0, 1)\\c(1, 0)&c(1, 1) \end{pmatrix}\)\(\begin{pmatrix}d(0, 0)&d(0, 1)\\d(1, 0)&d(1, 1) \end{pmatrix}\) 互逆。这是 \(c, d\) 大矩阵互逆的一个必要条件。实际上这也是一个充分的条件。可以通过不断扩展左上角的正方形矩形归纳法证明。读者自证不难实际上是我懒得证明了,在此略去。

所以我们只需要求出 \(c\) 的逆矩阵 \(d\) 即可。那么这是就显然可以得到 \(c\) 的第三条性质:要有逆矩阵。这就可以解释为什么 \(c\) 不能全取 \(1\) 或者全取 \(0\),因为此时 \(c\) 没有逆矩阵。

我们称 \(\begin{pmatrix}c(0, 0)&c(0, 1)\\c(1, 0)&c(1, 1) \end{pmatrix}\) 为一个位矩阵。


此时计算 \(F\) 仍然是 \(O(n^2)\) 的,非常劣。仿照 FFT 的思路我们来分治。

\[F_i = \sum\limits_{j = 0}^{n - 1}c(i, j)A_j \\ = \sum\limits_{j = 0}^{\frac{n}{2} - 1}c(i, j)A_j + \sum\limits_{j = \frac{n}{2}}^{n - 1}c(i, j)A_j \\ = c(i_0, 0)\sum\limits_{j = 0}^{\frac{n}{2} - 1}c(i', j')A_j + c(i_0, 1)\sum\limits_{j = \frac{n}{2}}^{n - 1}c(i', j')A_j \]

其中 \(i_0\) 代表 \(i\) 最高位,\(i', j'\) 代表 \(i, j\) 去掉最高位的值。第三个等式是根据 \(c\) 第二条性质。

我们发现这两边分别是一个 \(i\) 的位数减一,值域减半,但是形式类似的子问题。可以分治。一共 \(O(\log_2 n)\) 层,每层 \(O(n)\),复杂度 \(O(n\log_2 n )\),这与 FFT 是十分相似的。

一份笨笨的代码

void fwt(ll *arr, ll c[2][2]) {
	for(int k = 1; k < (1 << n); k <<= 1) {
		for(int i = 0; i < (1 << n); i += (k << 1)) {
			for(int j = 0; j < k; j++) {
				const ll rest = arr[i + j];
				arr[i + j] = (rest * c[0][0] + arr[i + j + k] * c[0][1]) % Mod;
				arr[i + j + k] = ((rest * c[1][0] + arr[i + j + k] * c[1][1]) % Mod + Mod) % Mod;
			}
		}
	}
}

or,and,xor 的卷积

or,and,xor 卷积的位矩阵

or 的位矩阵通常用 \(\begin{pmatrix}1 & 0\\ 1 & 1\end{pmatrix}\),逆矩阵是 \(\begin{pmatrix}1 & 0 \\ -1 & 1\end{pmatrix}\)

and 的位矩阵通常用 \(\begin{pmatrix}1 & 1\\ 0 & 1\end{pmatrix}\),逆矩阵是 \(\begin{pmatrix}1 & -1 \\ 0 & 1\end{pmatrix}\)

xor 的位矩阵通常用 \(\begin{pmatrix}1 & 1\\ 1 & -1\end{pmatrix}\),逆矩阵是 \(\begin{pmatrix}0.5 & 0.5 \\0.5 & -0.5\end{pmatrix}\)


or 其它意义

我们来观察一下 or 卷积中 \(c(i, j)\) 有没有什么组合意义之类的东西。

\(n, m\) 的二进制第 \(i\) 位分别为 \(n_i\)\(m_i\)\(c(n, m) = \prod_{i = 0}c(n_i, m_i)\)。我们发现这个式子等于 \(0\) 当且仅当存在 \(i\) 使得 \(x_i = 0, y_i = 1\)。也就是如果 \(m\subset n\),那么 \(c(n, m) = 1\),否则 \(c(n, m) = 0\)。换句话说,\(F_n = \sum\limits_{m}c(n, m)A_m = \sum\limits_{m \subset n} A_m\)

于是你还格外的发现这个东西很像高维前缀和一样的东西。


xor 其它意义

观察一下 xor 卷积中 \(c(i, j)\),注意到正变换中有:

\[F_i = \sum\limits_{j = 0}^{2^n - 1}(-1)^{|j\cap i|}A_j \]

逆变换中有:

\[F_i = \dfrac{1}{2^n}\sum\limits_{i = 0}^{2^n - 1}(-1)^{|j\cap i|}A_j \]


子集卷积

给定 \(A, B\),求这样的卷积:
\(C_{k} = \sum\limits_{i\cup j = k\\ i\cap j = \empty}A_iB_j\)

\(i\cup j = k, i\cap j = \empty \iff i\cup j = k, |i|+|j|=|k|\),于是我们定义 \(F_i(x) = \sum\limits_{j = 0}^{n}x^jA_j[|j|=i], G_i(x) = \sum\limits_{j = 0}^nx^jB_j[|j|=i]\)\(H_{k} = \sum\limits_{i=0}^kF_{i}G_{k - i}\),最后 \(C_k = [x^k]H_{|k|}\)

更为本质的看法是我们将多项式每一项看作是 \(x^Sy^t\)\(y\) 用于维护集合大小,于是子集卷积可以看作是第一维进行 or 卷积,第二维就是一般的加乘卷积,以 \(y\) 为主元,看作一个多项式,每一项系数是一个关于 \(x\) 的集合幂级数,然后进行卷积。

新设置一个元用于记录集合大小,这是一个常用的思想!

结合这样一个思想,我们以子集卷积为上指标运算模式,进行多项式求逆,求 ln 和求 exp,在子集卷积下它们分别有各自的组合意义。


求逆

\(y\) 为主元,每一项系数为一个关于 \(x^S\) 的集合幂级数,然后就是求逆:

\[G(y) = F^{-1}(y)\\G(y)F(y) = 1\\ g_0f_0 + \sum\limits_{i = 1}^n x^i(\sum\limits_{j = 0}^ig_jf_{i -j}) = 1 \]

于是 \(g_0 = f_0^{-1}\)\(g_i = \dfrac{ - \sum_{j = 0}^{i - 1}g_jf_{i - j}}{f_0}(i>0)\)


求 ln

如果以 \(y\) 为主元那么 \(F(x, y)\) 相当于每一项的系数都是一个一个关于 \(x\) 的多项式。设 \(\ln F(x) = G(x)\),接下来进行一点小推导:

\[e^G(x) = F(x) \\ G'(x)F(x) = F'(x) \\ \sum\limits_{i = 0}^{n - 1}x^i \sum\limits_{j = 0}^i(j+1)g_{j+1}f_{i - j} = \sum\limits_{i = 0}^{n - i}x^i(i+1)f_{i+1}\\ f_i = \dfrac{1}{i}\sum\limits_{j = 1}^i jg_jf_{i - j}\\ g_i = f_i - \dfrac{\sum_{j = 1}^{i - 1}jg_j f_{i - j}}{i} \]

即可。

求 exp

\(G(y) = e^{F(y)}\),做一些基本的推导。

\[G(y) = e^{F(y)}\\\Rightarrow G'(y) = F'(y)G(y) \\ \Rightarrow\sum\limits_{i = 0}^{n - 1} x^i(i+1)g_{i+1} = \sum\limits_{i = 0}^{n - 1}x^i\sum\limits_{j = 0}^{i}(j+1)f_{j+1}g_{i -j}\\ \Rightarrow g_i = \dfrac{1}{i}\sum\limits_{j = 1}^{i}jf_jg_{i - j} \]

即可。


贴一份我的四合一板子

namespace poly {
	void fwt(int *arr, bool tp) {
		for(int k = 1; k < (1 << n); k <<= 1)
			for(int i = 0; i < (1 << n); i += (k << 1)) {
				for(int j = 0; j < k; j++) {
					int rest = arr[i + j];
					if(!tp) 
						arr[i + j + k] = (rest + arr[i + j + k]) % Mod;
					else arr[i + j + k] = (arr[i + j + k] - rest + Mod) % Mod;
				}
			}
	}
	
	int f[N + 3][(1 << N) + 3], g[N + 3][(1 << N) + 3], h[N + 3][(1 << N) + 3];
	void submul(int *a, int *b, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = h[i][S] = 0;
			
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i],
			g[__builtin_popcount(i)][i] = b[i];
		for(int i = 0; i <= n; i++)
			fwt(f[i], 0), fwt(g[i], 0);
		
		for(int S = 0; S < (1 << n); S++)
			for(int i = 0; i <= n; i++)
				for(int j = 0; j <= i; j++)
					upd(h[i][S], 1ll * f[j][S] * g[i - j][S] % Mod);
		for(int i = 0; i <= n; i++) fwt(h[i], 1);
		for(int i = 0; i < (1 << n); i++) c[i] = h[__builtin_popcount(i)][i];
	}
	void exp(int *a, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = 0;
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i];
		
		for(int i = 0; i <= n; i++) fwt(f[i], 0);
		g[0][0] = 1;
		fwt(g[0], 0);
		for(int i = 1; i <= n; i++) {
			int invn = qpow(i, Mod - 2);
			for(int j = 1; j <= i; j++)
				for(int S = 0; S < (1 << n); S++)
					upd(g[i][S], 1ll * j * f[j][S] % Mod * g[i - j][S] % Mod);
			for(int S = 0; S < (1 << n); S++) g[i][S] = 1ll * g[i][S] * invn % Mod;
		}
		for(int i = 0; i <= n; i++) fwt(g[i], 1);
		
		for(int i = 0; i < (1 << n); i++)
			c[i] = g[__builtin_popcount(i)][i];
	}
	void ln(int *a, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = 0;
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i];
		
		for(int i = 0; i <= n; i++) fwt(f[i], 0);
		g[0][0] = 0;
		for(int i = 1; i <= n; i++) {
			int invn = qpow(i, Mod - 2);
			for(int S = 0; S < (1 << n); S++) {
				g[i][S] = 1ll * i * f[i][S] % Mod;
				for(int j = 1; j < i; j++)
					upd(g[i][S], Mod - 1ll * j * g[j][S] % Mod * f[i - j][S] % Mod);
				g[i][S] = 1ll * g[i][S] * invn % Mod;
			}
		}
		for(int i = 0; i <= n; i++) fwt(g[i], 1);
		
		for(int i = 0; i < (1 << n); i++)
			c[i] = g[__builtin_popcount(i)][i];
	} 
	void inv(int *a, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = 0;
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i];
		
		int invn = qpow(a[0], Mod - 2);
		for(int i = 0; i <= n; i++) fwt(f[i], 0);
		g[0][0] = invn; fwt(g[0], 0);
		for(int i = 1; i <= n; i++) {
			for(int S = 0; S < (1 << n); S++) {
				for(int j = 0; j <= i - 1; j++)
					upd(g[i][S], Mod - 1ll * g[j][S] * f[i - j][S] % Mod);
				g[i][S] = 1ll * g[i][S]	* invn % Mod;
			}
		}
		for(int i = 0; i <= n; i++) fwt(g[i], 1);
		
		for(int i = 0; i < (1 << n); i++)
			c[i] = g[__builtin_popcount(i)][i];
	}
}

组合意义

为什么单独提出这三种呢?

\(\sum\limits_{i = 0}G^i(x) = \dfrac{1}{1 - G(x)}\),这代表对于集合 \(S\) 进行子集划分,划分包含顺序的方案数。

\(\exp G(x) = \sum\limits_{i = 0}\dfrac{G^i(x)}{i!}\) 这代表划分不包含顺序的方案数。那么相应的 \(\ln G(x)\) 就代表只由自己贡献的的方案数。这似乎有点抽象,举个例子,如果 \(G(x)\) 是联通块的方案数,那么 \(\exp G(x)\) 就是连通块拼成图的方案数。


逐点牛顿迭代法

Tips:本部分纯属本人口胡,如果有不严谨不准确的地方欢迎拷打我。提前谢罪。

增量计算,每次考虑集合最高位 \(n\) 如果加入了会对集合产生什么贡献。优势在于:

  1. 有些时候确实增量考虑能够找到一个抓手。
  2. 部分问题中,因为上一轮涉及到的规模是 \((n - 1)\),于是所有规模之和是 \(O(2^n)\) 级别的,也许会降低复杂度。
  3. 在图计数里面,我们通常是考虑加点后连通块的性质。这样我们可以往往可以直接用子集划分刻画加入点后新连通块的形态。

经典卷积形式

对于 \(S\in [0, 2^n)\),已知 \(f, g\),求解 \(h_S\),有

\[h_S = \sum\limits_{\operatorname{lowbit}(S) \in T \subseteq S}f_Sg_{S - T} \]

如果 \(f, g\) 相等那么就是经典的 exp。但是这里不相等。

我们考虑增量从高到低加点,恰好在加点时计算含此为最低位的贡献。将所有集合按照最低位位数分类。考虑到最低位 \(m\),这个贡献相当好计算。

设上一轮中的答案 \(h_{m + 1, S}\),其中 lowbit 小于等于 \(m\) 的集合均 \(h_{m+1, S} = 0\)。在这一轮中 \(f_{m, S} = \begin{cases}f_S & \operatorname{lowbit}(S) = n \\ 0 & \operatorname{otherwise} \end{cases}\)\(g\) 直接保留,然后做子集卷积的结果,然后更新新加入的位即可。

一共 \(n\) 轮,每一次都要进行一个 \(O(m^22^m)\) 的计算,惊喜的发现 \(\sum\limits_{m = 1}^n m^22^m\)\(O(n^22^n)\) 级别的,所以最后也是 \(O(n^22^n)\) 的。

实现的时候可以考虑少 FWT 几次。

无根树计数

给定一个无向图 \(G\),求解对于每个子集 \(S\) 导出子图中树的数量。

增量加点,考虑当前加入最大值 \(n\) 对原来答案的影响。令上一轮的 gf 为 \(G_{n - 1}\),那么这一轮 \(G_{n, S}\) 考虑 \(S\) 的一个子集划分 \(T_1, T_2, \dots T_k\),最后是 \(\prod\limits_{i = 1}^k G_{n -1, T_i} c_{n, T_i}\),其中 \(c_{n, T_i}\)\(n\) 这个点和 \(T_i\) 这个点集之间的边数量。点集划分有序,所以可以用 exp 刻画。建立 \(H_S = G_{n - 1, S}c_{n, S}\) 那么 \(G_n = \exp H\)

因为每次做 exp 的范围不断扩大 2 倍,所以最终是 \(O(n^2 2^n)\) 的。

LOJ155. Tutte 多项式

非常厉害的一个东西。

求解

\[T_G(x, y) = \sum\limits_{A \subseteq E} (x - 1)^{k(A) - k(E)}(y - 1)^{k(A) + |A| - |V|} \]

\(x, y\) 可能为 \(1\),做法尽可能要规避对 \(0\) 的除法(要扩域虽然应该也可以做)

首先我们考虑 \(G\) 为联通图,不难发现不联通只要把每个连通分量的 Tutte 多项式乘起来即可。于是我们令 \(k(E) = 1\)

此时显然主要的问题是在对 \(k(A)\) 的处理上面,考虑枚举其中的每一个连通块。具体而言对于点集 \(S\),考虑将其子集划分为连通块 \(T_1, T_2, \dots, T_k\)。对于连通块内部,应该有 \(f_{T} = \sum\limits_{B}(x - 1)^0 (y - 1)^{|B| + 1 - |T|}\),其中 \(B\) 为使得 \(T\) 是连通图的边集,那么复合起来的答案 \(g_S\) 就应该有:

\[g_S = \sum\limits_{T_1, T_2, \dots, T_k} \dfrac{\prod_{i = 1}^k f_{T_i}}{k!}(x - 1)^{k - 1} \]

我们可以先对于 \((x - 1)f_S\) 求一个它的 exp 结果 \(h\),然后我们考虑 lowbit 所在位上 \(g_S = \sum\limits_{\operatorname{lowbit}(S) \in T \subseteq S} h_Tf_{S - T}\) 以此来放掉一个 \((x - 1)\)。我们可以用上面的做法来 \(O(n^22^n)\) 做这个经典卷积形式。

现在问题在于处理 \(f_S\)\(f_S = \sum\limits_{B}(y - 1)^{|B| - (|S| - 1)}\),要求 \(B\) 使得 \(S\) 联通,如何求解。

仍然考虑逐点牛顿迭代。

加入点 \(n\) 后,对于 \(S\) 进行子集划分:\(\{n\}, T_1, T_2, \dots, T_k\)。内部就是 \(\prod\limits_{i = 1}^k f_{T_i}\)。而对于新加入的边,令 \(n\)\(T\) 之间有 \(c(n, T)\) 条边,那么这部分的贡献就是:

\[\dfrac{\sum\limits_{i = 1}^{c(n, T)}\binom{c(n, T)}{i}(y - 1)^{i}}{y - 1} =\dfrac{y^{c(n, T)} - 1}{y - 1} \]

这是一个等比数列求和的形式。

此时应当 \(f_{n, S} = \sum\limits_{T_1, T_2, \dots, T_k}\dfrac{\prod_{i = 1}^k f_{n - 1, T_i}(\frac{y^{c(n, T_i)} - 1}{y - 1})}{k!}\)

为了规避掉后面这个可能涉及到除以 \(0\) 的操作,我们发现后面这一部分是一个等比数列求和。

这是一个 exp 形式。我们依旧可以做到 \(O(n^22^n)\)


题目

P6570 [NOI Online #3 提高组] 优秀子序列

很容易想到对于每个数出现次数 \(c_i\) 构造集合幂级数 \(F(x) = \sum\limits_{i = 0} c_ix^i\),那么优秀子序列就是不交集合,划分不包含顺序,则就是 \(e^{F(x)}\)。做一个 exp 即可。要注意对于 \(0\) 的特判,这是因为 \(\exp\) 要求空集为 \(0\)\(\ln\) 要求空集为 \(1\)(否则根据组合意义那么空集可以无限加,方案数就无限了)

P11734 [集训队互测 2015] 胡策的统计

夸赞自己,终于想到了组合意义,感动。

联通块数量的阶乘太难了,因为求综合,自然想到转化为对 \(S\) 进行子集划分,划分包含顺序的方案数。子集划分的每一部分是一个连通块计数,这一部分显然可以看作是对于任意图先计数,然后取 \(\ln\)。具体而言,设 \(E(S)\)\(S\) 点集内边数,那么 \(F(x) = \sum\limits_{S = 0}2^{E(S)}x^S, G(x) = \ln F(x), H(x) = \dfrac{1}{1 - G(x)}\)。实现上直接套板子会被卡常,求解 \(\ln F(x)\) 最后剩下一个 FWT 的形式,不用逆变换回去,直接做即可。

QOJ5411. 杏仁

如果没有 \(s\to u\) 的约束怎么做?设 \(g(S)\) 为经过 \(S\) 内点的路径数量,构成形式幂级数 \(G\),那么答案为 \(H = \exp G\)。加上这条限制,设 \(f[u, S]\) 为从 \(s\) 第一个经过 \(u\),最终经过路径为 \(S\) 的方案数,那么剩余部分就最多只能经过 \(U - S\),对 \(H\) 做一个高维前缀和即可。

CF1411G No Game No Life

对于每个点求出 SG 函数,SG 函数是 \(O(\sqrt{m})\) 级别的,可以直接求解。如果选择出来的子集异或和为 \(0\),那么先手必败,否则必胜。于是设 \(F(x)\) 中每一项的系数 \([x^S]F(x)\) 代表选出来的 sg 函数为 \(S\) 的概率,最终得到和的概率为 \(G(x)\),最后一次是 \(\dfrac{1}{n+1}\) 的概率,之前的概率则是 \(F^k(x)\),那么 \(G = \dfrac{1}{(n+1)(1 - F)}\)

特别注意这里面求逆是对异或卷积求逆。为什么子集卷积求逆那么麻烦,因为它实际上是 \(x^Sy^t\) 这个多项式。fwt 具有线性性,位运算卷积直接做就好了。

CF1119H Triple

本题弱化版本为 UOJ310。

根本不会,严肃学习,肃然起敬,该加训了。

首先这里面有 \(3\) 种,做起来比较麻烦,于是选择将 \(a, b, c\gets 0, a\operatorname{xor} b, a\operatorname{xor} c\),最后统计答案的时候全体异或 \(a\) 即可,下文默认 \(a = 0\)

我们不难构造出答案的 gf 应该是 \(F = \prod\limits_{i = 1}^n(c+dx^{b_i}+ex^{c_i})\)。为了加速运算容易想到根据 FWT 的线性性,对于每个 \((c+dx^{b_i} + ex^{c_i})\) 进行 FWT,最后按位乘起来。对于一位 \(i\) 那么 FWT 变换后 \(\operatorname{FWT}(S) = c+ (-1)^{|b_i\cap S|}d+ (-1)^{|c_i \cap S|}e\)。后面两个根据正负有 \(4\) 种情况,但是直接计算四种情况数量又很困难,接下来我就不会了。

联想到,如果只剩下一个维度,那么我们可以直接算 xor 下的 FWT,然后通过 \(a+b = n, a - b = \operatorname{FWT}(S)\) 来解决。这启发我们可以不必拘泥于直接计算。又因为 \((-1)\) 的上指标上面 \(\operatorname{xor}\) 关于 \(\operatorname{and}\) 有分配律,于是我们对 \(b_i, c_i, b_i \operatorname{xor} c_i\) 分别做一次 FWT,结合四种情况和为 \(n\) 列出方程,求解四种情况分别的数量。

推而广之,对于 \(D\) 维的情况,枚举非空的 \(2^{D} - 1\) 种子集,对于 \(2^D\) 中情况的方案数记作 \(c_D\)。当前枚举用在 FWT 里面的是子集 \(S\),即用 \(S\) 里面的异或和放到 FWT 里面去,此时若 \(T\) 中取符号的集合与 \(S\) 交集大小为奇数,那么在这个方程中 \(c_T\) 为的系数为负数,否则为正数。结合所有 \(c\) 之和为 \(n\) 可以列出 \(2^D\) 个方程,高斯消元解出来然后计算,最后 IFWT 回去。

总结:

  • 遗留的问题:两个维度可以直接数吗?比如用 sosdp 状物。
  • 可以不用直接算。
  • \((-1)^{(A\operatorname{xor} B) \operatorname{and}S} = (-1)^{A\operatorname{and}S} (-1)^{B\operatorname{and}S}\),这样就完成了两部分的独立。

QOJ5089

异曲同工之妙。

首先考虑判定,判定是一个图度数均为偶数即合法。证明可以考虑欧拉回路,也有相应的环空间理论(一定会努力补习的,呜呜呜)。

一条边的状态记作 \(b\),那么答案的 gf 为 \(G = \prod\limits_{i = 1}^m(1+x^{b})\)。因为存在一个数量限制,所以我们考虑增加一个 \(y\) 维度维护这件事情,\(G = \prod\limits_{i = 1}^m (1 + x^b y)\)

\(x\) 为主元,每一位上都是关于 \(y\) 的普通多项式。对于一个 \(i\) 的 FWT,用类似于 Tripple 那题的手法,我们很容易数出来 \(\operatorname{FWT}_S(G) = (1+y)^a(1 - y)^{m - a}\) 中的 \(a\) 的数量。接着我们要逆变换求出实际的 \(G_0\),于是:

\[G_0 = \dfrac{1}{2^n}\sum\limits_{S = 0}^{2^n - 1}(1+y)^{a_s}(1 - y)^{m - a_s}\\ = \dfrac{1}{2^n}\sum\limits_{i = 0}^m y^i \sum\limits_{S = 0}\sum\limits_{j = 0}^i \binom{a_S}{j}\binom{m - a_S}{i - j}(-1)^{i - j} \]

枚举 \(S\) 之和 \(a_S\) 有关,记录 \(w_x\)\(a_S = x\)\(S\) 数量,然后就可以做到最后 \(O(m^3)\) 统计了。前面处理 \(a_S\) 因为每一条边的状态中集合数量为 \(2\),所以可以 \(O(2^n)\) 求出来,那么最终就是 \(O(2^n + m^3)\) 的。

我严肃怀疑题解中 \(O(2^n + m^2)\) 是笔误,翻了几个最优解似乎都是 \(O(m^3)\) 的。不过也有可能是我二项式恒等式水平太拉垮了 QAQ 如果有朋友能够确定的话能不能教教我 QAQ

LOJ6673. EntropyIncreaser 与山林

本题最大难点在于读懂题目以及很神秘的一手操作。

题意是对于图 \(G = (V, E)\) 求有多少子图 \(G' = (V, E'), E' \subseteq E\) 满足 \(G'\) 有欧拉回路。注意是 \(V\)

欧拉回路要满足度数为 \(2\) 和联通,联通可以用 ln 直接求出,那么只要考虑有多少边集合 \(E\) 是的点集合 \(S\) 中每个点度数为偶数即可。我根本不会,学习到了很牛的一个做法。对于一棵树,通过删边任意改变一个点的度数奇偶性,其中这个方案是唯一的,可以从子树归纳。于是可以想到对于一个连通块方案数应当为 \(2^{m - n + 1}\),最终对于 \(S\) 的方案数就是 \(2^{E(S) - |S| + T(S)}\)\(T(S)\)\(S\) 内的连通块数。然后对于它构造成 gf 后取 ln 即可。

P13275 [NOI2025] 集合

感谢 Mirasycle 老师教我的容斥做法!不过作为子集反演真爱粉这里利用子集反演而不是容斥来做。

不是这不是集合幂级数博客吗歪!

\(g(x,y) = \sum\limits_{S, T}[v(S) = x, v(T) = y]\)\(f(x, y) = \sum\limits_{S, T}[v(S)\subseteq x, v(T) \subseteq y] = \prod\limits_{i = 0}^n(1+ [i \subseteq x]a_i + [i\subseteq y]a_i)\)。根据子集反演,有 \(g(x, y) = \sum\limits_{x\subseteq a, y \subseteq b}(-1)^{|a|+|b|-|x|-|y|}f(a, b)\)

最终要求的就是

\[\sum\limits_{S}g(S, S) = \sum\limits_{S}\sum\limits_{S\subseteq T_1, S\subseteq T_2}(-1)^{|T_1|+|T_2|}\prod_{i = 0}^{n}(1+[T_1\subseteq i]a_i + [T_2\subseteq i]a_i)\\ \sum\limits_{T_1, T_2}2^{|T_1 \cap T_2|}(-1)^{|T_1|+ |T_2|}\prod\limits_{i = 0}^n (1 + [T_1 \subseteq i]a_i + [T_2 \subseteq i]a_i) \]

后面一部分像一个高维后缀和,但是又有 \(T_1, T_2\) 两个限制,如何快速计算?考虑一个简单的单步容斥,高位后缀和计算 \(f_S = \prod\limits_{S\subseteq T}(1+ a_T), g_S = \prod\limits_{S\subseteq T}\dfrac{(1+ 2a_t)}{(1+a_T)^2}\),那么就有:

\[\sum\limits_{T_1, T_2}2^{|T_1\cap T_2|}(-1)^{|T_1|+ |T_2|}f_{T_1}f_{T_2}g_{T_1\cup T_2}\\ T_1\cap T_2 = T_1 + T_2 - T_1\cup T_2\\ \sum\limits_{T_1, T_2}\dfrac{f_{T_1}f_{T_2}(-2)^{|T_1|}(-2)^{|T_2|}}{2^{|T_1\cup T_2|}}g_{T_1\cup T_2} \]

记录 \((-2)^{|S|}f_S\) 和自己的或卷积结果为 \(h_S\),那么就是 \(\sum\limits_{S}\dfrac{g_S h_S}{2^{S}}\)

因为可能涉及除以 \(0\),所以扩域,一个数 \((a, b)\) 代表 \(a\times 0^b\)

图计数

一些碎碎念:

图计数的一个准则是:

  • 不保证联通图考虑能不能用连通图拼合计算,连通图考虑能不能用不连通图正难则反。
  • 或者用 exp-ln 的变换。
  • 即便不是连通性问题,有的时候从联通图着手也会更加有帮助。
  • 在利用逐点牛顿迭代的时候,优先考虑对联通块增量加点,因为你可以把最大的点删掉后做子集划分。

本部分严肃 copy 自 chenxinyang2006 的课件。融合了部分我本人的理解。

加训!!!

联通图计数

给定一张图 \(G\),第 \(i\) 条边边权为 \(p_i\),一张图的权定义为所有边边权乘积,对于每个点集 \(S\) 求解点集 \(S\) 的导出子图中连通图的权和。

正难则反,考虑没有约束,那么一条边选或者不选,有 \(g_S = \prod\limits_{(u, v), u,v \in S}(1 + p_{(u, v)})\),设 \(f_S\)\(S\) 是连通图,那么同样的对于 \(S\) 的一个子集划分 \(T_1, T_2, \dots, T_k\),则有 \(g_S = \sum\limits_{T_1, T_2, \dots, T_k} \dfrac{\prod_{i = 1}^k f_{T_i}}{k!}\)。为什么后面要除以 \(k!\)?因为 \(T_1, T_2, \dots, T_k\) 的枚举是规定好顺序的,这个东西会被枚举 \(k!\) 次。所以要除以 \(k!\)。我们注意到这正是集合幂级数 exp-ln 的集合意义,所以 \(F = \ln G\)

为了构造好这个东西,先不考虑连通约束,然后考虑每个连通块对总的贡献是不是满足 exp 的形式,如果不满足要修修改改。

ARC105F Lights Out on Connected Graph

询问有多少可黑白染色联通子图,也就是数二分图子图。

仍然先考虑没有联通约束。也就是说对于 \(S\) 内的点划分成 \(A, (S - A)\),只允许 \(A\)\((S - A)\) 内存在连边。如果只计数子图形态的话非常困难,于是选择对于不同染色划分,那么这个时候这个方案数就是 \(2^{\operatorname{cross}(A, S - A)}\),其中 \(\operatorname{cross}(A, B)\)\(A, B\) 内的边数量。对于每一个连通块均有 \(2\) 种这样的染色划分,刚好能够满足,于是构造 \(g_S = \sum\limits_{T\subseteq S} 2^{\operatorname{cross}(S, T - S)}\),然后 \(F = \ln G\)。最终答案为 \(\dfrac{\sum_{S}f_S}{2}\)

特别的注意到 \(g_S\) 可以写成一个子集卷积的形式,然后就可以 \(O(n^22^n)\) 求解了。

强连通与 DAG

强连通或者 DAG 子图计数,关键在于对强连通或者 DAG 的刻画。关键在于:

  • 正难则反。
  • 用缩点与拓扑排序刻画。强连通就是缩点后只有一个点的 DAG,所以问题重心放到 DAG 上面。
  • DAG 就是可拓扑排序,每次剥一层入度为 \(0\) 的点,这里钦定-恰好进行一次子集反演。

P6846 [CEOI 2019] Amusement Park

给定一张有向图,要求修改定向后形成 DAG 的改变方向边数量之和。

容易观察到对于一张 DAG 其反图也是 DAG,所以这两个改变的边是相反的,和为 \(m\)。所以我们并不在乎有向图原来的边方向,看作无向边,直接考虑将它定向为 DAG 的定向方案数 \(S\),答案就是 \(\dfrac{mS}{2}\)

这题我会!要求这个无向图是一个边双联通图!

用拓扑排序的方式刻画,考虑枚举入度为 \(0\) 的点。设 \(f_S\)\(S\) 内构成 DAG 的方案数,钦定 \(T\) 内的为入度为 \(0\) 的点。如果 \(T\) 恰好就是入度为 \(0\) 的点全集,那么要求 \(T\) 内部是一个独立集,这部分是 \(h_Tf_{S - T}\),其中 \(h_T\) 是艾弗森括号 \(T\) 是独立集。考虑容斥系数,由于这是对非空全集求和,所以系数是 \((-1)^{|T|+1}\)。于是

\[f_S = \sum\limits_{\empty \subset T\subseteq S} (-1)^{|T| + 1}h_Tf_{S- T} \]

对于这个东西可以 \(O(3^n)\) 直接递推。也可以发现它实际上是一个子集卷积的形式,然后就可以 \(O(n^22^n)\) 做掉了。

Bonus:如何对这个容斥系数进行推导。
实际上也是蛮经典的问题啦。我们给它一般化一下

给定一个集合 \(S\),对于其中所有非空子集权求和,我们只能钦定其中一部分 \(S\),求容斥系数。空子集权为 \(0\)

\[\sum\limits_{\empty \subset T \subseteq S}\sum\limits_{T \subseteq W \subseteq S}(-1)^{|W| - |T|}f(W)\\ = \sum\limits_{\empty \subset W \subseteq S}(-1)^{|W|}f(W)\sum\limits_{\empty \subset T\subseteq W}(-1)^{|T|}\\ = \sum\limits_{\empty \subset W \subseteq S}(-1)^{|W| + 1}f(W) \]

Mirasycle 老师是子集反演小黑子,我已动手看他有何话可说

算是经典结论了。

ABC306Ex Balance Scale

考虑如何处理等号?

仍然是考虑钦定 \(T\) 内的点均入度为 \(0\),不同的是这里面的点可以缩点形成一个单点,内部都是等号,缩点后相互之间没有边相连即可。

因为每条边都需要定向,所以相当于这里面有且仅有一种定向方法,那么对于 \(T\) 那么这个时候的容斥系数就是 \((-1)^{c + 1}\)\(c\)\(T\) 点集内有多少连通块。

P11714 [清华集训 2014] 主旋律

询问一张强连通图有多少导出子图仍然强连通。

我写过一次题解,这一次我重新推一下。

强连通图等价于缩点后有且仅有一个点的图。显然这很困难,考虑正难则反,我们用所有图数量减去非强连通图数量。对于点集 \(S\),设 \(E(S)\)\(S\) 内的边数量,那么总共就有 \(2^{E(S)}\) 个。

然后我们减去缩点后不止一个点的情况。入度为 \(0\) 的点如果只有一个,那么它不能是全集,入度为 \(0\) 的点有多个那么肯定不是强连通图。基于此我们枚举缩点后入度为 \(0\) 的点集合 \(T\subseteq S\),当然是钦定 \(T\)。如果 \(T\subset S\) 那么我们并不在乎 \(T\) 内部划分成一个还是多个缩的点,如果 \(T = S\) 那么 \(T\) 必须要划分成多个缩的点。最后系数是划分出的缩点数量 \(c\)\((-1)^{c+1}\)

分别讨论:

对于 \(T \subset S\)。我们发现这个本质上是一个子集划分,设 \(f_S\)\(S\) 内的点为强连通图,那么令 \(g_S\)\(S\) 内的点钦定(不是恰好!所以要带 \((-1)^{c+1}\) 的容斥系数)为入度为 \(0\) 的缩点的点的方案数。于是此时 \(g_S \to g_S + \prod\limits_{S_1, S_2, \dots, S_k}(-1)^{k+1}f_{S_i}\),这一部分可以直接枚举 lowbit 所在子集决定。

对于 \(T = S\),则要求上面的划分中 \(k \ge 2\)。但是你发现如果 \(k = 1, S_k = T\),我是要用 \(g_S\) 来求 \(f_S\),所以这个时候可以认为 \(f_S = 0\),没啥关系。所以求解 \(g_S\) 就是一个比较容易的简单子集划分。可以直接对着 lowbit 所在强连通分量 dp。

最后有 \(f_S = 2^{E(S)} - \sum\limits_{\empty \subset T\subseteq S} g_T 2^{E(T, S - T) + E(S - T)}\)\(E(A, B)\)\(A\)\(B\) 边数,这个可以一边写一边求。最后时间复杂度可以做到 \(O(n3^n)\)

其实你也可以开一个 umap 做到 \(O(3^n)\),但是我试着写了一下常数巨大无比

我不理解我之前写的题解意味在哪里。无语了。

#include <bits/stdc++.h>
#define il inline
using namespace std;
const int N = 15, Mod = 1e9 + 7;
il int lowbit(int x) {
	return x & (-x);
}
il void upd(int &x, int y) {
	x = (((x + y) >= Mod) ? (x + y - Mod) : (x + y));
}

int n, m, bas2[N * N + 10];
bool gra[N + 3][N + 3];
int H[(1 << N) + 10], out[(1 << N) + 10];
int f[(1 << N) + 10], g[(1 << N) + 10];
int E(int S, int T) {
	int rest = 0;
	while(S) {
		int R = lowbit(S);
		rest += __builtin_popcount(out[R] & T);
		S -= R;
	}
	return rest;
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	bas2[0] = 1;
	for(int i = 1, x, y; i <= m; i++) {
		cin >> x >> y;
		x--, y--;
		bas2[i] = bas2[i - 1] * 2 % Mod;
		gra[x][y] = 1;
		for(int S = 0; S < (1 << n); S++)
			if(((S >> x) & 1) && ((S >> y) & 1))
				H[S]++;
		out[(1 << x)] |= (1 << y);
	}

	for(int S = 1; S < (1 << n); S++) {
		if(__builtin_popcount(S) == 1) {
			f[S] = g[S] = 1;
			continue;
		}
		for(int T = S - lowbit(S); ; T = (T - 1) & (S - lowbit(S))) {
			int R = T + lowbit(S);
			upd(g[S], (Mod - 1ll * f[R] * g[S - R] % Mod) % Mod);
			if(!T) break;
		}
		f[S] = bas2[H[S]];
		for(int T = S; T; T = (T - 1) & S)
			upd(f[S], (Mod - 1ll * g[T] * bas2[E(T, S - T) + H[S - T]] % Mod) % Mod);
		upd(g[S], f[S]);
	}
	cout << f[(1 << n) - 1] << endl;
}

点双联通-联通

loj6729. 点双连通生成子图计数

删除点双后,联通子图会变成若干不连通子图的并,这实际上为逐点牛迭提供了天然的划分方式。

考虑逐点牛迭,进行 \(n\) 轮每一轮要求当前图中割点编号大于 \(n\)。在第 \(n\) 轮,令上一轮的答案为 \(g_{n - 1, S}\)。若 \(n\in S\),然后删除 \(n\) 对于 \(S\) 进行子集划分 \(\{n\}, T_1, T_2, \dots, T_k\) 得到每个连通块结果,联通块内部都是合法的,加上外部连边,这部分是 \(g_{n, T_i + \{n\}}\)。以 exp 来合并,最后得到了 \(g_{n - 1, S}\)

列一下式子即:

\[g_{n - 1, S} = \sum\limits_{\{n\}, T_1, T_2, \dots, T_k}\dfrac{\prod_{i = 1}^k g_{n, T_i + \{n\}}}{k!} \]

于是可以发现我们构造出 \(q_{n, S} = \begin{cases} 0 & n\in S \\ g_{n, S - \{n\}} & n \not\in S\end{cases}\)。进行 \(\ln q_u\) 后结果那么 \(g_{n, S} = \begin{cases}g_{n - 1, S} & n\not\in S \\ \ln q_{u, S - \{n\}} & n \in S\end{cases}\)

带权发现本质一致。时间复杂度因为并不是“加入最高位”(考虑割边最高位),所以不能优化掉。是 \(O(n^32^n)\) 的。

loj6719. 「300iq Contest 2」数仙人掌 加强版

仙人掌允许点相邻,所以我们仍然考虑类似的状物。

考虑逐点牛顿迭代,加入 \(n\),仍然考虑删除 \(n\) 后形成的各个自子仙人掌。设 \(g_{n, S}\) 为构造出来度数超过 2 的圆点最大点为 \(n\) 的方案数。开始 \(g_{0, S}\) 就是 \(S\) 内部构成环方案数。转移考虑 \(S\) 的子集划分 \(T_1, T_2, \dots, T_k\),显然是一个 exp 的贡献形式,即 \(g_{n} = \exp g'_{n - 1}\),只考虑更新包含 \(n\) 的位,就做完了。

边双联通-联通

loj6730. 边双连通生成子图计数

边双联通生成子图计数。

依旧牛迭,对于第 \(n\) 轮考虑割边两端点均超过 \(n\) 的方案。

依旧考虑一个端点为 \(n\) 的方案。对于删除这个点后,考虑这些割边情况。令最终答案为 \(g_S\)。这一轮答案是 \(g_{n, S}\)

考察上一轮中的 \(g_{n + 1}\) 和这一轮 \(g_n\) 的关系,发现是 \(n\) 提供一部分一个点双内的点 \(W\),然后剩下的部分做子集划分 \(T_1, T_2, \dots, T_k\),每一个子集代表一个联通块,和 \(n\) 之间只有一条割边相连。

写成式子发现是:

\[g_{n, S} = \sum\limits_{\{n\} \in W \subseteq S}g_{n + 1, W}\sum\limits_{T_1, T_2, \dots, T_k}\dfrac{\prod_{i = 1}^kg_{n, T_i}c(n, T_i)}{k!} \]

把后面写成 \(h_{T_i}\),然后改写成集合幂级数形式就是 \(g_{n} = g_{n+1}\times \exp(h_{n})\),即 \(g_{n+1} = g_{n}\times \exp^{-1}(h_{n})\)

类似的,时间复杂度 \(O(n^32^n)\)

upd on 20260227

在我今天来复习这个地方的时候发现之前写的不够清晰,重新解释一下。

要求当前割边最大编号比 \(n\) 大,则 \(g_n\) 即所求,\(g_1\) 为初始,即连通图计数。考虑如何从 \(g_n\) 推到 \(g_{n + 1}\)。考虑 \((n + 1)\) 这个点与之相连的割边。首先是 \(n\) 所在的这个“边双”,注意这个边双是允许割边编号比 \(n\)\((n + 1)\) 更大的边双,而不是一般意义下的边双。

\(g_n\) 看作是若干个 \(g_{n+1}\) 的拼合,可以得到上面的式子。注意一下其实后面本来应该是 \(g_{n + 1, T_i}\),但是由于 \(T_i\) 不包含 \(n\) 所以并不会产生最大割边上的点为 \(n\) 的边双。

uoj962. 【UR #30】交通管制

考虑判定,对于一张图边双缩点后,\(c_i = 1\) 为两点联通,\(c_i = 2\) 为两点之间经过一个大小 \(>1\) 的边双。这个“存在”非常难做,于是我们考虑“不存在”即两点之间是一个极大的大小为 \(1\) 的边双。记大小为 \(1\) 的边双为白的,大于 \(1\) 的为黑的。记 \(P\) 为一个极大的白色连通块\(Q\) 为黑点,那么 \(U = P_1\cup P_2 \cup \dots \cup P_{m_1} \cup Q_1\cup Q_2\cup\dots \cup Q_{m_2}\)。考虑对这个划分求解方案数。

对于 \(Q\) 内部的计数就是经典的边双子图计数,对于 \(P\) 内部的计数直接强上矩阵数定理暴力。拼起来怎么数?拼起来构造成一颗树,要求 \(P\)\(Q\) 不相邻。考虑容斥。首先对于划分出来的 \(P_1, P_2, \dots, P_m\) 相连,边权为 \((-1)\),接下来对于 \(P_1, P_2, \dots, P_{m_{1}}, Q_1, Q_2, \dots, Q_{m_{2}}\) 任意相连,边权为 \(1\),一棵树的权为其中所有边权乘积,最后总和就是合法树数量。到这一步容斥的动机明确而手法巧妙,真是太牛了!

计算生成树的方案数如何计算?这个问题和求解边双联通分量其实十分相似!仍然考虑最后删除的边中最大的编号为 \(u\),删掉之后划分出若干个集合,每个集合都要向 \(u\) 连一条边,边权有 \(-1\)\(+1\)。考虑正向的边双连通-联通过程,每次考虑以 \(u\) 为编号最大的割边会产生的情况。是类似的不再赘述。

自豪的给出我的代码(叉腰小章鱼:

#include <bits/stdc++.h>
using namespace std;
const int N = 16, Mod = 998244353;
int qpow(int n, int m) {
	int res = 1;
	while(m) {
		if(m & 1) res = 1ll * res * n % Mod;
		n = 1ll * n * n % Mod;
		m >>= 1;
	}
	return res;
}
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int modint(int x, int y) {
	return ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int lowbit(int x) {
	return x & (-x);
}

int n, m, qk;
int opt[N * N + 10], s[N * N + 10], t[N * N + 10];
int valid[(1 << N) + 10], E[(1 << N) + 3], able[N + 3];
int gra[N + 3][N + 3], wgra[N + 3][N + 3], invnn[N + 3];
int es[N + 10], f[(1 << N) + 10], g[(1 << N) + 10];

int mat[N + 3][N + 3];

namespace poly {
	void fwt(int *arr, bool tp) {
		for(int k = 1; k < (1 << n); k <<= 1)
			for(int i = 0; i < (1 << n); i += (k << 1)) {
				for(int j = 0; j < k; j++) {
					int rest = arr[i + j];
					if(!tp) 
						arr[i + j + k] = modint(rest, arr[i + j + k]);
					else arr[i + j + k] = modint(arr[i + j + k], - rest + Mod);
				}
			}
	}
	
	int f[N + 3][(1 << N) + 3], g[N + 3][(1 << N) + 3], h[N + 3][(1 << N) + 3];
	void submul(int *a, int *b, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = h[i][S] = 0;
			
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i],
			g[__builtin_popcount(i)][i] = b[i];
		for(int i = 0; i <= n; i++)
			fwt(f[i], 0), fwt(g[i], 0);
		
		for(int S = 0; S < (1 << n); S++)
			for(int i = 0; i <= n; i++)
				for(int j = 0; j <= i; j++)
					upd(h[i][S], 1ll * f[j][S] * g[i - j][S] % Mod);
		for(int i = 0; i <= n; i++) fwt(h[i], 1);
		for(int i = 0; i < (1 << n); i++) c[i] = h[__builtin_popcount(i)][i];
	}
	void exp(int *a, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = 0;
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i];
		
		for(int i = 0; i <= n; i++) fwt(f[i], 0);
		g[0][0] = 1;
		fwt(g[0], 0);
		for(int i = 1; i <= n; i++) {
			int invn = invnn[i];
			for(int j = 1; j <= i; j++)
				for(int S = 0; S < (1 << n); S++)
					upd(g[i][S], 1ll * j * f[j][S] % Mod * g[i - j][S] % Mod);
			for(int S = 0; S < (1 << n); S++) g[i][S] = 1ll * g[i][S] * invn % Mod;
		}
		for(int i = 0; i <= n; i++) fwt(g[i], 1);
		
		for(int i = 0; i < (1 << n); i++)
			c[i] = g[__builtin_popcount(i)][i];
	}
	void trans(int *a, int *b, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = h[i][S] = 0;
		for(int i = 0; i < (1 << n); i++) {
			f[__builtin_popcount(i)][i] = a[i];
			h[__builtin_popcount(i)][i] = b[i];
		}
		
		for(int i = 0; i <= n; i++) fwt(f[i], 0), fwt(h[i], 0);
		for(int i = 0; i < (1 << n); i++) g[0][i] = 1;
		for(int i = 1; i <= n; i++) {
			int invn = invnn[i];
			for(int j = 1; j <= i; j++)
				for(int S = 0; S < (1 << n); S++)
					upd(g[i][S], 1ll * j * f[j][S] % Mod * g[i - j][S] % Mod);
			for(int S = 0; S < (1 << n); S++) g[i][S] = 1ll * g[i][S] * invn % Mod;
		}
		
		for(int i = 0; i <= n; i++) {
			for(int S = 0; S < (1 << n); S++) {
				f[i][S] = 0;
				for(int j = 0; j <= i; j++)
					upd(f[i][S], 1ll * g[j][S] * h[i - j][S] % Mod);
			}
		}
		for(int i = 0; i <= n; i++) fwt(f[i], 1);
		for(int i = 0; i < (1 << n); i++) c[i] = f[__builtin_popcount(i)][i];
	}
	void ln(int *a, int *c) {
		for(int i = 0; i <= n; i++)
			for(int S = 0; S < (1 << n); S++)
				f[i][S] = g[i][S] = 0;
		for(int i = 0; i < (1 << n); i++)
			f[__builtin_popcount(i)][i] = a[i];
		
		for(int i = 0; i <= n; i++) fwt(f[i], 0);
		g[0][0] = 0;
		for(int i = 1; i <= n; i++) {
			int invn = qpow(i, Mod - 2);
			for(int S = 0; S < (1 << n); S++) {
				g[i][S] = 1ll * i * f[i][S] % Mod;
				for(int j = 1; j < i; j++)
					upd(g[i][S], Mod - 1ll * j * g[j][S] % Mod * f[i - j][S] % Mod);
				g[i][S] = 1ll * g[i][S] * invn % Mod;
			}
		}
		for(int i = 0; i <= n; i++) fwt(g[i], 1);
		
		for(int i = 0; i < (1 << n); i++)
			c[i] = g[__builtin_popcount(i)][i];
	} 
}

int hsh[N + 10];
void calcf(int S) {
	if(valid[S]) {
		f[S] = 0;
		return ;
	}
	int tot = 0;
	for(int i = 0; i < n; i++)
		if((S >> i) & 1) hsh[i] = tot++;
	for(int i = 0; i < tot; i++)
		for(int j = 0; j < tot; j++) 
			mat[i][j] = 0;
	for(int i = 0; i < n; i++)
		for(int j = i + 1; j < n; j++)
			if(gra[i][j] && ((S >> i) & 1) && ((S >> j) & 1))
				upd(mat[hsh[i]][hsh[j]], Mod - 1), upd(mat[hsh[j]][hsh[i]], Mod - 1),
				upd(mat[hsh[i]][hsh[i]], 1), upd(mat[hsh[j]][hsh[j]], 1);
				
	int res = 1;
	for(int i = 1; i < tot; i++) {
		for(int j = i + 1; j < tot; j++) {
			while(mat[i][i]) {
				int div = mat[j][i] / mat[i][i];
				for(int k = i; k < n; k++)
					mat[j][k] = ((mat[j][k] - 1ll * div * mat[i][k] % Mod) % Mod + Mod) % Mod;
				swap(mat[i], mat[j]), res = Mod - res;
			}
			swap(mat[i], mat[j]), res = Mod - res;
		}
	}
	for(int i = 1; i < tot; i++)
		res = 1ll * res * mat[i][i] % Mod;
	f[S] = res;
}

bool stp[N + 3];
int p[N + 3][(1 << N) + 3], q[N + 3][(1 << N) + 3], cof[N + 3][(1 << N) + 3], h[(1 << N) + 3];
void calcg() {	
	for(int S = 0; S < (1 << n); S++)
		p[n - 1][S] = qpow(2, E[S]);
	poly::ln(p[n - 1], p[n - 1]);
	
	for(int u = n - 1; u >= 1; u--) {
		for(int S = 1; S < (1 << n); S++)	
			if(!((S >> u) & 1))
				q[u - 1][S] = (Mod - 1ll * p[u][S] * cof[u][S] % Mod) % Mod;
		poly::trans(q[u - 1], p[u], p[u - 1]);
		for(int S = 0; S < (1 << n); S++)
			if(!((S >> u) & 1))
				p[u - 1][S] = p[u][S];
	}
	
	for(int S = 0; S < (1 << n); S++) 
		if(S != lowbit(S)) g[S] = p[0][S];
}
void transf(int *arr, int type) {
	for(int i = 0; i < n; i++)
		for(int S = 0; S < (1 << n); S++)
			p[i][S] = q[i][S] = 0;
	
	for(int S = 0; S < (1 << n); S++)
		p[0][S] = arr[S];
	for(int u = 1; u < n; u++) {
		for(int S = 0; S < (1 << n); S++)
			if(!((S >> u) & 1))
				q[u - 1][S] = 1ll * type * p[u - 1][S] % Mod * cof[u][S] % Mod;
		poly::trans(q[u - 1], p[u - 1], p[u]);
		for(int S = 0; S < (1 << n); S++)
			if(!((S >> u) & 1))
				p[u][S] = p[u - 1][S];		 
	}
	for(int S = 0; S < (1 << n); S++) arr[S] = p[n - 1][S];
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> qk;
	for(int i = 1; i <= n; i++) invnn[i] = qpow(i, Mod - 2);
	for(int i = 1, x, y; i <= m; i++)
		cin >> x >> y, x--, y--,
		gra[x][y]++, gra[y][x]++,
		es[x] |= (1 << y),
		es[y] |= (1 << x);
	for(int i = 1; i <= qk; i++) {
		cin >> s[i] >> t[i] >> opt[i],
		s[i]--, t[i]--;
		
		if(opt[i] == 2) {
			wgra[s[i]][t[i]]++, wgra[t[i]][s[i]]++;
			if(s[i] == t[i]) stp[s[i]] = 1;
		}
		able[s[i]] |= (1 << t[i]), 
		able[t[i]] |= (1 << s[i]);
	}
	for(int i = 0; i < n; i++) {
		if(!stp[i]) continue;
		for(int S = 0; S < (1 << n); S++)
			if((S >> i) & 1)
				valid[S] = 1;
	}
	for(int S = 0; S < (1 << n); S++) {
		if(lowbit(S) == S) continue;
		int i1 = __lg(lowbit(S));
		int i2 = __lg(lowbit(S - lowbit(S)));
		valid[S] = valid[S - (1 << i1)] + valid[S - (1 << i2)] + wgra[i1][i2] - valid[S - (1 << i1) - (1 << i2)];
		E[S] = E[S - (1 << i1)] + E[S - (1 << i2)] + gra[i1][i2] - E[S - (1 << i1) - (1 << i2)];
	}
	for(int u = 0; u < n; u++)
		for(int S = 1; S < (1 << n); S++) {
			int ind = __lg(lowbit(S));
			cof[u][S] = cof[u][S - lowbit(S)] + (u > ind && gra[u][ind]);
		}
	
	for(int S = 0; S < (1 << n); S++) calcf(S);
	calcg();
	transf(f, Mod - 1);
	for(int S = 0; S < (1 << n); S++)
		upd(f[S], g[S]);
	transf(f, 1);
	for(int S = 0; S < (1 << n); S++) {
		bool flag = 1;
		for(int i = 0; i < n; i++)
			if((S >> i) & 1) {
				if((able[i] & S) != able[i]) {
					flag = 0;
					break;
				}
			}
		if(!flag) f[S] = 0;
	}
	poly::exp(f, f);
	cout << f[(1 << n) - 1] << '\n';
}
posted @ 2025-12-21 21:40  CatFromMars  阅读(191)  评论(1)    收藏  举报