多项式合集

多项式全家桶

注意有以下习惯:

#define int ll
#define ll long long
#define void inline void
#define il inline
#define Mod(x) (((x) % mod + mod) % mod)

书写习惯:

\(F(x)\) 表示一个多项式,有的地方直接省略成 \(F\)\(F[x]\) 表示其第 \(x\) 项系数。

FFT

这篇博客真的写得好!

前置知识

复数、分治、数论。

引入

对于 \(F(x) = a_0 x ^ 0 + a_1 x ^ 1 + \dots + a_{n - 1} x ^ {n - 1}\)\(G(x) = b_0 x ^ 0 + b_1 x ^ 1 + \dots + b_{n - 1} x ^ {n - 1}\),我们要求他的卷积,直接求显然是 \(n ^ 2\),我们如何让他快一点呢?

我们选择另辟蹊径。我们注意到有个东西叫拉插,我们可以用 \(n+1\) 个点值(有序数对)来描述一个 \(n\) 次多项式。我们发现可一若将 \(F(x)\)\(G(x)\) 的每一个点值乘起来,在转回多项式的形式假如说为 \(W(x)\),我们不难发现 \(W(x) = F(x) \times G(x)\)

但是注意到直接算点值再插也是 \(n ^ 2\),但天才傅里叶(但似乎不是)给出了解法。注意到插入单位根并插回去是可以优化的。

单位根

表示方程 \(x ^ n = 1\) 的根,显然有多个。于是我们令 \(\omega^k_n\) 表示第 \(k + 1\) 个根,即:

\[\omega_n^k = e ^ {2\pi \times \frac{k}{n}} \]

我们学过复数,所以显然有:

\[(\omega_n^k) ^ 2 = \omega_n ^ {2k} = \omega_{\frac{n}{2}} ^ k \]

\[\omega_{n}^{k + \frac{n}{2}} = -\omega_n^k \]

\[\omega_{n}^{k + n} = \omega_n^k \]

上面两条性质后面会用。

快速傅里叶变换

我们考虑使用分治来进行变换,毕竟暴力算是 \(n ^ 2\) 的。首先假设多项式有 \(n\) 项且 \(n\)\(2\) 的整数次幂,我们对 \(F(x)\) 进行奇偶性分组,我们可以得到:

\[F(x) = (F[0] + F[2]x ^ 2 + \dots + F[n - 2] x ^ {n - 2}) + (F[1]x + F[3]x ^ 3 + \dots + F[n - 1] x ^ {n - 2}) \]

\(F_1(x) = F[0]x ^ 0 + F[2]x ^ 1 + \cdots\)\(F_2(x) = F[1]x ^ 0 + F[3]x ^ 1 + \cdots\),整理有:

\[F(x) = F_1(x ^ 2) + x \times F_2(x ^ 2) \]

\(x = \omega_n ^ k\),得:

\[F(\omega_n^k) = F_1((\omega_n^k) ^ 2) + \omega_n^k \times F_2((\omega_n^k) ^ 2) \\ = F_1(\omega_{\frac{n}{2}}^k) + \omega_n^k \times F_2(\omega_{\frac{n}{2}}^k) \]

注意到这样只能求出 \(k < \frac{n}{2}\) 的情况,那么我们考虑通过带入 \(\omega_n^{k + \frac{n}{2}}\) 来求,得:

\[F(\omega_n^{k + \frac{n}{2}}) = F_1(\omega_{\frac{n}{2}}^{k + \frac{n}{2}}) + \omega_n^{k + \frac{n}{2}} \times F_2(\omega_{\frac{n}{2}}^{k + \frac{n}{2}}) \\ = F_1(\omega_{\frac{n}{2}}^k) - \omega_n^k \times F_2(\omega_{\frac{n}{2}}^k) \]

分治求解即可,由于递归常数大建议用递推。

蝴蝶变换

0 1 2 3 4 5 6 7 
0 2 4 6|1 3 5 7 
0 4|2 6|1 5|3 7 
0|4|2|6|1|5|3|7 

这是我们求值的过程,显然第一层为新的 \(F(x)\) 记为 \(G(x)\),且 \(G[k] = \omega_n^k\)。我们要想办法求出第4层的数组状况,然后才能往上合并。我们注意到最后的序列是原序列的二进制反转。

