参考资料:论文集2015《集合幂级数的性质及其快速算法》吕凯风
两个序列 (当然,以集合为下标)的子集卷积为
它是非常重要的集合卷积;事实上它可能是最重要的集合卷积。
我们都知道它的经典 算法:考虑把 和 卷积到 上,这样 显然一定是两个不相交的 卷出来的,否则第一维一定会比 大。
显然就是普通的序列卷积,这提示我们定义如下的
定义 是 的集合占位幂级数,如果任意 是一个形式幂级数,且对于 有 ,同时有 。
我也知道乱改论文里的符号不太好,但是在右下角标里写太多东西就是让我很难受……
显然对于 的 我们完全不关心。大概这就是占位罢(。显然集合占位幂级数的如下定义的卷积
就是子集卷积。
实现方法就是之前提到的经典算法,不再赘述。
接下来我们简称集合占位幂级数为集合幂级数。
此处指的是子集卷积意义下的逆。显然子集卷积意义下的乘法单位元就是 。
回想子集卷积,我们只是单纯地 or_FWT 后把对应位置的集合幂级数相乘。从而有
于是我们把 (是一个形式幂级数)挨个求逆然后 回去即可。从而也可以看出集合幂级数 有逆当且仅当
由于 一个集合幂级数是 的,所以时间复杂度瓶颈并不在求逆上,可以使用简单常数小的暴力 求逆。
考虑如下定义的 Exp:
从而,由于 有线性和积性, 有线性,我们可以得到
某种意义上说:。
从而只需要 - 对每个形式幂级数求 - 即可。
完全类似。
同样,由于对形式幂级数的操作并非时间复杂度瓶颈,从而可以使用暴力 算法。
虽然集合占位幂级数一看就让人很有封装的欲望,但是不仅没有封装的必要而且一旦封装还会让你的常数起飞……
xusing namespace std;const int p = 998244353, maxN = 18, mL = 1 << maxN, mN = maxN + 1, inv2 = (p + 1) >> 1;struct Z {int x; Z(int x0 = 0) : x(x0) {}};int inline check(int x) { return x >= p ? x - p : x; }Z operator +(const Z a, const Z b) { return check(a.x + b.x); }Z& operator +=(Z &a, const Z b) { return a = a + b; }Z operator -(const Z a, const Z b) { return check(a.x - b.x + p); }Z operator -(const Z a) {return check(p - a.x);}Z& operator -=(Z &a, const Z b) { return a = a - b; }Z operator *(const Z a, const Z b) { return 1LL * a.x * b.x % p; }Z& operator *=(Z &a, const Z b) { return a = a * b; }Z qpow(Z a, int k) { Z ans = 1; for (; k; a *= a, k >>= 1) if(k & 1) ans *= a; return ans;}Z fac[mL], ifac[mL], inv[mL];int ppc[mL], ctz[mL];void init() { inv[1] = fac[0] = fac[1] = ifac[0] = ifac[1] = 1; for (int i = 2; i < mL; i++) fac[i] = fac[i - 1] * i, inv[i] = -inv[p % i] * (p / i), ifac[i] = ifac[i - 1] * inv[i]; for (int i = 1; i < mL; i++) { ppc[i] = ppc[i >> 1] + (i & 1); ctz[i] = (i & 1) ? 0 : (ctz[i >> 1] + 1); }}namespace Poly {void Mul(Z f[], Z g[], int n) { for (Z *i = g + n; i >= g; i--) { *i *= f[0]; forj *i += *jf * *jg; }}void Inv(Z f[], Z g[], int n) { memset(g, 0, (n + 1) * sizeof(Z)); g[0] = qpow(f[0], p - 2); for (Z *i = g + 1; i <= g + n; i++) { forj *i -= *jf * *jg; *i *= g[0]; }}Z tg[mN]; //tg = 0.5ttgg = 0.5瑇!void copy(Z f[], Z g[], int n) { for (Z *i0 = f, *i1 = g; i0 <= f + n; i0++, i1++) *i1 = *i0; }void Inv(Z f[], int n) { copy(f, tg, n); Inv(tg, f, n); }void Ln(Z f[], Z g[], int n) { memset(g, 0, (n + 1) * sizeof(Z)); g[0] = 0; for (Z *i = g + 1; i <= g + n; i++) { *i = (i - g) * f[i - g]; forj *i -= (jg - g) * *jf * *jg; *i *= inv[i - g]; }}void Ln(Z f[], int n) { copy(f, tg, n); Ln(tg, f, n); }void Exp(Z f[], Z g[], int n) { memset(g, 0, (n + 1) * sizeof(Z)); g[0] = 1; for (Z *i = g + 1; i <= g + n; i++) { forj *i += (jf - f) * *jf * *jg; *i *= inv[i - g]; }}void Exp(Z f[], int n) { copy(f, tg, n); Exp(tg, f, n); }}struct SPS { //Set Power Series int n; Z D[mL][mN]; void FWT() { for (int i = 1; i < 1 << n; i <<= 1) for (auto j = D; j < D + (1 << n); j += i << 1) for (auto k0 = j, k1 = j + i; k0 < j + i; k0++, k1++) for (auto l0 = *k0, l1 = *k1; l0 <= *k0 + n; l0++, l1++) *l1 += *l0; } void iFWT() { for (int i = 1; i < 1 << n; i <<= 1) for (auto j = D; j < D + (1 << n); j += i << 1) for (auto k0 = j, k1 = j + i; k0 < j + i; k0++, k1++) for (auto l0 = *k0, l1 = *k1; l0 <= *k0 + n; l0++, l1++) *l1 -= *l0; } void clear() { for (auto s = D; s < D + (1 << n); s++) for (auto j = *s; j <= *s + n; j++) *j = 0; } void copy(SPS &b) { for (auto s0 = D, s1 = b.D; s0 < D + (1 << n); s0++, s1++) for (auto j0 = *s0, j1 = *s1; j0 <= *s0 + n; j0++, j1++) *j0 = *j1; }};int main() { init(); // important}题面完全没有体现出要求的是生成子图啊……
考虑计算点度均为偶数的生成子图个数。暴力枚举点集,于是得到一个异或方程组,解数即偶子图个数。抽出其中的线性基,其他边均可随意选或不选。
然后集合幂级数 即可。
复杂度 。
注意 体现的重要组合意义: 会把 拆解成不能再拆解的形式,因为它是 ,即完全任意组合的逆运算。
Exp 组合意义的名字是 x义x 乱取的,不要在意
我们都知道点/边双是和连通有某种容斥关系的。在生成子图问题中我们仍然可以类推。具体来讲:
首先我们可以算出连通生成子图的集合幂级数 。这个很显然,生成子图的集合幂级数 一下即可。
令 为所有割点编号都大于等于 的连通生成子图的集合幂级数。考虑从 推到 。也就是要去除所有 是其割点的图:删去 后变为多个 (你可能会想不应该是 吗,但是这些图本来就不含 , 不影响它们)的图。
但是这怎么列式子呢,你可以看看大概长这样的一个图

于是我们得到
其中 是强制把不包含 的位置置 0 得到的集合幂级数。
建议细品。理解这个完全基于点的容斥的处理边的方式是不太容易的。关注处理连通性的方式:这里用 把连通块“缝”了起来。上面的变换也和 Exp 有了很大差别,不如称为“连通组合”?
复杂度 。你可能会很惊讶,这玩意不是 的吗,是怎么过的呢,小编也很惊讶,但事实就是这样。

啊这,边的限制……这就很难搞了。我们必须想个办法把它变成点。
边双的每一条边都包含在至少一个环里。环意味着什么?点双连通分量。
也就是说,每一个有边的点对都包含在一个更大的点双里。边双连通图就是没有大小为 2 的点双连通分量的图。
注意大小为 2 的点双连通分量是不可(按上面的方式)拆解的。
我们考虑先求出点双的集合幂级数,然后挖掉所有的大小为 2 的位置,记为 ,再一步步把它“组装”回去。令 是允许编号小于等于 的节点成为其割点的答案。如果你真的认真考虑过了上面两个问题,你应该能轻松写出
复杂度 。

一个无向图 的 Tutte 多项式定义为
我们先考虑计算连通的 ,之后再 Exp 上去。
设 是所有使 连通的 的 之和。考虑加一个节点 。设 之间的边数为 。则如果加入 则应该乘的是
可以发现如果新的连通块是由多个 拼成,那么只需要对每个 分别乘这个东西就好了。那么暴力把 个 全都加进去就做完了。复杂度 。
注意 的情况。

还是考虑加入一个点 。观察一下 的“子节点”:

那么我们设 是点集为 的仙人掌,而且删掉 还是仙人掌的数量,从而答案就是
经验告诉我们应该分别讨论单纯的点和环两种情况。设前一种为 : 表示儿子为 ,后一种为 : 表示 在环上的“下一个点”是 ,显然 会因此算两遍。然后还要特判只有 的迫真环,从而有
非常的简单,它只要求 有边,而且 是一个仙人掌。
接下来考虑 。 是由一串仙人掌串在一个环上,不妨修改 的定义:不再要求 有边,我们只是简单地把仙人掌串到了 处。也不妨利用一下 ,它也不再要求 有边。最后统计的时候强行要求即可。

(↑ 请注意实际上 的“占地”并不包含 ,图中只是为了方便理解)
于是可以写出
初值为 。考虑
于是从较低的 推上去即可。时间复杂度为 。

注意到只需要承认一条边是一个长度为 2 的环,此时仙人掌就是环套在一起,立即得到一个好写的做法。
是不是什么图计数题都可以搬到集合幂级数上啊(
生 成 烷 烃(错乱)