集合幂级数全家桶

· · 科技·工程

集合幂级数全家桶(?)

1. 集合幂级数

对于一个全集 U,给 U 的每个子集 S 赋予一个权值 f_S。我们也可以通过类似“多项式”的方法描述:

F(x) = \sum_S f_Sx^S

由此可以进行一些运算。

2. 基本操作

  1. 子集卷积
  2. 集合幂级数求逆
  3. 集合幂级数 \ln
  4. 集合幂级数 \exp

3. FWT, FMT

3.1 基本操作

集合幂级数的基本运算是:

H(x) = F(x) \times G(x) = \sum_{S}x^S\sum_{R \odot T = S}f_Rg_T

其中 \odot 是一种常见的集合运算符,包括 \cup(并),\cap(交),\Delta(差),\sqcup(不交并)。

前面三种是基础的,最后一种基于前面的算法。

显然,我们可以 O(4^n) 来枚举 R, T 来得到 H,但这太慢了。这就需要引入算法:快速沃尔什变换(FWT)

3.2 恰当的中间值

在 FFT 中,我们先对 F, G 进行变换 \hat{F}, \hat{G},然后逐位相乘得到 \hat{H},再逆变换得到 H。在集合幂级数的运算中,我们同样有类似的中间值。此时,对于不同的运算 \cup, \cap, \Delta,我们有:

\hat{f}_S=\begin{cases} \sum_{T \sube S} f_T & \text{operator is } \cup\\ \sum_{T \supe S} f_T & \text{operator is } \cap\\ \sum_{T \circ S = 0} f_T - \sum_{T \circ S = 1} f_T & \text{operator is } \Delta\\ \end{cases}

其中 T \circ S 表示 |T \cap S| \bmod 2。我们一一验证一下它的正确性。对于 \cup

\begin{aligned} \hat{f}_S \hat{g}_S &= \sum_{T \sube S} f_T \sum_{R \sube S} g_R\\ &= \sum_{(T\cup R) \sube S} f_Tg_R\\ &= \sum_{A \sube S} \sum_{(T \cup R) = A}f_Tg_R\\ &= \sum_{A \sube S} h_A\\ &= \hat{h}_S \end{aligned}

对于 \cap,我们每一个 S 取补集就和 \cup 一样了。对于 \Delta,证明稍微复杂:

\begin{aligned} \hat{f}_S\hat{g}_S &= (\sum_{S \circ T = 0} f_T - \sum_{S \circ T = 1}f_T)(\sum_{S \circ R =0} g_R - \sum_{S \circ R = 1} g_R)\\ &= \left(\sum_{S \circ T = 0}f_T \sum_{S \circ R = 0}g_R + \sum_{S \circ T = 1}f_T \sum_{S \circ R = 1} g_R\right) - \left(\sum_{S \circ T = 1}f_T \sum_{S \circ R = 0}g_R + \sum_{S \circ T = 0}f_T \sum_{S \circ R = 1} g_R\right)\\ &= \sum_{S \circ (T \Delta R) = 0}f_Tg_R - \sum_{S \circ (T \Delta R) = 1} f_Tg_R\\ &= \hat{h}_S \end{aligned}

3.3 高维前缀和

由于在这些运算中,位之间都是相对独立的。我们可以逐位处理。

对于 \cup,我们在处理一个位 i 时,我们执行 f_S \overset{+}{\to} f_{S \cup \{i\}},这样执行完后,对于 S 的每个子集 T,其恰好有一种贡献路径达到 S。至于怎么处理,可以类似 FFT 中的分治处理:

void fwt_or(ll *f, int len) // len 是集合个数
{
    for (int h = 2; h <= len; h <<= 1) // 枚举处理的位 * 2
    {
        for (int j = 0; j < len; j += h)
        {
            for (int i = 0; i < (h >> 1); i++)
            {
                (f[j + i + (h >> 1)] += f[j + i]) %= MOD;
            }
        }
    }
}

我们也将处理 \cup 操作的 FWT 称为快速莫比乌斯变换(FMT)。