我们可以递推求出反转后的位置并提前换过去,这样我们的递推会方便很多。具体细节见后面代码。

逆运算

就是把 \(G(x)\) 插回 \(F(x)\),有:

\[n \times F[k] = \sum_{i = 0} ^ {n - 1} (\omega_n ^ k) ^ i G[i] \]

类似的傅里叶变换即可。

证明不会。

代码

注意我们要把长度变成 \(2\) 的整数次幂。

这里有个常数优化:我们直接算 \(\omega_n^k\) 较慢,我们可以边算边递推地求,有 \(\omega_n ^ k = \omega_n^{k - 1} \times \omega_n ^ 1\)

struct comp {
	double a, b;
	comp(double aa = 0, double bb = 0) { a = aa, b = bb; }
	comp(double rho, double k, double n) {
		a = rho * cos(2 * pi * k / n);
		b = rho * sin(2 * pi * k / n);
	}
} ;
comp operator +(comp a, comp b) { return comp(a.a + b.a, a.b + b.b); }
comp operator -(comp a, comp b) { return comp(a.a - b.a, a.b - b.b); }
comp operator *(comp a, comp b) { return comp(a.a * b.a - a.b * b.b, a.a * b.b + a.b * b.a); }

int Log[maxn];
void pre() {
    Log[0] = -1;
    for(int i = 1; i < maxn; i++) Log[i] = Log[i / 2] + 1;
}
#define omega(k, n) comp(1.00, k, n)
int rev[maxn];
void FFT(comp *a, int n, int type) {
    if((1ll << Log[n]) != n) n = 1ll << (Log[n] + 1);
    for(int i = 0; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (Log[n] - 1));
    for(int i = 0; i < n; i++) if(i < rev[i]) swap(a[i], a[rev[i]]);
    for(int k = 2; k <= n; k *= 2) {
        comp delta = omega(type, k);
        for(int j = 0; j < n; j += k) {
            comp w = comp(1, 0);
            for(int i = j; i < j + k / 2; i++) {
                comp u = a[i], v = a[i + k / 2];
                a[i] = u + v * w;
                a[i + k / 2] = u - v * w;
                w = w * delta; 
            }
        }
    }
    if(type == -1) for(int i = 0; i < n; i++) a[i].a = a[i].a / n, a[i].b = a[i].b / n;
}

NTT

注意到原来用单位根使用精度误差的。发现我们用单位根最核心的原因就是上面的那三条性质,并且其实原根加取模也有类似的性质,代替即可。

int Log[maxn], tmp[maxn]; // tmp 为草稿数组
void prelog() { Log[0] = -1; for(int i = 1; i < maxn; i++) Log[i] = Log[i / 2] + 1; }
il int ksm(int a, int k = mod - 2) { int res = 1; for(; k; k >>= 1, a = a * a % mod) if(k & 1) res = res * a % mod; return res; }
#define omega(k, n) ksm(3, (mod - 1) / (n) * (((k) + mod - 1) % (mod - 1)))
#define check(n) n = (n == (1 << Log[n]) ? n : (1 << (Log[n] + 1)))
int rev[maxn];
void NTT(int *a, int n, int type) {
    check(n);
    for(int i = 0; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (Log[n] - 1));
        for(int i = 0; i < n; i++) if(i < rev[i]) swap(a[i], a[rev[i]]);
        for(int k = 2; k <= n; k *= 2) {
            int delta = omega(type, k);
            for(int j = 0; j < n; j += k) {
                int w = 1;
                for(int i = j; i < j + k / 2; i++) {
                    int u = a[i], v = a[i + k / 2];
                    a[i] = (u + w * v % mod) % mod;
                    a[i + k / 2] = Mod(u - w * v % mod);
                    w = w * delta % mod;
                }
            }
        }
        if(type == -1) for(int i = 0, iv = ksm(n); i < n; i++) a[i] = a[i] * iv % mod;
}

其实一般都用 NTT。

多项式操作

void clear(int *a, int l, int r) { for(int i = l; i < r; i++) a[i] = 0; }

芝士清空。

卷积

直接 NTT 即可。

