多项式基础

创建时间: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_1 + z_2 = (a + b) + (c + d)i \]

\[z_1 - z_2 = (a - b) + (c - d)i \]

\[\begin{aligned} z_1 z_2 &= ac + adi + bci + bdi^2 \\ &= ac + adi + bci - bd \\ & = (ac - bd) + (ad + bc)i \end{aligned}\]

显然,复数满足加法交换律、加法结合律、乘法交换律、乘法结合律、乘法分配律。

复数的几何意义

因为一个复数 \(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\) 个单位根分别为

\[\cos (\frac {2k\pi} n) + i \sin (\frac {2k\pi} n) \]

\[e^{i \frac {2k\pi} 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,消去引理:

\[\omega_{dn}^{dk} = \omega_n^k \]

::::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,求和引理:

\[\begin{aligned} \sum_{j=0}^{n-1} \omega_n^{jk} = \begin{cases} n & n \mid k \\ 0 & n \nmid k \end{cases} \end{aligned}\]

::::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) = \sum_{i=0}^{\max(n, m)} (a_i + b_i) x^i \]

\(C(x) = A(x) + B(x)\) 是一个次数不超过 \(\max(n,m)\) 的多项式。

减法类似,\(C(x) = A(x) - B(x)\) 也是一个次数不超过 \(\max(n,m)\) 的多项式。

假设 \(C(x) = A(x) B(x)\),则

\[\begin{aligned} C(x) &= (\sum_{i=0}^n a_i x^i) (\sum_{j=0}^m b_j x^j) \\ &= \sum_{i=0}^n \sum_{j=0}^m a_i b_j x^{i+j} \\ &= \sum_{i=0}^{n+m}\sum_{j=0}^i a_j b_{i-j} x^i \end{aligned}\]

因为 \(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)\) 的系数按奇偶分组,令

\[A^{|0|}(x) = a_0 + a_2 x + a_4 x^2 + \cdots a_{n-2} x^{\frac n 2 - 1} \]

\[A^{|1|}(x) = a_1 + a_3 x + a_5 x^2 + \cdots + a_{n-1}x^{\frac n 2 - 1} \]

所以

\[A(x) = A^{|0|}(x^2) + x A^{|1|}(x^2) \]

根据折半引理,\((\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}\),则

\[a_k = \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^{-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\}\) 置换前的二进制表示:

\[\{(000)_2, (001)_2, (010)_2, (011)_2, (100)_2, (101)_2, (110)_2, (111)_2\} \]

置换后 \(\{0, 4, 2, 6, 1, 5, 3, 7\}\) 的二进制表示:

\[\{(000)_2, (100)_2, (010)_2, (110)_2, (001)_2, (101)_2, (011)_2, (111)_2\} \]

不难发现,置换本质上是将 \(\{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] 青蕈领主

posted @ 2026-06-02 16:53  xubaichuan  阅读(14)  评论(0)    收藏  举报