将其反过来处理(交换第 9 行),就可以得到 \cap 的 FWT 结果了。

而对于 \Delta 操作,我们只要只需进行 (f'_S, f'_{S + \{i\}}) \gets (f'_{S} + f'_{S + \{i\}}, f'_{S} - f'_{S + \{i\}}) 即可。这是因为,对没有 iS,添加一个 i 之后,两集合的交大小奇偶性不会变化,而对于有 iS 会变,所以就可以得到这个式子了。

3.4 逆变换

这其实相当的简单。例如 \cup 操作,我们在 FWT 时进行了 f_S \overset{+}{\to} f_{S \cup \{i\}},IFWT 时进行 f_{S \cup \{i\}} \gets f_{S \cup \{i\}} - f_S 即可。

其他操作也类似。在 \Delta 操作的 IFWT 中,我们还要除以 2(类似于三角函数的积化和差)。可以发现,这些过程和 FWT 过程非常类似,我们可以放在一个函数内写。最终可以得到:

void fwt_or(ll *f, int len, bool on)
{
    ll type = on ? 1 : -1;
    for (int h = 2; h <= len; h <<= 1)
    {
        for (int j = 0; j < len; j += h)
        {
            for (int i = 0; i < (h >> 1); i++)
            {
                (f[j + i + (h >> 1)] += MOD + f[j + i] * type) %= MOD;
            }
        }
    }
}

void fwt_and(ll *f, int len, bool on)
{
    ll type = on ? 1 : -1;
    for (int h = 2; h <= len; h <<= 1)
    {
        for (int j = 0; j < len; j += h)
        {
            for (int i = 0; i < (h >> 1); i++)
            {
                (f[j + i] += MOD + f[j + i + (h >> 1)] * type) %= MOD;
            }
        }
    }
}

void fwt_xor(ll *f, int len, bool on)
{
    ll type = on ? 1 : I2;
    for (int h = 2; h <= len; h <<= 1)
    {
        for (int j = 0; j < len; j += h)
        {
            for (int i = 0; i < (h >> 1); i++)
            {
                ll u = f[j + i], t = f[j + i + (h >> 1)];
                f[j + i] = (u + t) * type % MOD;
                f[j + i + (h >> 1)] = (u - t + MOD) * type % MOD;
            }
        }
    }
}

4. 子集卷积

我们还需要应对不交并 \sqcup 操作。

4.1 多项式套幂级数?

对于集合 S, T,我们观察它们的大小 |S|, |T|。当进行不交并时,若有 |S \cup T| = |S| + |T|,我们才会真正乘一起。而当我们真正进行这个过程时,相当于将大小为 |S| 的集合与大小为 |T| 的集合权值乘一起,贡献到 |S| + |T| 的集合去了。能够发现,这和多项式里的 x^{|S|} \times x^{|T|} = x^{|S| + |T|} 很像。

因此,我们可以定义一个新的(不知道用什么词)式来表达我们初始的 f_S

F(x, y) = \sum_S y^{|S|} x^S f_S

当我们给 F(x, y)G(x, y) 相乘时,所有的 f_Sg_T 会被乘进 [x^{S \cup T}y^{|S| + |T|}](F \times G)(x, y)。而我们最终只取 [x^Sy^{|S|}](F \times G)(x, y),也就保证了当 |S| + |T| = |S \cup T| 时,乘法的结果才会被我们取到。所以我们得到了正确的不交并卷积,也就是子集卷积。

4.2 多套幂的操作

我们可以将 F 存进一个二维数组 ll f[MAXL][MAXN],其中第一维存 y,第二维存 x

考虑到乘法的实质,我们要做的是先对 x 这一维进行 FWT,然后对 y 这一维进行 FFT,然后再分别位置相乘。这个可以达到理论复杂度 O(2^nn\log n)

然而在实践中,由于 y 的次数较小,我们不会对它进行 FFT 优化。也就是用 O(n^2) 的多项式乘法,达到 O(2^nn^2)