int tmpr[maxn];
void times(int *res, int *a, int n, int *b, int m) {
    int len = n + m - 1;
    check(len);
    clear(tmp, 0, len), clear(tmpr, 0, len);
    for(int i = 0; i < n; i++) tmpr[i] = a[i];
    for(int i = 0; i < m; i++) tmp[i] = b[i];
    NTT(tmpr, len, 1), NTT(tmp, len, 1);
    for(int i = 0; i < len; i++) res[i] = tmpr[i] * tmp[i] % mod;
    NTT(res, len, -1);
}

多项式求逆

求多项式 \(A(x)\)\(x ^ {n}\) 的幂。

我们考虑分治。

设我们求出了模 \(x ^ \frac{n}{2}\) 的逆元 \(i(x)\)(后面省略 \((x)\)),模 \(x ^ n\) 的逆元为 \(I\)

显然有:

\[I - i \equiv 0 \quad (\bmod~ x ^ \frac{n}{2}) \]

平方有:

\[(I - i) ^ 2 \equiv I ^ 2 - 2 \times i \times I + i ^ 2 \equiv 0 \quad (\bmod~ x ^ n) \]

两边乘 \(A\) 有:

\[I - 2 \times i + i ^ 2 \times A \equiv 0 \quad (\bmod~ x ^ n) \]

所以有:

\[I \equiv 2 \times i - i ^ 2 \times A \quad (\bmod~ x ^ n) \]

递推即可,注意边界。

void Inv(int *a, int *inv, int n) {
    check(n);
    if(n == 1) { inv[0] = ksm(a[0]); AC; }
    Inv(a, inv, n / 2);
    int len = 2 * n; for(int i = n / 2; i < len; i++) inv[i] = 0;
    clear(tmp, 0, len);
    for(int i = 0; i < n; i++) tmp[i] = a[i];
    NTT(tmp, len, 1), NTT(inv, len, 1);
    for(int i = 0; i < len; i++) inv[i] = inv[i] * Mod(2 - tmp[i] * inv[i] % mod) % mod;
    NTT(inv, len, -1); for(int i = n; i < len; i++) inv[i] = 0;
}

分治 FFT

解决这样一个问题:

\[f_i = \sum_{j = 1} ^ i f_{i - j} g_j \quad f_0 = 1 \]

我们考虑分治,考虑 \(f([l, mid])\)\(f([mid + 1, r])\) 的贡献。这个贡献其实是 \(f([l, mid]) \times g([0, r - l])\),直接卷积相加即可。

分治:

int cdqz[maxn];
void Cdq(int *f, int *g, int l, int r) { // f_i = \sum_{j = 1} ^ i f_{i - j} g_j & f_0 = 1
    if(l == r) AC;
    int mid = l + (r - l) / 2;
    Cdq(f, g, l, mid);
    times(cdqz, f + l, mid - l + 1, g, r - l + 1); // 左闭右开所以加 1
    for(int i = mid + 1; i <= r; i++) f[i] = (f[i] + cdqz[i - l]) % mod;
    Cdq(f, g, mid + 1, r);
}

主函数:

void ACehomoxue() {
	prelog();
    cin >> n;
    for(int i = 1; i < n; i++) cin >> g[i];
    f[0] = 1;
    Cdq(f, g, 0, n - 1);
    for(int i = 0; i < n; i++) cout << f[i] << ' ';
    el;
}

多项式除法

\(F = G \times Q + R\),求 \(Q(x)\)\(R(x)\),其中 \(F\)\(n\) 项,\(G\)\(m\) 项,\(Q\)\(n - m + 1\) 项,\(R\)\(m - 1\) 项。

\(A_r(x)\) 表示多项式 \(A\) 系数反转后的多项式,显然有:\(A_r(x) = A(\frac{1}{x}) \times x ^ {n - 1}\),其中 \(n - 1\) 为其最高次数。

那么对于原等式,我们带入 \(\frac1x\) 得:

\[F(\frac1x) = G(\frac1x) \times Q(\frac1x) + G(\frac1x) \]

两边同乘 \(x ^ {n - 1}\),有:

\[F(\frac1x) \times x ^ {n - 1} = (G(\frac1x) \times x ^ {m - 1}) \times (Q(\frac1x) \times x ^ {n - m}) + R(\frac1x) \times x ^ {m - 2} \times x ^ {n - m + 1} \]

