多项式基础
创建时间:2026-01-20
前置知识
复数的定义
我们把形如 \(z = a + bi\)(\(a,b \in \R\)) 的数叫做复数(complex number),其中 \(i\) 为虚数单位,是方程 \(x^2 = -1\) 的解。
对于复数 \(z = a + bi\),其中 \(a\) 叫做 \(z\) 的实部(real part),\(b\) 叫做 \(z\) 的虚部(imaginary part)。
复数四则基本运算
设 \(z_1 = a + bi, z_2 = c + di\),那么
显然,复数满足加法交换律、加法结合律、乘法交换律、乘法结合律、乘法分配律。
复数的几何意义
因为一个复数 \(z = a + bi\) 可以唯一对应一个二元组 \((a,b)\),所以 \(z\) 能唯一对应一个平面直角坐标系中坐标 \((a,b)\)。同理,一个坐标 \((a,b)\) 也能唯一确定一个复数 \(z = a + bi\)。我们将上述坐标系称为复平面,\(x\) 轴叫做实轴,\(y\) 轴叫做虚轴。
欧拉定理:\(e^{ix} = \cos x + i \sin x\)。
\(n\) 次单位根的定义
定义 \(n\) 次单位根(n-th unit root)为复数域下方程 \(x^n = 1\) 的所有解。
可以证明,这 \(n\) 个单位根分别为
或
其中 \(k = 0, 1, 2, \cdots, n - 1\)(或 \(k = 1, 2, 3, \cdots, n\))。
令 \(\omega_n = \cos (\frac {2\pi} n) + i \sin (\frac {2\pi} n)\),则任意一个 \(n\) 次单位根都可以写成 \(\omega_n^k\) 的形式。
\(n\) 次单位根的几何意义
将所有 \(n\) 次单位根放到复平面上,不难发现它们将单位圆 \(n\) 等分,所以 \(\omega_n^k\) 可以理解为将单位圆分成 \(n\) 份,取其中 \(k\) 份。
故容易发现,\(\omega_n^n = 1 = \omega_n^0\)。
\(n\) 次单位根的性质
性质1,消去引理:
::::info
代数证明:
\(\omega_{dn}^{dk} = \cos (\frac {2dk\pi} {dn}) + i \sin (\frac {2dk\pi} {dn}) = \cos (\frac {2k\pi} n) + \sin(\frac {2k\pi} n) = \omega_n\)。
直观理解:
把单位圆分成 \(dn\) 份取其中 \(dk\) 份等价于把单位圆分成 \(n\) 份取其中 \(k\) 份。
::::
性质2,折半引理:
当 \(n\) 为偶数时,\(n\) 个 \(n\) 次单位根的平方构成的集合等于 \(\frac n 2\) 个 \(\frac n 2\) 次单位根构成的集合。即,\((\omega_n^{k + \frac n 2})^2 = (\omega_n^k)^2 = \omega_{\frac n 2}^k\)。
::::info
代数证明:
\((\omega_n^{k + \frac n 2})^2 = \omega_n^{2k + n} = \omega_n^{2k} \omega_n^n = \omega_n^{2k} \times 1 = \omega_n^{2k}\),而 \(\omega_n^{2k} = (\omega_n^k)^2\),又根据消去引理得 \(\omega_n^{2k} = \omega_{\frac n 2}^k\)。
直观理解:
在复平面上,\(\omega_n^{k + \frac n 2}\) 可以理解为 \(\omega_n^k\) 逆时针旋转 \(\pi\),故 \(\omega_n^{k + \frac n 2}\) 的值和 \(\omega_n^k\) 的值互为相反数,所以 \((\omega_n^{k + \frac n 2})^2 = (\omega_n^k)^2\)。又因为 \((\omega_n^k)^2\) 的辐角等于 \(\omega_n^k\) 的辐角的两倍,也就是 \(\omega_n^{2k}\),这就等于 \(\omega_{\frac n 2}^{k}\)。
::::
性质3,求和引理:
::::info
证明:
当 \(n \mid k\) 时,\(\forall 0 \le j \le n - 1, \omega_n^{jk} = (\omega_n^k)^j = 1^j = 1\),所以 \(\sum_{j=0}^{n-1} \omega_n^{jk} = n\)。
当 \(n \nmid k\) 时,运用等比数列求和公式,\(\sum_{j=0}^{n-1} \omega_n^{jk} = \omega_n^0 + \omega_n^k + \omega_n^{2k} + \cdots + \omega_n^{(n - 1)k} = \frac {\omega_n^k \omega_n^{(n - 1)k} - \omega_n^0} {\omega_n^k - 1} = \frac {\omega_n^{nk} - \omega_n^0} {\omega_n^k - 1} = \frac {1 - 1} {\omega_n^k - 1} = 0\)。
::::
多项式
多项式的定义
在 OI 中,如果 \(P(x) = a_n x^n + a_{n-1} x^{n-1} + a_{n-2} x^{n-2} + \cdots + a_0\)(\(a_n \neq 0\)),我们把 \(P(x)\) 叫做一个多项式(polynomial),进一步,称 \(P(x)\) 是一个 \(n\) 次多项式,其中 \(n\) 叫做它的次数。
如果一个数 \(N\) 严格大于多项式 \(P(x)\) 的次数,则把 \(N\) 叫做 \(P(x)\) 的一个次数界。
多项式的基本运算
令 \(n\) 次多项式 \(A(x) = \sum_{i=0}^n a_i x^i\)(\(a_n \neq 0\)),\(m\) 次多项式 \(B(x) = \sum_{i=0}^m b_i x^i\)(\(a_m \neq 0\))。
当 \(i>n\) 时,认为 \(a_i = 0\),对 \(m,b_i\) 同理。
假设 \(C(x) = A(x) + B(x)\),那么
故 \(C(x) = A(x) + B(x)\) 是一个次数不超过 \(\max(n,m)\) 的多项式。
减法类似,\(C(x) = A(x) - B(x)\) 也是一个次数不超过 \(\max(n,m)\) 的多项式。
假设 \(C(x) = A(x) B(x)\),则
因为 \(a_n \neq 0, b_m \neq 0\),所以 \(a_n b_m \neq 0\),所以 \(C(x) = A(x) B(x)\) 是一个次数恰好为 \(n+m\) 的多项式。
多项式的系数表示法
在前面的计算中,我们都是通过形如 \(a_n x^n + a_{n-1} x^{n-1} + a_{n-2} x^{n-2} + \cdots + a_0\) 表示一个多项式的,这被称为多项式的系数表示法。
显然,一个多项式有且只有唯一一个系数表示法,一个系数表示法也只能表示唯一一个多项式。
系数表示法的优点:
- 直观清晰
- 可以快速求多项式单点值
- 方便求导、反导
然而,系数表示法有一个明显的缺点:乘法等复杂运算太慢了。
多项式的点值表示法
对于一个 \(n\) 次多项式 \(P(x)\),将 \(n+2\) 个不同数 \(x_1,x_2,x_3,\cdots,x_{n+2}\) 带入 \(P(x)\),求出多项式的值 \(y_1,y_2,y_3,\cdots,y_{n+2}\),则 \((x_1,y_1),(x_2,y_2),(x_3,y_3),\cdots,(x_{n+2},y_{n+2})\) 就是 \(P(x)\) 的一组点值表示法。
根据一些奇妙的数学知识,只要有至少 \(n+2\) 个不同 \(x\) 的点值,就能唯一确定一个 \(n\) 次多项式。
故,一个 \(n\) 次多项式可以有多个点值表示法,而有一个 \(n+2\) 对数的点值表示法只能唯一确定 \(n\) 次一个多项式。
如果使用点值表示法,加减乘等很多运算都能在 \(O(n)\) 的时间内快速计算。
离散傅里叶变换
前面说到多项式系数表示法直观清晰,但运算效率低;点值表示法运算很快,但较为抽象。那能否将二者结合?答案是可以的。
以多项式乘法为例,给定 \(n\) 次多项式 \(A(x) = a_n x^n + a_{n-1} x^{n-1} + a_{n-2} x^{n-2} + \cdots + a_0\),\(m\) 次多项式 \(B(x) = b_m x^m + b_{m-1} x^{m-1} + b_{m-2} x^{m-2} + \cdots + b_0\)(\(n,m \le 10^6\)),试求 \(C(x) = A(x) B(x)\) 的系数表示法。
直接套多项式乘法公式是 \(O(n^2)\) 的,不够优秀。
因为点值表示法可以 \(O(n)\) 计算乘法,所以可以考虑选取 \(k\) 个不同的数 \(x_1,x_2,x_3,\cdots,x_k\),分别求出它们在 \(A(x),B(x)\) 上的取值 \(y_{1 1}, y_{1 2}, y_{1 3}, \cdots, y_{1 k}\) 与 \(y_{2 1}, y_{2 2}, y_{2,3}, \cdots, y_{2 k}\),得到 \(C(x)\) 的点值表示法 \((x_1, y_{1 1} y_{2 1}), (x_2, y_{1 2} y_{2 2}), (x_3, y_{1 3} y_{2 3}), \cdots, (x_k, y_{1 k} y_{2 k})\)。再由点值表示法求出 \(C(x)\) 的系数表示法就能解决问题。
我们把由系数表示法到点值表示法的过程称为离散傅里叶变换(Discrete Fourier Transform, DFT);点值表示法到系数表示法的过程称为逆离散傅里叶变换(Inverse Discrete Fourier Transform, IDFT)。
快速傅里叶变换
快速傅里叶变换
设 \(n\) 是偶数,有多项式 \(A(x) = a_0 + a_1 x + a_2 x^2 + \cdots + a_n^{n-1} x^{n-1}\)(根据定义 \(n\) 是 \(A(x)\) 的一个次数界)。
考虑求出 \(1,\omega_n,\omega_n^2,\cdots, \omega_n^{n-1}\) 在 \(A(x)\) 上的值。
将 \(A(x)\) 的系数按奇偶分组,令
所以
根据折半引理,\((\omega_n^k)^2 = (\omega_n^{k + \frac n 2})^2 = \omega_{\frac n 2}^k\)
所以,只要求出 \(1,\omega_{\frac n2 },\omega_{\frac n 2}^2,\cdots, \omega_{\frac n 2}^{\frac n 2 - 1}\) 在 \(A^{|0|},A^{|1|}\) 上的值,就能等同于知道 \(A^{|0|}((\omega_n^k)^2)\)、\(A^{|1|}((\omega_n^k)^2)\) 的值,进而求出 \(\omega_n^k\) 在 \(A(x)\) 上的值。
故只要当 \(n\) 是偶数的时候就能按上述方法递归分治,当 \(n=1\) 时直接返回。为了保证在整个过程的可行性,需要在 \(n\) 不是 \(2\) 的幂时将 \(n\) 补全为 \(2\) 的幂。
这就是快速傅里叶变换(Fast Fourier Transform, FFT) 的过程,它在 \(O(n \log n)\) 的时间内求出 \(1,\omega_n,\omega_n^2,\cdots, \omega_n^{n-1}\) 在任意多项式 \(A(x)\) 上的值 。
具体实现:
typedef complex<double> comp;
void FFT(comp a[], int tot) {
if (tot == 1)
return ;
comp a0[tot / 2];
comp a1[tot / 2];
for (int i = 0; i < tot / 2; i++) {
a0[i] = a[i * 2];
a1[i] = a[i * 2 + 1];
}
FFT(a0, tot / 2);
FFT(a1, tot / 2);
comp wn(cos(2 * Pi / tot), sin(2 * Pi / tot));
comp w(1, 0);
for (int i = 0; i < tot / 2; i++) {
a[i] = a0[i] + w * a1[i];
a[i + tot / 2] = a0[i] - w * a1[i];
w *= wn;
}
}
int main() {
cin >> n;
for (int i = 0; i <= n; i++)
cin >> a[i];
int tot = 1 << __lg(n) + 1;
FFT(a, tot);
for (int i = 0; i < tot; i++)
cout << a[i] << ' ';
return 0;
}
逆快速傅里叶变换
逆快速傅里叶变换(Inverse Fast Fourier Transform, IFFT),和 IDFT 类似。刚刚我们已经将系数表示法转换为了 \(1,\omega_n,\omega_n^2,\cdots, \omega_n^{n-1}\) 处的点值表示法,现在考虑怎么转换回去。
这里有一个很 nb 的结论:
设原系数表示法是 \(A(x) = a_0 + a_1 x + a_2 x^2 + \cdots + a_n^{n-1} x^{n-1}\),\(1,\omega_n,\omega_n^2,\cdots, \omega_n^{n-1}\) 处的值分别为 \(y_0, y_1, y_2, \cdots, y_{n-1}\),则
证明可以用求和引理,这里不证了。
注意到 \(\frac 1 n \sum_{j=0}^{n-1} y_j \omega_n^{-jk} = \frac 1 n \sum_{j=0}^{n-1} y_j (\omega_n^{-j})^k\)。
上面式子不就是由多项式 \(y_0 + y_1 x + y^2 x^2 + \cdots + y_{n-1} x^{n-1}\) 在 \(1, \omega_n^{-1}, \omega_n^{-2}, \cdots, \omega_n^{-(n-1)}\) 处的点值再除以 \(n\) 吗?所以可以同 FFT 的方法求出 \(A(x)\) 的系数表示法。
typedef complex<double> comp;
void FFT(comp a[], int tot, int sign = 1) {
if (tot == 1)
return ;
comp a0[tot / 2];
comp a1[tot / 2];
for (int i = 0; i < tot / 2; i++) {
a0[i] = a[i * 2];
a1[i] = a[i * 2 + 1];
}
FFT(a0, tot / 2, sign);
FFT(a1, tot / 2, sign);
comp wn(cos(2 * Pi / tot), sign * sin(2 * Pi / tot));
comp w(1, 0);
for (int i = 0; i < tot / 2; i++) {
a[i] = a0[i] + w * a1[i];
a[i + tot / 2] = a0[i] - w * a1[i];
w *= wn;
}
}
int main() {
cin >> n;
for (int i = 0; i <= n; i++)
cin >> a[i];
int tot = 1 << __lg(n) + 1;
FFT(a, tot); // 快速傅里叶变换
FFT(a, tot, -1); // 逆快速傅里叶变换
for (int i = 0; i <= n; i++)
cout << int(a[i].real() / tot + 0.5) << ' '; // 四舍五入,避免精度误差
return 0;
}
蝴蝶变换
上面的所有代码都是基于递归实现的,常数比较大,再加上频繁在递归函数内开数组,可能有爆栈的风险。如果可能,我们应该尽量避免递归实现。
观察 FFT 函数的递归过程:

