Note -「多项式」基础模板(FFT/NTT/多模 NTT)光速入门
进阶篇戳这里。
何为「多项式」
多项式(polynomial)是指由变量(variable)、系数(coefficient)以及它们之间的加、减、乘、幂运算(非负整数次方)得到的表达式。多项式就是整式。
基本概念
在初中阶段就有所接触:整式可以包含加、减、乘、除、乘方五种对变量的运算,同时要求除数不含有变量。设 为变量,其余字母为常量,则 是整式, 也是整式,因为根号下的 是常量; 则不是整式。
在本文中,我们所说的多项式通常指一元多项式,即仅含一个变量 的多项式。并简记一个多项式 ,其中 是 的次数。
系数表示法 & 点值表示法
在上文中 就是多项式的系数表示法。而可以证明,对于一个 次多项式,可以用任意 个其函数图像上不重合的点来唯一确定这个多项式。我们任取 个横坐标的值 ,代入 ,求得 个点 ,就可以用这 个点唯一确定 ,这也被称为 的点值表示法。注意到 是任意的,所以同一个多项式的点值表示法不唯一。
接下来,抛出我们的问题:设有两个多项式 ,如何计算它们的乘积(亦称作卷积) 呢?
一个简单的想法,我们可以利用乘法分配律求到 每一项的系数。于是:
注意求和中的 只是一种形式记法,事实上,当 大于 的次数与 的次数之和时, 的系数 显然为 。
不过呢……从算法设计的角度,这种求多项式乘法的方式的复杂度是 的,好慢 qwq。
联系到点值表示法,如果对于同一个横坐标 ,其在 上的坐标为 ,在 上的坐标为 ,那么代入最初的表达式,它在 上的坐标就是 呐。所以,在已知 和 的点值表示法时,我们可以用 的时间求出 的点值表示法!
傅里叶(Fourier)变换
我们继续解决上文多项式乘法的问题——找到一个高效的算法,实现系数与点值表示法的转换。
本节中,若非特别说明,。
概述
离散傅里叶变换(Discrete Fourier Transform,DFT),是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。
FFT 是一种高效实现 DFT 的算法,称为快速傅立叶变换(Fast Fourier Transform,FFT)。它对傅里叶变换的理论并没有新的发现,但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步。
前置知识 - 复数
其实很简单:我们把形如 的数称为复数,其中 ; 为实部, 为虚部, 有 ,为虚数单位。我们可以把复数与二维平面上的向量 一一对应,此时 轴称为实轴, 轴称为虚轴。并记其与 轴正方向的夹角为辐角 ,向量的模长为 。如图,对于 :
其中 为模长, 为辐角。接下来我们研究复数的一些基本运算:
-
加减法:。
-
乘法:。
进一步研究,发现:
最后一步用了简单的因式分解技巧。由于 ,所以有 。
此外,运用 的和角公式,还可以求到 。
所以,复数的乘法有一个重要的几何意义:模长相乘,辐角相加。
最后,我们定义 为 关于 轴对称的向量,称为 的共轭复数。即若 ,有 。可以发现 。 为后文方便叙述,记 。
单位根
定义 次单位根为所有满足 的 。根据乘法的几何意义,可知 ,,其中 。于是满足条件且小于 的 集合为 ,共 个。我们记 (,/'əʊmɪɡə/)为第 个 次单位根()。如图,当 时:
运用简单的三角函数知识,得到:
可以发现,单位根实际上体现了一个周期意义:。所以此后单位根的上标(乘方或是编号,它们在运算意义上等价)默认对单位根次数取模。单位根有如下性质:
- 。
- 。
- 。
可以结合乘法几何意义与公式理解。
快速傅里叶正变换(FFT)
有必要重申我们的目标:实现系数与点值表示法的转换。
现在,设有 ,我们希望代入 次单位根 这 个复数来取得 的点值表示法。
分开奇数项与偶数项,记:
利用 和 来表示 ,有:
设 ,尝试代入一个 :
注意到 ,所以:
类似地,代入 ,有:
所以,当已知 和 时,可以 计算出 和 。现在,就有了一个算法雏形:
Function name: FFT.
Input: a polynomial which's length is .
Output: a set .
if then
return
while do
return
复杂度是 。在后文中,“多项式 的点值表示”均指由上述 FFT 算法得到的 。
别着急做题,这种实现方法 SPFA 了 qwq。
快速傅里叶逆变换(IFFT)
利用 FFT,我们可以快速地将系数表示法转换成点值表示法,那怎么在同样优秀的复杂度内转回去呢?
从本质入手,考虑到 ,我们把它写成矩阵乘法的形式:
令左侧的系数矩阵为 ,考虑系数矩阵 :
那么:
当 时,显然 。
当 时,发现是等比数列的形式。所以 。又因为 ,所以分子有 ,则原式 。
综上,,即 。
以此,就有:
注意到 ,所以依然能够用 FFT 的递归形式求解。
迭代实现
常数惊人呐……
这里,我们将尝试把递归的 FFT Fast-Fast TLE 转换为仅用循环迭代的 FFT。
观察递归时各系数的位置:
观察第一层和最后一层:
原序列 dec | ||||||||
---|---|---|---|---|---|---|---|---|
原序列 bin | ||||||||
新序列 dec | ||||||||
新序列 bin |
发现原序列下标的二进制翻转过来就是新序列下标。
所以我们可以先安排好新序列下标,再从下往上迭代就可以了。
例题
「洛谷 P3803」「模板」多项式乘法(FFT)
题意简述
link.
给两个多项式的每项系数,求其相乘得到的多项式的每项系数。
数据规模
多项式长度 。
直接给出完整代码,建议先模仿实现,以免常数惊人 owo。
不过我不知道怎么折叠显示代码,影响阅读很抱歉 qwq。
#include <cmath>
#include <cstdio>
#include <iostream>
inline int rint () {
int x = 0, f = 1; char s = getchar ();
for ( ; s < '0' || '9' < s; s = getchar () ) f = s == '-' ? -f : f;
for ( ; '0' <= s && s <= '9'; s = getchar () ) x = x * 10 + ( s ^ '0' );
return x * f;
}
template<typename Tp>
inline void wint ( Tp x ) {
if ( x < 0 ) putchar ( '-' ), x = -x;
if ( 9 < x ) wint ( x / 10 );
putchar ( x % 10 ^ '0' );
}
const int MAXL = 1 << 21;
const double PI = acos ( -1 );
int n, m, rev[MAXL + 5];
struct Complex { // 复数结构体。
double x, y;
Complex () {} Complex ( const double tx, const double ty ): x ( tx ), y ( ty ) {}
inline Complex operator + ( const Complex t ) const { return Complex ( x + t.x, y + t.y ); }
inline Complex operator - ( const Complex t ) const { return Complex ( x - t.x, y - t.y ); }
inline Complex operator * ( const Complex t ) const { return Complex ( x * t.x - y * t.y, x * t.y + y * t.x ); }
inline Complex operator / ( const double t ) const { return Complex ( x / t, y / t ); }
} A[MAXL + 5], B[MAXL + 5];
inline void FFT ( const int n, Complex* A, const int tp ) {
// n为多项式长度,保证是2的整数幂;A为每项系数;tp=1表示正变换,tp=-1表示逆变换。
for ( int i = 0; i < n; ++ i ) if ( i < rev[i] ) std :: swap ( A[i], A[rev[i]] ); // 安排系数位置。
for ( int i = 2, stp = 1; i <= n; i <<= 1, stp <<= 1 ) {
Complex w ( cos ( PI / stp ), tp * sin ( PI / stp ) ); // 当前正在处理$\omega_i$。
for ( int j = 0; j < n; j += i ) {
Complex r ( 1, 0 );
for ( int k = 0; k < stp; ++ k, r = r * w ) {
// $r=\omega_i^k$。
Complex ev ( A[j + k] ), ov ( r * A[j + k + stp] );
A[j + k] = ev + ov, A[j + k + stp] = ev - ov;
}
}
}
if ( ! ~ tp ) for ( int i = 0; i < n; ++ i ) A[i] = A[i] / n; // 逆变换,最后每项/n。
}
inline void ployConv ( const int n, const int m, const Complex* A, const Complex* B, Complex* C ) {
// 计算A(x)与B(x)的卷积,答案为C(x)。
int lg = 0, len = 1;
static Complex tmpA[MAXL + 5], tmpB[MAXL + 5];
for ( ; len < n + m - 1; len <<= 1, ++ lg ); // C(x)长度为n+m-1,找到适合的长度len。
for ( int i = 0; i < len; ++ i ) {
tmpA[i] = A[i], tmpB[i] = B[i];
rev[i] = ( rev[i >> 1] >> 1 ) | ( ( i & 1 ) << lg >> 1 ); // rev[i]为i翻转后的位置,自行理解。
}
FFT ( len, tmpA, 1 ), FFT ( len, tmpB, 1 ); // 正变换求到点值表示。
for ( int i = 0; i < len; ++ i ) C[i] = tmpA[i] * tmpB[i]; // 求到C(x)的点值表示。
FFT ( len, C, -1 ); // 逆变换求到C(x)。
}
int main () {
n = rint () + 1, m = rint () + 1; // 注意这里要+1。
for ( int i = 0; i < n; ++ i ) A[i].x = rint ();
for ( int i = 0; i < m; ++ i ) B[i].x = rint ();
ployConv ( n, m, A, B, A );
for ( int i = 0; i < n + m - 1; ++ i ) {
wint ( ( int ) round ( A[i].x ) );
putchar ( i == n + m - 2 ? '\n' : ' ' );
}
return 0;
}
快速数论变换(NTT)
事实上,由于 FFT 存在大量浮点数运算,其精度和常数都不尽人意。那么能否在整数域下找到单位根的替代品呢?
我们来总结一下单位根应具有的性质:
- ,注意这里的上标就意义上来说不是乘方,而是编号(当然运算上就是乘方 www)。
- ,我们需要这个性质进行递归处理。
- ,这一性质体现在了合并计算的步骤。
- ,它保证了点值表示法的坐标不重合。
- ,它保证了逆变换顺利进行。
考虑在模意义下……
原根
OI-Wiki 讲得好高深 qwq……
对于素数 ,定义其原根 为满足 的数。模仿 的定义,我们令 (这里 是随便取的名字 www),考虑其是否满足上述几条性质:
- ,乘方性质,显然。
- ,成立。
- 。注意因为 且根据 的定义, ,所以 ,等式成立。
- 由定义,成立。
- 显然 ,成立。
于是,把 FFT 中的所有 替换为 就变成 NTT 啦!记得取模 owo。
实现
inline void NTT ( const int n, int* A, const int tp ) {
for ( int i = 0; i < n; ++ i ) if ( i < rev[i] ) A[i] ^= A[rev[i]] ^= A[i] ^= A[rev[i]];
for ( int i = 2, stp = 1, w; i <= n; i <<= 1, stp <<= 1 ) {
w = qkpow ( G, ( MOD - 1 ) / i );
// G为原根,qkpow(a,b)为a的b次方模MOD的结果。
if ( ! ~ tp ) w = qkpow ( w, MOD - 2 );
for ( int j = 0; j < n; j += i ) {
for ( int r = 1, k = j; k < j + stp; ++ k, r = 1ll * r * w % MOD ) {
int ev = A[k], ov = 1ll * r * A[k + stp] % MOD;
A[k] = ( ev + ov ) % MOD, A[k + stp] = ( ev - ov + MOD ) % MOD;
}
}
}
if ( ! ~ tp ) {
int invn = qkpow ( n, MOD - 2 ); // invn为n的逆元。
for ( int i = 0; i < n; ++ i ) A[i] = 1ll * A[i] * invn % MOD;
}
}
NTT 模数
注意到, 的大小是随多项式长度变化的,而我们必须保证 ,所以 所含 的因子个数决定了 NTT 能够接受的最大多项式长度。对于素数 的选取自然也有了考究。
记住一点,,适合做 NTT 模数,原根为 ;,不适合做 NTT 模数。
更多 NTT 模数可见这篇博客。
到此,多项式最最基础的知识就结束啦。
奇怪的模数 - 任意模数 NTT
你 NTT 对模数有要求对吧?出题人就笑了……/xyx
好吧,在不接受 FFT 精度误差的情况下,我们来想想办法应对模数不是 NTT 模数,甚至不是素数的毒瘤题吧!
三模 NTT
我偏要用 NTT 模数 www。
顾名思义,我们任取三个 NTT 模数 ,分别求出多项式乘积的第 项模 的结果,然后用中国剩余定理(CRT)解出该项模题目给定模数 的结果就行了。不熟悉 CRT 的读者可以参考这篇博客。
我个人认为不太优美懒,因此没有具体实现。在抱歉之余推荐这篇博客的模板 owo。
拆系数 FFT(MTT)
我偏要用 FFT。
缩写释义:MTT - MaoXiao Theory(?) Transformation。快速毛论变换(雾。
假设多项式 ,长度均为 ,给定的模数 ,我们要求它们的卷积 。如果直接 FFT,最大的系数可达到 的级别。我们假定 ,那么这样的精度误差是不被接受的。我们取 ,并定义以下几个多项式:
可以发现,以上每个多项式的系数都是不超过 的,那么任意两个多项式卷积的最大系数不超过 ,在 double
的承受范围以内。(保险起见,建议开 long double
,不过对于某些 g++
版本貌似并没有作用 owo。)我们用这四个多项式表示 :
这样,在 FFT 过程中,系数大小能被接受;作系数拼凑时步步取模亦能保证不溢出,我们就用 FFT 解决了任意模数 NTT 的问题。一般来说,取 ,可以用位运算节省常数;还应利用 的通项预处理出所有单位根以保障精度。
七次转五次
数一数,我们对 分别进行了一次 FFT,对 分别进行了一次 IFFT,一共有七次变换操作,常数有点点大 qwq。
接下来,我们假设有实系数多项式 ,长度均为 ,我们希望分别求到它们的点值表示 和 。于是定义:
其中 是虚数单位。设 与 的点值表示分别为 ,并记 的辐角 为 。那么:
推导时,时刻留意 的上标在模 意义下嗷!
最终,发现 可以在求得 的前提下 求到。只需要用 和 表示出 和 :
就可以用一次 FFT 求出两个多项式的点值表示啦!(我认为毛啸的论文在 的表达式处有误,右侧分子应为本文的 而非 ,欢迎指正 qwq。)
于是,我们可以把对 的 FFT 合并为两组,就只需要五次 FFT 啦!
五次转四次
正变换已经足够优秀了,我们接下来考虑逆变换的合并。按照上文的推导,我们只需要求到 或者 就可以了——因为原多项式是实系数的,所以对于 或 的每一项,我们能够区分出其实部就是 的系数而虚部就是 的系数。既然已知 和 ,直接利用定义式,有:
逆变换 得到 ,就可以把两次逆变换合并为一次了。最终,我们把四次正变换合并为两次,三次逆变换合并为两次,仅用四次 FFT 就解决了这一问题。
例题
「洛谷 P4245」「模板」任意模数 NTT
题意简述
link.
给两个多项式,求其在模 意义下的卷积。
数据规模
多项式长度 ,系数 ,。
这里仅给出四次 FFT 的 MTT 做法。
#include <cmath>
#include <cstdio>
#include <iostream>
typedef long long LL;
inline int rint () { /* 快读 */ }
inline void wint ( const int x ) { /* 快输 */ }
const int MAXN = 1 << 18;
const double PI = acos ( -1 );
int n, m, p, A[MAXN + 5], B[MAXN + 5], rev[MAXN + 5];
struct Complex {
/* 复数结构体 */
} omega[MAXN + 5], P[MAXN + 5], Q[MAXN + 5], C[MAXN + 5], D[MAXN + 5], E[MAXN + 5], F[MAXN + 5];
inline void FFT ( const int n, Complex* A, const int tp ) { /* FFT,应使用预处理好的单位根omega[]。 */ }
int main () {
n = rint () + 1, m = rint () + 1, p = rint ();
for ( int i = 0; i < n; ++ i ) A[i] = rint () % p, P[i] = Complex ( A[i] & 0x7fff, A[i] >> 15 );
for ( int i = 0; i < m; ++ i ) B[i] = rint () % p, Q[i] = Complex ( B[i] & 0x7fff, B[i] >> 15 );
int lg = 0, len = 1;
for ( ; len < n + m - 1; len <<= 1, ++ lg );
for ( int i = 0; i < len; ++ i ) rev[i] = ( rev[i >> 1] >> 1 ) | ( ( i & 1 ) << lg >> 1 );
for ( int i = 1; i < len; i <<= 1 ) { // 预处理单位根。
for ( int k = 0; k < i; ++ k ) {
omega[len / i * k] = Complex ( cos ( PI * k / i ), sin ( PI * k / i ) );
}
}
FFT ( len, P, 1 ), FFT ( len, Q, 1 );
for ( int i = 0; i < len; ++ i ) {
Complex t ( P[( len - i ) % len].x, -P[( len - i ) % len].y );
C[i] = ( P[i] + t ) / 2, D[i] = Complex ( 0, 1 ) * ( t - P[i] ) / 2;
}
for ( int i = 0; i < len; ++ i ) {
Complex t ( Q[( len - i ) % len].x, -Q[( len - i ) % len].y );
E[i] = ( Q[i] + t ) / 2, F[i] = Complex ( 0, 1 ) * ( t - Q[i] ) / 2;
}
for ( int i = 0; i < len; ++ i ) {
Complex c ( C[i] ), d ( D[i] ), e ( E[i] ), f ( F[i] );
C[i] = c * e, D[i] = c * f + d * e, E[i] = d * f;
P[i] = C[i] + Complex ( 0, 1 ) * E[i];
}
FFT ( len, D, -1 ), FFT ( len, P, -1 );
for ( int i = 0; i < n + m - 1; ++ i ) {
int c = ( LL ( P[i].x + 0.5 ) % p + p ) % p, d = ( LL ( D[i].x + 0.5 ) % p + p ) % p, e = ( LL ( P[i].y + 0.5 ) % p + p ) % p;
wint ( ( c + ( 1ll << 15 ) % p * d % p + ( 1ll << 30 ) % p * e % p ) % p );
putchar ( i + 1 == n + m - 1 ? '\n' : ' ' );
}
return 0;
}
「CF 623E」Transforming Sequence
题意简述
link.
求有多少个长度为 的序列 ,满足 。这里的集合运算把 作为了二进制集合。对 取模。
数据规模
。
详见我的题解。题意描述略有不同,请自行理解。为什么我的 MTT 有八次变换啊……
专门提起这道题的目的,除了让大家实战运用 MTT 之外,还想让大家感受倍增这种几乎能融入所有 OI 知识点的思想。
本来还有多项式全家桶,但因为太懒所以烂尾咯 qwq。
参考资料
按在文中第一次借鉴或引用的位置顺序排列:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现