/**
 * calculate h(x) = f(x) * g(x), where x^S * x^T = { 0          if S ∩ T != ∅
 *                                                 { x^{S ∪ T}  else
 * please offer f, g, h in ll[log len][len], where [x^S]f(x) is stored in f[|S|][S].
 */
template <typename ArrayType>
void subset_conv(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen, __restrict__ ArrayType h)
{
    int len = 1 << loglen;
    for (int i = 0; i <= loglen; i++)
    {
        fwt_or(f[i], len, true);
        fwt_or(g[i], len, true);
    }
    for (int i = 0; i <= loglen; i++) // 这一段实际上就是多项式乘法
    {
        for (int j = 0; j < len; j++)
        {
            h[i][j] = 0;
            for (int k = 0; k <= i; k++)
            {
                (h[i][j] += f[k][j] * g[i - k][j] % MOD) %= MOD;
            }
        }
    }
    for (int i = 0; i <= loglen; i++)
    {
        fwt_or(f[i], len, false);
        fwt_or(g[i], len, false);
        fwt_or(h[i], len, false);
    }
}

5. 初等函数

5.1 多项式的初等函数搬进 \sqcup

在实际情况中,我们遇到的基本都是 \sqcup 操作。既然我们定义了 \sqcup 上的乘法(也就是子集卷积),我们同样可以衍生出别的函数,例如 \exp, \ln 和求逆。

在这些函数中,可以发现它们在 y 这一维度上都和多项式一样。这可能很违反直觉,也可能难以理解,这是因为它和我们常见的操作差异巨大,且比较难找到合适的组合意义。

这类操作都可以封装在一个模板中:

/**
 * calculate f(x) = oper(g(x)), where x^S * x^T = { 0          if S ∩ T != ∅
 *                                               { x^{S ∪ T}  else
 * please offer f, g in ll[log len][len], where [x^S]g(x) is stored in g[|S|][S].
 * 
 * @param oper The operation on sets.
 *             Its type should be void (*) (ll *f, ll *g, int loglen) where f is the destination
 */
template <typename ArrayType, typename Oper> // 理论上 ArrayType 应当是类似 (ll **) 或 (ll *[])
void subset_oper(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen, const Oper oper)
{
    int len = 1 << loglen;
    for (int i = 0; i <= loglen; i++)
    {
        fwt_or(g[i], len, true);
    }
    static ll F[MAXL], G[MAXL];
    for (int j = 0; j < len; j++)
    {
        for (int i = 0; i <= loglen; i++)
        {
            G[i] = g[i][j];
        }
        oper(F, G, loglen);
        for (int i = 0; i <= loglen; i++)
        {
            f[i][j] = F[i];
        }
    }
    ll inl = pow(len, MOD - 2, MOD);
    for (int i = 0; i <= loglen; i++)
    {
        fwt_or(g[i], len, false);
        fwt_or(f[i], len, false);
    }
}

5.2 \exp 的平方解