带入 \(A_r(x) = A(\frac{1}{x}) \times x ^ {n - 1}\) 有:

\[F_r = G_r \times Q_r + R_r \times x ^ {n - m + 1} \]

由于我们要求 \(Q_r\)\(Q_r\) 最高项小于 \(x ^ {n - m + 1}\),有:

\[F_r \equiv Q_r \times G_r \pmod {x ^ {n - m + 1}} \]

求出 \(G_r\) 此时的逆元即可求解 \(Q_r\),反转求得 \(Q\),再减一下求的 \(R\)

void Rev(int *res, int *a, int n) { n--; for(int i = 0, t; i <= n / 2; i++) t = a[i], res[i] = a[n - i], res[n - i] = t; }
int ivgr[maxn], fr[maxn]; 
void Div(int *f, int n, int *g, int m, int *q, int *r) { // f = g \times q + r
    Rev(fr, f, n), Rev(g, g, m);
    Inv(g, ivgr, n - m + 1); Rev(g, g, m);
    times(q, fr, n, ivgr, n - m + 1);
    Rev(q, q, n - m + 1);
    clear(q, n - m + 1, n); 
    times(r, g, m, q, n - m + 1);
    for(int i = 0; i < m; i++) r[i] = Mod(f[i] - r[i]);
    clear(r, m, n);
}

多项式开根

类似于求逆,设原多项式为 \(A(x)\),求出了模 \(x ^ {\frac n 2}\) 的开根为 \(s(x)\),要求的模 \(x ^ n\) 的开根为 \(S(x)\),则有:

\[s \equiv S \pmod {x ^ {\frac n 2}} \]

移项:

\[s - S \equiv 0 \pmod {x ^ {\frac n 2}} \]

两边平方:

\[S ^ 2 - 2 \times s \times S + s ^ 2 \equiv 0 \pmod {x ^ n} \]

带入 \(S ^ 2 = A\) 并整理,得:

\[\frac {A + s ^ 2} {2s} \equiv S \pmod {x ^ n} \]

递归即可,当 \(n = 1\) 时就用 BSGS 求一下常数项的二次剩余即可。

int ivr[maxn];
void Sqrt(int *a, int *res, int n) {
    if(n == 1) { res[0] = ksm(3, bsgs(a[0]) / 2); AC; } // 二次剩余
    check(n);
    Sqrt(a, res, n / 2); clear(res, n / 2, n);  
    Inv(res, ivr, n); times(res, res, n / 2, res, n / 2);
    for(int i = 0, inv = ksm(2); i < n; i++) res[i] = Mod(a[i] + res[i]) * inv % mod;
    times(res, res, n, ivr, n);
}

求导与积分

很简单没什么好说的。

\[\frac{d}{dx}(\ln x) = \frac{1}{x} \]

\[\frac{d}{dx}(\ln(f(x))) = \frac{1}{f(x)} \cdot f'(x) = \frac{f'(x)}{f(x)} \]