思考怎么通过迭代地合并来求出答案。
易知,最底层本质上是 \(\{a_0, a_1, a_2, a_3, a_4, a_5, a_6, a_7\}\) 的一个置换,那这个置换有什么规律吗?
这是 \(\{0, 1, 2, 3, 4, 5, 6, 7\}\) 置换前的二进制表示:
置换后 \(\{0, 4, 2, 6, 1, 5, 3, 7\}\) 的二进制表示:
不难发现,置换本质上是将 \(\{0,1,2,3,4,5,6,7\}\) 做位逆序排列(bit-reversal permutation),即把 \(i\) 的二进制表示法前后翻转。
所以只要求出 \(0, 1, 2, 3, \cdots, n - 1\) 的位逆序排列,就能通过一层层向上合并得到答案,这便是蝴蝶变换(butterfly operator)的思想。而位逆序排列可以递推求出,详见代码部分。
void FFT(comp a[], int tot, int sign = 1) {
for (int i = 0; i < tot; i++)
if (i < rev[i])
swap(a[i], a[rev[i]]);
for (int i = 1; i < tot; i <<= 1) {
comp wn(cos(Pi / i), sign * sin(Pi / i));
for (int j = 0; j < tot; j += i << 1) {
comp w(1, 0);
for (int k = 0; k < i; k++) {
auto x = a[j + k], y = w * a[j + k + i];
a[j + k] = x + y;
a[j + k + i] = x - y;
w *= wn;
}
}
}
if (sign == -1)
for (int i = 0; i < tot; i++)
a[i] /= tot;
}
int main() {
cin >> n;
for (int i = 0; i <= n; i++)
cin >> a[i];
int bit = 0, tot = 1;
while (tot <= n)
bit++, tot <<= 1;
for (int i = 0; i < tot; i++)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << bit - 1);
FFT(a, tot);
FFT(a, tot, -1);
for (int i = 0; i <= n; i++)
cout << int(a[i].real() + 0.5) << ' ';
return 0;
}
快速傅里叶变换的应用
快速傅里叶变换最经典的也是最基础的应用就是前面 DFT 中提到多项式乘法。
根据先前 DFT 中讲到的思路,我们可以先用 FFT 把 \(A(x), B(x)\) 分别转成点值表示法,将其点值相乘,就得到了 \(C(x)\) 的点值表示法,再通过 IDFT 转回系数表示法,就解决了整个问题。
主函数代码:
int main() {
cin >> n >> m;
for (int i = 0; i <= n; i++)
cin >> a[i];
for (int i = 0; i <= m; i++)
cin >> b[i];
int bit = 0, tot = 1;
while (tot <= n + m)
bit++, tot <<= 1;
for (int i = 0; i < tot; i++)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << bit - 1);
FFT(a, tot);
FFT(b, tot);
for (int i = 0; i < tot; i++)
c[i] = a[i] * b[i];
FFT(c, tot, -1);
for (int i = 0; i <= n + m; i++)
cout << int(c[i].real() + 0.5) << ' ';
return 0;
}
快速数论变换
数论变换(Number-Theoretic Transform, NTT),是 DFT 在数论板块中呈现的形式,即也求出多项式的点值表示法,不过是某个模数意义下的。换句话说,就是把结果对某个数去取模。
快速数论变换(Fast Number-Theoretic Transform, FNTT),则是 FFT 在数论板块中呈现的形式,其实现几乎和 FFT 一样。在 OI 中提到 NTT,一般默认为快速数论变换。
FFT 是利用了复数域下 \(n\) 次单位根来加速计算,那 NTT 自然要利用数论意义下的 \(n\) 次单位根。
设模数为 \(M\),我们可以求出 \(M\) 的原根 \(g\),即 \(x^{\varphi(M)} \equiv 1 \pmod M\) 在 \([0, M)\) 范围内的最小整数解,则 $\forall 0 \le j < n, (\omega_n)^j \equiv 1 \pmod M $,所以可以令 \(\omega_n = g^{\frac {\varphi(M)} n}\),且它与复数域下原根有着很多相似性质,可以用于分治求解点值。
一般 NTT 的题目中 \(M\) 都是一个形如 \(p \times 2^k +1\) 且 \(2^k\) 足够大的质数,常用模数表如下:
| \(M\) | \(k\) | \(g\) |
|---|---|---|
| \(104857601\) | \(22\) | \(3\) |
| \(167772161\) | \(25\) | \(3\) |
| \(469762049\) | \(26\) | \(3\) |
| \(754974721\) | \(23\) | \(11\) |
| \(950009857\) | \(21\) | \(7\) |
| \(998244353\) | \(23\) | \(3\) |
| \(1004535809\) | \(21\) | \(3\) |
| \(2013265921\) | \(27\) | \(31\) |
| \(2281701377\) | \(27\) | \(3\) |
| \(3221225473\) | \(30\) | \(5\) |
除了 \(\omega_n\) 的定义不同,以及运算中需要取模,NTT 的剩余部分和 FFT 完全相同。
代码:
void NTT(int a[], int tot, int sign = 1) {
for (int i = 0; i < tot; i++)
if (i < rev[i])
swap(a[i], a[rev[i]]);
int invg = qpow(g, mod - 2);
for (int i = 1; i < tot; i <<= 1) {
int wn = qpow(sign == 1 ? g : invg, (mod - 1) / 2 / i);
for (int j = 0; j < tot; j += i << 1) {
int w = 1;
for (int k = 0; k < i; k++) {
int x = a[j + k], y = 1ll * w * a[j + k + i] % mod;
a[j + k] = x + y < mod ? x + y : x + y - mod;
a[j + k + i] = x - y >= 0 ? x - y : x - y + mod;
w = 1ll * w * wn % mod;
}
}
}
if (sign == -1) {
int invtot = qpow(tot, mod - 2);
for (int i = 0; i < tot; i++)
a[i] = 1ll * a[i] * invtot % mod;
}
}
习题
P3803 【模板】多项式乘法(FFT)
P3723 [AHOI2017/HNOI2017] 礼物
P4245 【模板】任意模数多项式乘法
P3338 [ZJOI2014] 力
P5488 差分与前缀和
P4173 残缺的字符串
P5641 【CSGRound2】开拓者的卓识
P4721 【模板】分治 FFT
P8958 「CGOI-3」残暴圣所
P5395 第二类斯特林数·行
P4566 [CTSC2018] 青蕈领主

浙公网安备 33010602011771号