事实上,我们已经在求多项式 $\exp$ 时得到了这么一个式子(见多项式全家桶初等函数段): $$ n[x^n]\exp f(x) = \sum_{i=0}^{n - 1}([x^i]\exp f(x))((n - i)[x^{n - i}]f(x)) $$ 这个式子显然是可以 $O(n^2)$ 转移的。 #### 5.3 $\ln$ 操作 首先肯定可以通过 $\int \frac{f'(x)}{f(x)}$ 得到,但过于复杂。考虑到 $\ln$ 是 $\exp$ 的反函数,即: $$ \begin{aligned} n[x^n]f(x) &= \sum_{i=0}^{n - 1}([x^i]f(x))((n - i)[x^{n - i}]\ln f(x))\\ n[x^n] \ln f(x) \times \underbrace{[x^0]f(x)}_{=1} &= n[x^n]f(x) - \sum_{i=1}^{n - 1}([x^i]f(x))((n - i)[x^{n - i}]\ln f(x))\\ [x^n]\ln f(x) &= [x^n]f(x) - \frac{\sum_{i=1}^{n - 1}([x^i]f(x))((n - i)[x^{n - i}]\ln f(x))}{n}\\ \end{aligned} $$ #### 5.4 求逆 这相当于 $\sum f_if^{-1}_{n - i} = [n = 0]$,直接移项就可以得到。我们可以写出以上三者的代码: ```cpp line-numbers void exp_bf(ll *f, ll *g, int loglen) // 建议不要和我自己写的 POLY::poly_exp 混用,因为 POLY::poly_exp 只支持 2^n 长度 { const ll *inv = get_inv(); f[0] = 1; for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j <= i; j++) { (f[i] += f[i - j] * g[j] % MOD * j) %= MOD; } (f[i] *= inv[i]) %= MOD; } } void ln_bf(ll *f, ll *g, int loglen) { const ll *inv = get_inv(); f[0] = 0; for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j < i; j++) { (f[i] += f[j] * g[i - j] % MOD * j) %= MOD; } f[i] = (g[i] - inv[i] * f[i] % MOD + MOD) % MOD; } } void inv_bf(ll *f, ll *g, int loglen) { f[0] = pow(g[0], MOD - 2, MOD); for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j <= i; j++) { (f[i] += g[j] * f[i - j]) %= MOD; } f[i] = (MOD - f[0] * f[i] % MOD) % MOD; } } ``` ### 6. 完整代码 ```cpp line-numbers #define MAXL 22 #define MAXN 1100005 #define MOD 998244353ll #define I2 499122177ll #define popcnt __builtin_popcount ll pow(ll b, ll p, ll m) { ll r = 1; while (p) { if (p & 1) { r = r * b % m; } b = b * b % m; p >>= 1; } return r; } namespace SSPOLY // subset poly { void fwt_or(ll *f, int len, bool on) { ll type = on ? 1 : -1; for (int h = 2; h <= len; h <<= 1) { for (int j = 0; j < len; j += h) { for (int i = 0; i < (h >> 1); i++) { (f[j + i + (h >> 1)] += MOD + f[j + i] * type) %= MOD; } } } } void fwt_and(ll *f, int len, bool on) { ll type = on ? 1 : -1; for (int h = 2; h <= len; h <<= 1) { for (int j = 0; j < len; j += h) { for (int i = 0; i < (h >> 1); i++) { (f[j + i] += MOD + f[j + i + (h >> 1)] * type) %= MOD; } } } } void fwt_xor(ll *f, int len, bool on) { ll type = on ? 1 : I2; for (int h = 2; h <= len; h <<= 1) { for (int j = 0; j < len; j += h) { for (int i = 0; i < (h >> 1); i++) { ll u = f[j + i], t = f[j + i + (h >> 1)]; f[j + i] = (u + t) * type % MOD; f[j + i + (h >> 1)] = (u - t + MOD) * type % MOD; } } } } /** * @param fwt The FWT function. * Its type should be void (*) (ll *f, int len, bool on). * Provided functions are fwt_or, fwt_and, fwt_xor. */ template <typename FWT_Type> void conv(ll __restrict__ *f, ll __restrict__ *g, int len, ll __restrict__ *h, const FWT_Type fwt) { fwt(f, len, true); fwt(g, len, true); for (int i = 0; i < len; i++) { h[i] = f[i] * g[i] % MOD; } fwt(f, len, false); fwt(g, len, false); fwt(h, len, false); } /** * calculate h(x) = f(x) * g(x), where x^S * x^T = x^{S ∪ T} */ void conv_or(ll __restrict__ *f, ll __restrict__ *g, int len, __restrict__ ll *h) { conv(f, g, len, h, fwt_or); } /** * calculate h(x) = f(x) * g(x), where x^S * x^T = x^{S ∩ T} */ void conv_and(ll __restrict__ *f, ll __restrict__ *g, int len, __restrict__ ll *h) { conv(f, g, len, h, fwt_and); } /** * calculate h(x) = f(x) * g(x), where x^S * x^T = x^{S Δ T} */ void conv_xor(ll __restrict__ *f, ll __restrict__ *g, int len, __restrict__ ll *h) { conv(f, g, len, h, fwt_xor); } /** * calculate h(x) = f(x) * g(x), where x^S * x^T = { 0 if S ∩ T != ∅ * { x^{S ∪ T} else * please offer f, g, h in ll[log len][len], where [x^S]f(x) is stored in f[|S|][S]. */ template <typename ArrayType> void subset_conv(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen, __restrict__ ArrayType h) { int len = 1 << loglen; for (int i = 0; i <= loglen; i++) { fwt_or(f[i], len, true); fwt_or(g[i], len, true); } for (int i = 0; i <= loglen; i++) { for (int j = 0; j < len; j++) { h[i][j] = 0; for (int k = 0; k <= i; k++) { (h[i][j] += f[k][j] * g[i - k][j] % MOD) %= MOD; } } } for (int i = 0; i <= loglen; i++) { fwt_or(f[i], len, false); fwt_or(g[i], len, false); fwt_or(h[i], len, false); } } /** * only inverse element under log len. */ const ll *get_inv() { static ll inv[MAXL]; if (!inv[1]) { for (int i = 1; i < MAXL; i++) { inv[i] = pow(i, MOD - 2, MOD); } } return inv; } void exp_bf(ll *f, ll *g, int loglen) { const ll *inv = get_inv(); f[0] = 1; for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j <= i; j++) { (f[i] += f[i - j] * g[j] % MOD * j) %= MOD; } (f[i] *= inv[i]) %= MOD; } } void ln_bf(ll *f, ll *g, int loglen) { const ll *inv = get_inv(); f[0] = 0; for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j < i; j++) { (f[i] += f[j] * g[i - j] % MOD * j) %= MOD; } f[i] = (g[i] - inv[i] * f[i] % MOD + MOD) % MOD; } } void inv_bf(ll *f, ll *g, int loglen) { f[0] = pow(g[0], MOD - 2, MOD); for (int i = 1; i <= loglen; i++) { f[i] = 0; for (int j = 1; j <= i; j++) { (f[i] += g[j] * f[i - j]) %= MOD; } f[i] = (MOD - f[0] * f[i] % MOD) % MOD; } } /** * calculate f(x) = oper(g(x)), where x^S * x^T = { 0 if S ∩ T != ∅ * { x^{S ∪ T} else * please offer f, g in ll[log len][len], where [x^S]g(x) is stored in g[|S|][S]. * * @param oper The operation on sets. * Its type should be void (*) (ll *f, ll *g, int loglen) where f is the destination * Provided functions are exp_bf, ln_bf, inv_bf */ template <typename ArrayType, typename Oper> void subset_oper(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen, const Oper oper) { int len = 1 << loglen; for (int i = 0; i <= loglen; i++) { fwt_or(g[i], len, true); } static ll F[MAXL], G[MAXL]; for (int j = 0; j < len; j++) { for (int i = 0; i <= loglen; i++) { G[i] = g[i][j]; } oper(F, G, loglen); for (int i = 0; i <= loglen; i++) { f[i][j] = F[i]; } } ll inl = pow(len, MOD - 2, MOD); for (int i = 0; i <= loglen; i++) { fwt_or(g[i], len, false); fwt_or(f[i], len, false); } } /** * calculate f(x) = exp(g(x)) * see subset_oper for more detail */ template <typename ArrayType> void subset_exp(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen) { subset_oper(f, g, loglen, exp_bf); } /** * calculate f(x) = ln(g(x)) * see subset_oper for more detail */ template <typename ArrayType> void subset_ln(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen) { subset_oper(f, g, loglen, ln_bf); } /** * calculate f(x) = 1 / g(x) * see subset_oper for more detail */ template <typename ArrayType> void subset_inv(__restrict__ ArrayType f, __restrict__ ArrayType g, int loglen) { subset_oper(f, g, loglen, inv_bf); } } ``` ### 7. 后记 我也不知道我为什么要学这个,也不知道我能不能记住,实战时更想不到。反正 CCF 不会考,但是 ATC 还是会有的。 实际上多项式全家桶和集合幂级数全家桶还没写完。 已知没写的:多项式复合、复合逆、多项式复合集合幂级数、……