\[\left( \frac{u}{v} \right)' = \frac{u'v - uv'}{v^2} \]

void dydx(int *res, int *a, int n) { for(int i = 1; i < n; i++) res[i - 1] = i * a[i] % mod; res[n - 1] = 0; } // 求导
void inte(int *res, int *a, int n) { for(int i = n - 2; i >= 0; i--) res[i + 1] = a[i] * ksm(i + 1) % mod; res[0] = 0; } // 积分

多项式 \(\ln\)

\(G \equiv \ln F \pmod {x ^ n}\),保证 \(F[0] = 1\)

对于原式,我们导一导:

\[G ^ {\prime} \equiv \frac {F ^ {\prime}} {F} \pmod {x ^ n} \]

所以有:

\[G \equiv \int \frac{F^\prime}{F} dx \pmod{x ^ n} \]

int iva[maxn];
void ln(int *a, int *res, int n) { // a_0 = 1
    check(n);
    Inv(a, iva, n);
    for(int i = 0; i < n; i++) res[i] = a[i];
    dydx(res, res, n);
    times(res, res, n, iva, n);
    inte(res, res, n);
    clear(res, n, 2 * n);
}

牛顿迭代

定义

对于 \(f(x) = 0\) 的根,假设已经取到一个近似值 \(x_0\),设 \(x_1\)\(f(x) = 0\) 近似值且精度是 \(x_0\) 的两倍,那么有:

\[x_1 = x_0 - \frac{f(x_0)}{f ^ \prime(x_0)} \]

注意:

1.需选择一个合适的初值,否则迭代式可能不收敛。

2.函数需连续可导

证明

对于我们的近似值 \(x_0\),我们过 \((x_0, f(x_0))\) 做切线,切线即为 \(g(x) = f ^ \prime(x_0)(x - x_0) + f(x)\),于是 \(x_1\) 即为 \(g(x) = 0\) 的解,解出得 \(x_1 = x_0 - \frac{f(x_0)}{f ^ \prime (x_0)}\)

可以到这里来看看。

对于多项式

以求逆为例。

\(f(G) = \frac{1}{G} - F \equiv 0\),有 \(f ^ \prime (G) = - \frac{1}{G ^ 2}\)

带公式得:

\[G = G_0 - \frac{f(G_0)}{f ^ \prime (G_0)} = 2G_0 - F \times {G_0} ^ 2 \]

多项式 \(\exp\)

\(G(x) \equiv e ^ {F(x)} \pmod {x ^ n}\),且 \(F[0] = 0\)

因为 \(G \equiv e ^ F \pmod {x ^ n}\),所以有 \(\ln {G} \equiv F \pmod {x ^ n}\),我们设 \(f(X) = ln {X} - F = 0\),代入牛迭有:

\[G = G_0 - \frac{f(G_0)}{f ^ \prime (G_0)} = G_0 - \frac{\ln G_0 - F}{\frac{1}{G_0}} \]

\[= G_0 - G_0 \ln G_0 + F \times G_0 \]

\[= G_0(1 - \ln G_0 + F) \]

int lg[maxn]; 
void Exp(int *a, int *res, int n) {
    if(n == 1) { res[0] = 1; AC; }
    Exp(a, res, n / 2); clear(res, n / 2, n);
    ln(res, lg, n);
    for(int i = 0; i < n; i++) lg[i] = Mod((i == 0) + a[i] - lg[i]);
    times(res, res, n, lg, n);  clear(res, n, n * 2);
}

多项式快速幂

\(G(x) \equiv F(x) ^ k \pmod {x ^ n}\)

对于 \(a_0 = 1\),很好办,答案就是 \(e ^ {k \ln A}\)

int la[maxn];
void qp(int *a, int *res, int n, int k) { // a_0 = 1
    check(n);
    ln(a, la, n);
    for(int i = 0; i < n; i++) la[i] = la[i] * k % mod;
    Exp(la, res, n);
}

对于 \(a_0 \ne 1\),我们考虑转化成 \(a_0 = 1\)。假设第一个不为零的位置为 \(k_0\),则去掉前缀 \(0\) 并给后面除掉 \(a[k_0]\),后面再乘上 \(a[k_0] ^ {k_2}\),这里的 \(k_2 \equiv k \pmod {\varphi{(mod)}}\)。具体细节看代码。

int la[maxn];
void qp(int *a, int *res, int n, int k) { // a_0 = 1
    check(n);
    ln(a, la, n);
    for(int i = 0; i < n; i++) la[i] = la[i] * k % mod;
    Exp(la, res, n);
}
int ff[maxn];
void Ksm(int *a, int *res, int n, string &s) {
    clear(res, 0, n * 2), clear(ff, 0, n * 2);
    int k1 = 0, k2 = 0, k0 = 0, c = 0, ivc = 0, d = 0;
    for(int i = 0; i < n; i++) if(a[i] != 0) { k0 = i; break; }
    for(auto c : s) {
        int x = c - '0';
        k1 = (k1 * 10 + x) % mod;
        d = min(d * 10 + x, n);
        k2 = (k2 * 10 + x) % (mod - 1);
    } 
    c = a[k0], ivc = ksm(c);
    for(int i = 0; i < n; i++) ff[i] = a[i] * ivc % mod;
    c = ksm(c, k2);
    d = min(d * k0, n);
    if(n == d) AC;
    qp(ff + k0, res + d, n - d, k1);
    for(int i = 0; i < n; i++) res[i] = res[i] * c % mod;
    clear(res, n, n * 2);
}

FWT

快速沃\(_{{}_尔}\)什变换。用于计算位运算卷积,即:

\[C[i] = \sum_{{i - j} \oplus k} A[i - j]B[k] \]

这里 \(\oplus\) 表示一种位运算。

为了方便表示,这变换前为 \(F\) 变换后为 \(G\)。假设变换为 \(FWT()\) 逆变换为 \(IFWT()\),我们目的是构造出 \(FWT()\)\(IFWT()\) 使得:

\[G = FWT(F) \quad F = IFWT(G) \quad (F1 {*}_{\oplus} F_2)[i] = IFWT(G1 {*}_{\oplus} G2)[i] \]

\({*}_{\oplus}\) 代表位运算卷积。

这些代码之前都应有这些预处理:

int Log[maxn];
void prelog() { Log[0] = -1; for(int i = 1; i < maxn; i++) Log[i] = Log[i / 2] + 1; }
#define check(n) n = (n == (1 << Log[n]) ? n : (1 << (Log[n] + 1)))
il int ksm(int a, int k = mod - 2) { int res = 1; for(; k; k >>= 1, a = 1ll * a * a % mod) if(k & 1) res = 1ll * res * a % mod; return res; }

按位或

为了变换后能直接乘,我们需使 \(G[i] = \sum_{i = i | j} F[i]\)

按位地推显然有:

void Or(int *a, int n, int type) {
    check(n);
    for(int k = 2; k <= n; k *= 2) {
        for(int j = 0; j < n; j += k) {
            for(int i = j; i < j + k / 2; i++) {
                int u = a[i], v = a[i + k / 2];
                a[i] = u;
                a[i + k / 2] = Mod(v + type * u);
            }
        }
    }
}

这样枚举的 \(i\)\(i + k / 2\) 两个点,枚举到的位的下面全部相同,而枚举到的位上 \(i\)\(0\)\(i + k / 2\)\(1\),按照定义加减贡献即可。

按位与

同或,都很简单。

void And(int *a, int n, int type) {
    check(n);
    for(int k = 2; k <= n; k *= 2) {
        for(int j = 0; j < n; j += k) {
            for(int i = j; i < j + k / 2; i++) {
                int u = a[i], v = a[i + k / 2];
                a[i] = Mod(u + v * type);
                a[i + k / 2] = v;
            }
        }
    }
}

按位异或

\(G[i]\) 的定义于上面两个运算有差别。

这里的 \(G[i] = \sum_j (-1) ^ {|i \& j|} F[i]\),其中 \(|i \& j|\) 表示 \(i\) 按位与 \(j\)\(1\) 的个数。我们发现这样直接乘是可行的。

证明

在有两个原始数组 \(a\)\(b\),它们经过变换后得到了新数组 \(a'\)\(b'\)。按照你的定义,在某一个位置 \(i\),我们将它们对应位置相乘:$$a'[i] \times b'[i] = \left( \sum_{j} (-1)^{|i \text{ AND } j|} a[j] \right) \times \left( \sum_{k} (-1)^{|i \text{ AND } k|} b[k] \right)$$利用乘法分配律,我们把这两个求和符号强行乘开,打包到一起:$$a'[i] \times b'[i] = \sum_{j} \sum_{k} (-1)^{|i \text{ AND } j| + |i \text{ AND } k|} a[j]b[k]$$现在,请把所有的注意力集中在指数上:$$|i \text{ AND } j| + |i \text{ AND } k|$$这个式子表示:计算 \(i\)\(j\) 的重合 1 的个数,加上 \(i\)\(k\) 的重合 1 的个数。而在模 2(即只看奇偶性)的眼光下,这个加法有一个惊人的性质:对于二进制的任何一位,只有当 \(j\)\(k\) 的这一位不相等(一个是 0 一个是 1)时,它与 \(i\) 的这一位相与才会对总和的奇偶性产生 1 的贡献。这不就是异或(\(\oplus\))的定义吗?因此,在奇偶性上,它完美等价于:$$|i \text{ AND } j| + |i \text{ AND } k| \equiv |i \text{ AND } (j \oplus k)| \pmod 2$$既然奇偶性相同,那么底数是 \(-1\) 时,结果就完全一样!我们可以把原式化简为:$$a'[i] \times b'[i] = \sum_{j} \sum_{k} (-1)^{|i \text{ AND } (j \oplus k)|} a[j]b[k]$$

现在我们令 \(j \oplus k = R\)(也就是异或卷积后的下标)。我们在上面的双重循环中,把所有满足 \(j \oplus k = R\) 的项组合起来。根据异或卷积的定义,原本暴力算出来的结果数组 \(c\) 的定义就是 \(c[R] = \sum_{j \oplus k = R} a[j]b[k]\)。我们把 \(c[R]\) 代入上式,它就变成了:$$a'[i] \times b'[i] = \sum_{R} (-1)^{|i \text{ AND } R|} c[R]$$看!右边这个式子,不就正好是根据你的定义,对数组 \(c\) 进行 XOR 变换后得到的 \(c'[i]\) 吗?也就是说:$$a'[i] \times b'[i] = c'[i]$$这就是异或变换能够把卷积变成对应点乘的终极数学原因。它通过 \((-1)^{|i \text{ AND } j|}\) 这个神奇的权重,把下标的异或运算悄悄偷换成了指数上的加法运算。

显然我并不会证明所以这是 ai 写的。

我们设枚举到了第 \(k\) 位,这一位是 \(0\) 的数的贡献是 \(u\),是 \(1\) 的贡献是 \(v\),我们考虑合并这两个的贡献。

\(i\) 这一位是 \(0\),不管怎么与都不会增加 \(0\) 的个数,所以直接有:\(u ^ \prime = u + v\)

当这一位是 \(1\),那么如果与了一个这一位是 \(1\) 的就会增加一个 \(1\),所以这一位是 \(1\) 的应将贡献乘 \(-1\),贡献即为 \(-v\);若与了一个这一位为 \(0\)\(1\) 的个数还是不变,贡献就为 \(u\)

整理一下:

\[u ^ \prime = u + v \quad v ^ \prime = u - v \]

联立方程可以求解出逆运算,即:

\[u = \frac {u ^ \prime + v ^ \prime} {2} \quad v = \frac {u ^ \prime - v ^ \prime} {2} \]

做完了。

void Xor(int *a, int n, int type) {
    check(n);
    for(int k = 2, iv = ksm(2); k <= n; k *= 2) {
        for(int j = 0; j < n; j += k) {
            for(int i = j; i < j + k / 2; i++) {
                int u = a[i], v = a[i + k / 2];
                a[i] = (u + v) % mod; 
                a[i + k / 2] = Mod(u - v);
                if(type == -1) a[i] = 1ll * a[i] * iv % mod, a[i + k / 2] = 1ll * a[i + k / 2] * iv % mod;
            }
        }
    }
}

子集卷积

\(h_x = \sum_{j | i = x \land i \And j = 0} f_i g_j\)

注意到 \(i \And j = 0\) 可以转换为 \(|i| + |j| = |x|\),做二维卷积即可,一维按集合大小暴力加法卷积,二维利用 FWT 优化进行或卷积,设卷积后得到 \(res_{i, j}\),那么有 \(h_x = res_{|x|, x}\)。做完了,复杂度 \(O(n \log ^ 2n)\)

int hh[maxd][maxn], ff[maxd][maxn], gg[maxd][maxn];
void Times(int *a, int *b, int n, int *res) {
    check(n);
    memset(hh, 0, sizeof(hh));
    memset(ff, 0, sizeof(ff));
    memset(gg, 0, sizeof(gg));
    int d = Log[n];
    for(int i = 0; i < n; i++) ff[__builtin_popcountll(i)][i] = Mod(a[i]), gg[__builtin_popcount(i)][i] = Mod(b[i]);
    for(int i = 0; i <= d; i++) Or(ff[i], n, 1), Or(gg[i], n, 1);
    for(int i = 0; i <= d; i++) {
        for(int j = 0; i + j <= d; j++) {
            for(int s = 0; s < n; s++) hh[i + j][s] = (hh[i + j][s] + 1ll * ff[i][s] * gg[j][s] % mod) % mod;
        }
    }
    for(int i = 0; i <= d; i++) Or(hh[i], n, -1);
    for(int i = 0; i < n; i++) res[i] = hh[__builtin_popcount(i)][i];
}
posted @ 2026-05-25 18:04  ACehomoxue  阅读(26)  评论(0)    收藏  举报