多项式乘法(FFT)学习笔记
Reference:
一、什么是 FFT
快速傅里叶变换(Fast Fourier Transform)是一种用于在
而往往在把系数表示法转化为了点值表示法后,我们可以更方便地解决一些问题,比如说多项式乘法。
二、思想
以 P3803 【模板】多项式乘法(FFT) 为例。
1. 多项式相乘
其实很简单,这里给出一个形式化定义。
设
2. 点值表示法
如果要在
为了优化复杂度,前人们引入了多项式的一个新的表示法——点值表示法。具体地,每一个多项式可以看成一个函数,那么每一个取值
引入点值表示法对于复杂度的优化有什么益处呢?如果现在已经有了两个多项式的点值表示法,且每个点取的
但是由于直接计算点值表示法的复杂度仍旧为
3. 一点点前置数学知识
-
三角函数
-
向量
-
复数
(前两者由于笔者在攥稿时已经在数学课上学习过了,暂且略去。)
-
虚数单位:定义常数
满足 ,称为虚数单位。 -
复数:定义形如
的数为复数。 -
实数和虚数:当且仅当
时,它是实数;当且仅当 时,它是虚数;当且仅当 且 时,它是纯虚数。
- 复平面:我们已知实数与数轴上的数一一对应。由于一个复数可以唯一地写作
,所以每一个复数都可以对应平面上的一个向量—— 。称这个平面为复平面。 轴是实数轴,即实轴; 轴为纯虚数轴,即虚轴。
-
模(绝对值):定义复数所对应向量的模,为该复数的模(又称绝对值)。形式化地,可表示为
。 -
幅角:以
轴正半轴为始边,复数对应的向量所在的射线为终边,形成的角。 -
三角表示法:一个复数还可以用模长 + 幅角的形式表示。设模长为
,幅角为 ,则它的三角表示法为: 。 -
复数的加、减:和向量的加减法则是一样的。
-
复数的乘法:
-
代数上来说,设
,则有: -
几何上来说,它是模长相乘、幅角相加。设复数
,则有:
-
-
单位根:定义满足
的 为 次单位根。注意,同次的单位根不止一个。这里姑且将所有 次单位根的集合记作 。 -
本原单位根:定义
,满足 (说人话,就是它的非负整数次幂可以生成所有的别的 次单位根),为 次本原单位根。记作 。 -
本原单位根的几何意义:在复平面上作一个单位圆。从
轴开始,把单位圆 等分,圆上分出来的点中幅角最小的那一个就是 。其余的点,则都可以写作 ( 为 的整数),这一结论由于复数乘法的几何意义是显然的。
(引自 @FlashHu 的博客)
的值:根据复数乘法的几何意义,明显可以得到:
【注意下面两条性质,它们是快速傅里叶变换的关键。】
-
性质一:
证明:
-
性质二:
证明:
4. 快速傅里叶变换(FFT)
简单概括一下 FFT 的思想:将所有的
【注意:以下推导默认
先设当前有一个多项式:
对它的下标进行奇偶性分类:
给右边的式子提个公因数,将左右化得更为相似:
设:
则:
这样,每次计算
看上去还是没用?让我们尝试随便代入一个值
再代入一个值
可以发现,前一个式子和后一个式子几乎是一样的。因此,当我们用递归计算
而在递归分别计算
当然,运用上述方法的必要条件是项数为偶数。为了保证每一层递归时项数都是偶数,我们要将项数设置为一个大于等于原项数的
一直递归,层数最多为
5. 快速傅里叶逆变换(IFFT)
快速傅里叶变换让我们成功地将系数表示法转化为了点值表示法。但是在我们运用点值表示法快速地得到了新多项式时,还需要把点值表示法转化回系数表示法。这时就需要用到快速傅里叶逆变换。
快速傅里叶逆变换的思路非常奇怪。它的思路是:将当前得到的点值表示,当作一个多项式的系数,再对这个新多项式做一遍快速傅里叶变换求点值表示。可以证明,这个二次快速傅里叶变换所得到的的结果和原本的系数表示法之间存在某种关系。
这里为了帮助我自己理解,手推(抄)了一遍 dalao@自为风月马前卒 给出的快速傅里叶逆变换的证明。
设有一向量
然后开始推导。
观察上式中第二个
而当
因此有:
即:
在学了 skc 的网课后,发现这还有另外一种从矩阵角度的理解方法:
三、实现
1. 递归版
我们来浅浅总结一下整体的流程:
-
先求出一个
,满足为第一个大于 的 的正整数次幂,令它为新项数。并为两个多项式补上一些系数为 的项。 -
对多项式
分别跑一次 FFT,得到二者的点值表示法。 -
将二者的点值表示法逐个相乘,得到二者卷积的点值表示法。
-
对卷积的点值表示法跑一遍 IFFT,得到它的系数表示法。
FFT 的流程:
-
当前递归到的是一个项数为
的多项式。 -
将该多项式的系数按照奇偶性拆为两个多项式,分别递归计算值。
-
通过计算到的两个多项式的答案,合并得到当前的答案。
IFFT 的流程:
-
将原本的单位根
改为 ,对点值表示法计算二次 FFT。一般直接使用性质 来计算 。 -
将二次 FFT 后得到的值全都除以
,即得到系数表示法。
直接使用递归法由于 FFT 的常数过大,在洛谷上无法通过。
2. 递推版
回忆一下上文的递归式:
如果按照递归写法,它需要开很多数组
观察下图:
可以发现两条巧妙的性质:
-
对于当前规模为
的递归层, 和 两个位置在下一层递归中恰好对应 和 。 -
最后结束时的序列为原序列的二进制翻转。
性质 1. 使得我们每一次从下往上递推时只需要调用下面的
性质 2. 使得我们可以轻松地得到递推的起始状态。
3. c++ STL complex 类
实现的时候我们可以选择自己封装一个复数类,也可以选择使用 c++ STL 自带的 complex 类。
- 定义
基本格式为 complex<T> x;
,T
为一种浮点数类型。
初始化变量的值有很多种方式。下面列举了几种:
complex<double> a(3, 4);
complex<double> b = 3.0 + 4i;
complex<double> c = {3, 4};
complex<double> d(3);//只初始化实部
- 实部和虚部
使用 real(x), imag(x)
可以分别取出 x
的实部和虚部,但无法赋值。
使用 x.real(), x.imag()
也可以分别取出 x
的实部和虚部。并且,可以通过 x.real(a), x.imag(b)
的方式分别将 x
的实部和虚部赋值为 a
和 b
。
- 运算
complex 类内置了复数加减乘除,以及与实数的加减乘除运算。我们只需要用运算符号正常运算即可。
- 输出
complex 类内置了输入输出流。直接写 cout<<x<<endl;
,会输出形如 (real,imag)
的结果。
4. code
点击查看代码
#include<bits/stdc++.h>
#define cp complex<double>
using namespace std;
const int MAXN = (1<<21)+5;
const double Pi = acos(-1);//为确保精度,需要写成这样。
int n, m, N = 1, rev[MAXN];
cp f[MAXN], g[MAXN], h[MAXN];
inline void Prework(){//预处理二进制翻转。
while(N <= n+m) N <<= 1;
for(int i = log2(N)-1, t = 0; i >= 0; i--){
int x = t;
for(int j = 0; j <= x; j++) rev[++t] = (1<<i)|rev[j];
}
return;
}
inline void DFT(cp a[], int v){//v 表示当前是 w_n^k 还是 w_n^{-k}
for(int i = 0; i < N; i++) if(rev[i] < i) swap(a[rev[i]], a[i]);
for(int i = 2; i <= N; i <<= 1)//枚举当前层中,每一组的大小 i
for(int k = 0; k < i/2; k++){//枚举组内编号 k
//从逻辑上应该先枚举 j 再枚举 k,但是我为了只计算一次不同的 w_n^k,就调换了一下顺序。
cp w(cos(2*Pi/i*k), sin(2*Pi/i*k*v));
for(int j = 0; j < N; j += i){//枚举每个组的开头下标 j
cp x = a[j+k], y = a[i/2+j+k];
a[j+k] = x+w*y;
a[i/2+j+k] = x-w*y;
}
}
return;
}
inline void IDFT(cp a[]){
DFT(a, -1);
for(int i = 0; i < N; i++) a[i] /= N;
return;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i <= n; i++){
int x; scanf("%d", &x);
f[i].real(x);
}
for(int i = 0; i <= m; i++){
int x; scanf("%d", &x);
g[i].real(x);
}
Prework();
DFT(f, 1), DFT(g, 1);
for(int i = 0; i < N; i++) h[i] = f[i]*g[i];
IDFT(h);
for(int i = 0; i <= n+m; i++) printf("%d ", int(round(real(h[i]))));
return 0;
}
四、NTT
NTT 和 FTT 不同的地方在于,它被用于解决模意义下的多项式乘法。
其实本质上的思路和 FFT 只能说是一模一样。只不过 NTT 中的“单位根”,是模意义下的单位根而已。
如何求出模意义下的单位根呢?首先需要看一下 原根与阶。设在该模数
NTT 的题目一般用
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = (1<<21)+5;
const ll Mod = 998244353, Rt = 3;
int n, m, N = 1, rev[MAXN];
ll f[MAXN], g[MAXN], h[MAXN];
inline ll Quick_Power(ll x, ll p){
if(!p) return 1;
ll tmp = Quick_Power(x, p>>1);
if(p&1) return tmp*tmp%Mod*x%Mod;
else return tmp*tmp%Mod;
}
inline ll Inv(ll x){
return Quick_Power(x, Mod-2);
}
inline void Prework(){
while(N <= n+m) N <<= 1;
for(int i = log2(N)-1, t = 0; i >= 0; i--){
int x = t;
for(int j = 0; j <= x; j++) rev[++t] = (1<<i)|rev[j];
}
return;
}
inline void DFT(ll a[], ll v){
for(int i = 0; i < N; i++) if(rev[i] < i) swap(a[rev[i]], a[i]);
for(int i = 2; i <= N; i <<= 1)
for(int k = 0; k < i/2; k++){
ll w = Quick_Power(Rt, (Mod-1)/i*(i+k*v));
for(int j = 0; j < N; j += i){
ll x = a[j+k], y = a[i/2+j+k];
a[j+k] = (x+w*y)%Mod;
a[i/2+j+k] = (x-w*y%Mod+Mod)%Mod;
}
}
return;
}
inline void IDFT(ll a[]){
DFT(a, -1);
ll inv = Inv(N);
for(int i = 0; i < N; i++) (a[i] *= inv) %= Mod;
return;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i <= n; i++) scanf("%lld", f+i);
for(int i = 0; i <= m; i++) scanf("%lld", g+i);
Prework();
DFT(f, 1), DFT(g, 1);
for(int i = 0; i < N; i++) h[i] = f[i]*g[i]%Mod;
IDFT(h);
for(int i = 0; i <= n+m; i++) printf("%lld ", h[i]);
return 0;
}
五、应用
【FBI Warning:以下的应用全都在模
1. 多项式求逆
参照 @Great_Influence 的题解,写得超级清楚。
【关于他的推导过程,这里留一点备注:为什么平方以后模数就可以由
这个方法简言之,就是构造了一个
简记一下结论:
关于实现,需要注意的是:这种结果对于
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = (1<<19)+5;
const ll Mod = 998244353, Rt = 3;
int n, Rev[MAXN];
ll W, w[MAXN];
inline ll Quick_Power(ll x, int p){
if(!p) return 1;
ll tmp = Quick_Power(x, p>>1);
if(p&1) return tmp*tmp%Mod*x%Mod;
else return tmp*tmp%Mod;
}
inline ll Inv(ll x){
return Quick_Power(x, Mod-2);
}
struct Polynomial{
int len;
ll a[MAXN];
inline void Input(){
for(int i = 0; i < n; i++) scanf("%lld", a+i);
len = 1;
while(len < n) len <<= 1;
return;
}
inline void Output(){
for(int i = 0; i < n; i++) printf("%lld ", a[i]);
return;
}
inline void Module(int N){
for(int i = N; i < len; i++) a[i] = 0;
len = N;
return;
}
inline void DFT(int N){
w[0] = 1;
for(int i = 1; i < N; i++) w[i] = w[i-1]*W%Mod;
for(int i = 0; i < N; i++) if(Rev[i] > i) swap(a[i], a[Rev[i]]);
for(int i = 2; i <= N; i <<= 1)
for(int j = 0; j < N; j += i)
for(int k = 0; k < i/2; k++){
ll x = a[j+k], y = a[i/2+j+k];
a[j+k] = (x+y*w[N/i*k]%Mod)%Mod;
a[i/2+j+k] = (x-y*w[N/i*k]%Mod+Mod)%Mod;
}
return;
}
inline void IDFT(int N){
W = Inv(W);
DFT(N);
ll inv = Inv(N);
for(int i = 0; i < N; i++) a[i] = a[i]*inv%Mod;
return;
}
Polynomial operator * (const int &tmp)const{
Polynomial ans; ans.len = len;
for(int i = 0; i < len; i++) ans.a[i] = a[i]*tmp%Mod;
return ans;
}
Polynomial operator - (const Polynomial &tmp)const{
Polynomial ans; ans.len = len;
for(int i = 0; i < len; i++) ans.a[i] = (a[i]-tmp.a[i]+Mod)%Mod;
return ans;
}
} f;
inline void Prework(int N){
W = Quick_Power(Rt, (Mod-1)/N);
for(int i = N>>1, j = 0; i; i >>= 1){
int t = j;
for(int k = 0; k <= t; k++) Rev[++j] = Rev[k]+i;
}
return;
}
Polynomial operator * (const Polynomial &x, const Polynomial &y){
int N = y.len<<1;//乘法必须算到 N*2 项
Prework(N);
Polynomial X = x, Y = y, ans;
X.Module(N>>1), Y.Module(N>>1);//对多项式项数取模
X.DFT(N), Y.DFT(N);
for(int i = 0; i < N; i++) ans.a[i] = X.a[i]*Y.a[i]%Mod;
ans.IDFT(N);
ans.Module(N>>1);
return ans;
}
inline Polynomial Inv_P(Polynomial &A){
Polynomial B = (Polynomial){1, {Inv(A.a[0])}};
for(int N = 2; N < n*2; N <<= 1){//N 为当前取模的长度
B.len = N;
B = B*2-A*B*B;
}
return B;
}
int main(){
scanf("%d", &n);
f.Input();
Polynomial ans = Inv_P(f);
ans.Output();
return 0;
}
2. 多项式 ln
对
先求逆再乘法即可。
3. 牛顿法
这个的前置知识:微积分学习笔记。
其实前面多项式求逆的方法本质运用的是一个方法——“牛顿法”。具体可以参考 @Miskcoo 的博客。
在这里就简述一下结论。
在已知
令
则通过将
注意这里
以多项式求逆举例。定义
则有:
这里的多项式
4. 多项式 exp
运用牛顿法推导一下就可以得到:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下