多项式合集
多项式全家桶
注意有以下习惯:
#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\) 个根,即:
我们学过复数,所以显然有:
上面两条性质后面会用。
快速傅里叶变换
我们考虑使用分治来进行变换,毕竟暴力算是 \(n ^ 2\) 的。首先假设多项式有 \(n\) 项且 \(n\) 为 \(2\) 的整数次幂,我们对 \(F(x)\) 进行奇偶性分组,我们可以得到:
设 \(F_1(x) = F[0]x ^ 0 + F[2]x ^ 1 + \cdots\),\(F_2(x) = F[1]x ^ 0 + F[3]x ^ 1 + \cdots\),整理有:
当 \(x = \omega_n ^ k\),得:
注意到这样只能求出 \(k < \frac{n}{2}\) 的情况,那么我们考虑通过带入 \(\omega_n^{k + \frac{n}{2}}\) 来求,得:
分治求解即可,由于递归常数大建议用递推。

蝴蝶变换
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)\),有:
类似的傅里叶变换即可。
证明不会。
代码
注意我们要把长度变成 \(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\)。
显然有:
平方有:
两边乘 \(A\) 有:
所以有:
递推即可,注意边界。
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([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\) 得:
两边同乘 \(x ^ {n - 1}\),有:
带入 \(A_r(x) = A(\frac{1}{x}) \times x ^ {n - 1}\) 有:
由于我们要求 \(Q_r\) 且 \(Q_r\) 最高项小于 \(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 ^ 2 = A\) 并整理,得:
递归即可,当 \(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);
}
求导与积分
很简单没什么好说的。
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\)。
对于原式,我们导一导:
所以有:
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\) 的两倍,那么有:
注意:
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}\)。
带公式得:
多项式 \(\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\),代入牛迭有:
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
快速沃\(_{{}_尔}\)什变换。用于计算位运算卷积,即:
这里 \(\oplus\) 表示一种位运算。
为了方便表示,这变换前为 \(F\) 变换后为 \(G\)。假设变换为 \(FWT()\) 逆变换为 \(IFWT()\),我们目的是构造出 \(FWT()\) 与 \(IFWT()\) 使得:
\({*}_{\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\)。
整理一下:
联立方程可以求解出逆运算,即:
做完了。
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];
}

浙公网安备 33010602011771号