FFT求卷积(多项式乘法)

卷积

如果有两个无限序列a和b,那么它们卷积的结果是:\(y_n=\sum_{i=-\infty}^\infty a_ib_{n-i}\)。如果a和b是有限序列,a最低的项为a0,最高的项为an,b同理,我们可以把a和b超出范围的项都设置成0。那么可以得出:y0=a0b0,y1=a1b0+a0b1,y2=a0b2+a1b1+a2b0……,y(n+m)=a(n)b(m)。

构造两个多项式A(x)和B(x):

\(A=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}+a_nx^n\),

\(B=b_0+b_1x+b_2x^2+...+b_{m-1}x^{m-1}+b_mx^m\)。

那么\(A(x)*B(x)=C(x)=a_0b_0+(a_0b_1+a_1b_0)x+...+a_nb_mx^{n+m}\),把系数提取出来,可以发现两序列卷积可以转换为用序列作系数进行多项式乘法。

多项式

一个多项式既可以用系数表示,也可以用点值表示。n个点可以表示一个n-1次多项式。

如果用系数表示法来多项式乘法,时间复杂度是\(O(n^2)\)的,而用点值表示法只需要\(O(n)\)的时间。然而我们需要的是系数表示法。所以我们需要找到一个优秀的算法将它们两者转换,这就是(我们眼中的)FFT。

复数

设\(i^2=-1\),a,b为实数,形如\(a+bi\)的数叫做复数。

用x轴表示a的大小,y轴表示b的大小,构造出的平面直角坐标系叫做复平面。复数的模长是原点到\((a, b)\)的距离,即\(\sqrt{a^2+b^2}\)。复数的辐角即为以逆时针为正方向,从x轴正半轴到已知向量的转角。

复数的加减法则是显然的,可以看作向量的加减。

复数可以写成\(N(cos\alpha+isin\alpha )\),\(\alpha\)表示复数的辐角。设\(z_1=A(cos\alpha + isin\alpha)\),\(z_2=B(cos\beta + isin\beta)\),那么\(z_1z_2=AB[(cos\alpha cos\beta-sin\alpha sin\beta)+i(sin\alpha cos\beta+cos\alpha sin\beta)]=AB[cos(\alpha+\beta)+isin(\alpha+\beta)]\)。也就是说,两复数相乘,模长相乘,辐角相加。如果写成普通形式的话,就是\((a+bi)(c+di)=(ac-bd)+(bc+ad)i\)。

单位根

在复平面上,以原点为圆心,1为半径作圆,所得得圆为单位圆。从x轴正半轴开始将圆n等分,联向第一个等分点所代表的复数\(\omega_n\)叫做n次单位根,意思是说\(w_n\)的n次方为1(根据复数的乘法运算法则)。可以推得,其他等分点代表的向量为\(\omega_n^1\),\(\omega_n^2\)……,一直到\(\omega_n^n = \omega_n^0=1\)。显然\(\omega_n^k=cosk*\frac{2\pi}{n}+isink*\frac{2\pi}{n}\)。

单位根有几个性质:

  • 消去引理:\(\omega_{2n}^{k+n}=-\omega_{2n}^k\)。这是最重要的性质,使得分叉个数为2。
  • 折半引理:\({(\omega_n^k)^2}={\omega_{n/2}^k}\)。这保证了FFT中子问题和原问题的规模都是n。
  • 求和引理:\(\sum_{i=0}^{n-1}(\omega_n^k)^i=\left\{ \begin{aligned} 0, n\nmid k \\ n, n\mid k \end{aligned} \right.\)这是用来证明逆变换的。

DFT

前面说过,DFT是要把多项式的系数表达转成点值表达。设多项式A(x)的系数为\((a_o,a_1,a_2,\ldots,a_{n-1})\),那么

\(A(x)=a_0+a_1*x+a_2*{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1}\)

将下标按照奇偶性分类,那么:\(A(x)=(a_0+a_2*{x^2}+a_4*{x^4}+\dots+a_{n-2}*x^{n-2})+(a_1*x+a_3*{x^3}+a_5*{x^5}+ \dots+a_{n-1}*x^{n-1})\)

设:

\(A_1(x)=a_0+a_2*{x}+a_4*{x^2}+\dots+a_{n-2}*x^{\frac{n}{2}-1}\)

\(A_2(x)=a_1+a_3*{x}+a_5*{x^2}+ \dots+a_{n-1}*x^{\frac{n}{2}-1}\)

那么:\(A(x)=A_1(x^2)+xA_2(x^2)\)

根据单位根的性质,将前面一半的值带入可得:

\(A(\omega_n^k)=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})=A_1(\omega_{\frac{n}{2}}^{k})+\omega_n^kA_2(\omega_{\frac{n}{2}}^{k})\)(折半引理的作用:将问题分解成条件完全相同的子问题)

同理带入后面的值:

\(A(\omega_n^{k+\frac{n}{2}})=A_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}(\omega_n^{2k+n})=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k})\)(消去引理:使得分叉个数为2。如果没有这个引理的话,就必须再去算一遍\(A(\omega_n^{k+\frac{n}{2}})\)的值,分叉个数变成4了。)

由于这两个式子只有加号减号不同,我们只需计算前面一半的点值即可。这样就将问题规模缩小了一半。当n=1时,点值是一个常数,直接返回即可。不难看出这是一个分治算法,时间复杂度为\(O(nlogn)\)。

为IDFT作准备

我们发现,FFT其实是在求下图的\(y_i\):(实在打不出来qwq)

那么现在的问题是,已知\(y_i\),如何推回\(a_i\)?

由于我太弱了,继续上图吧。。

(补充一下,只要求出那个范德蒙德行列式的逆矩阵,乘在等式两边,那么就可以通过\(y_i\)推出\(a_i\))

怎么构造\(V^{-1}\),使得\(v_i^Tv_j^{-1}=\left\{ \begin{aligned} 1, i=j \\ 0, i\ne j\end{aligned} \right.\)呢?

是不是很神?这样我们就构造出了\(V^{-1}\):

现在,问题就变成了用\(\omega_n^{-1}\)为本原单位根,对y向量作FFT以后除以n。PPT里说的吼啊,稍微修改一下代码就行了。

递归实现FFT

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std; const int maxn=2e6+5;
const double Pi=3.1415926535898;
int t, n, m, len=1; struct Cpx{ //复数
double x, y;
Cpx (double t1=0, double t2=0){ x=t1, y=t2; }
}A[maxn*2], B[maxn*2], C[maxn*2];
Cpx operator +(Cpx a, Cpx b){ return Cpx(a.x+b.x, a.y+b.y); }
Cpx operator -(Cpx a, Cpx b){ return Cpx(a.x-b.x, a.y-b.y); }
Cpx operator *(Cpx a, Cpx b){ return Cpx(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); } void fdft(Cpx *a, int n, int flag){ //快速将当前多项式从系数表达转换为点值表达
if (n==1) return; //如果只有1项系数为k,唯一的点值就是(w[1,1],k*w[1,1])=(1, k)
Cpx a1[(n>>1)+1], a2[(n>>1)+1];
for (int i=0; i<(n>>1); ++i) a1[i]=a[i<<1], a2[i]=a[i<<1|1];
fdft(a1, n>>1, flag); fdft(a2, n>>1, flag);
Cpx w1(cos(2*Pi/n), flag*sin(2*Pi/n)), w(1, 0); //idft用的负根
for (int i=0; i<(n>>1); ++i, w=w*w1){
a[i]=a1[i]+w*a2[i];
a[i+(n>>1)]=a1[i]-w*a2[i];
}
} int main(){
scanf("%d%d", &n, &m); int x;
for (int i=0; i<=n; ++i) scanf("%lf", &A[i].x);
for (int i=0; i<=m; ++i) scanf("%lf", &B[i].x);
while (len<n+m) len<<=1; //idft需要至少l1+l2个点值
fdft(A, len, 1); fdft(B, len, 1);
for (int i=0; i<len; ++i) C[i]=A[i]*B[i];
fdft(C, len, -1); //idft
for (int i=0; i<=n+m; ++i){
x=C[i].x/len+0.5;
printf("%d ", x);
}
return 0;
}

题目是luogu的模板。注意给出的n和m都是多项式的最高次数,也就是说乘起来后的多项式最高次数为n+m,至少需要n+m个点。

迭代版FFT

递归版的太慢了,暗中观察我们是如何处理序列的,可以发现:

把每个元素的编号二进制反转一下,就是我们要求的序列编号!原因是原序列的最后1位决定了当前元素被分到前半区还是后半区,也就是转换后元素编号的第1位。依次类推。

有一个O(n)推出n个数各自编号镜像反转的方法,大体思想是通过i<<1的反转推出i的反转。

由于各种原因,迭代版要比递归版快四倍左右~

#include <cmath>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std; const int maxn=2e6+5;
const double pi=3.1415926535898;
int t, n, m, len=1, l, r[maxn*2]; struct Cpx{ //复数
double x, y;
Cpx (double t1=0, double t2=0){ x=t1, y=t2; }
}A[maxn*2], B[maxn*2], C[maxn*2];
Cpx operator +(Cpx a, Cpx b){ return Cpx(a.x+b.x, a.y+b.y); }
Cpx operator -(Cpx a, Cpx b){ return Cpx(a.x-b.x, a.y-b.y); }
Cpx operator *(Cpx a, Cpx b){ return Cpx(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); } void fdft(Cpx *a, int n, int flag){ //快速将当前多项式从系数表达转换为点值表达
for (int i=0; i<n; ++i) if (i<r[i]) swap(a[i], a[r[i]]);
for (int mid=1; mid<n; mid<<=1){ //当前区间长度的一半
Cpx w1(cos(pi/mid), flag*sin(pi/mid)), x, y;
for (int j=0; j<n; j+=(mid<<1)){ //j:区间起始点
Cpx w(1, 0);
for (int k=0; k<mid; ++k, w=w*w1){ //系数转点值
x=a[j+k], y=w*a[j+mid+k];
a[j+k]=x+y; a[j+mid+k]=x-y;
}
}
}
} inline int getint(int &x){
char c; int flag=0;
for (c=getchar(); !isdigit(c); c=getchar())
if (c=='-') flag=1;
for (x=c-48; c=getchar(), isdigit(c);)
x=(x<<3)+(x<<1)+c-48;
return flag?x:-x;
} int main(){
getint(n); getint(m); int x;
for (int i=0; i<=n; ++i) getint(x), A[i].x=x;
for (int i=0; i<=m; ++i) getint(x), B[i].x=x;
while (len<=n+m) len<<=1, ++l; //idft需要至少l1+l2个点值
for (int i=0; i<len; ++i) //编号的字节长度为l
r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
fdft(A, len, 1); fdft(B, len, 1);
for (int i=0; i<len; ++i) C[i]=A[i]*B[i];
fdft(C, len, -1); //idft
for (int i=0; i<=n+m; ++i) printf("%d ", int(C[i].x/len+0.5));
return 0;
}

这样可以做到1e6的数据最差也能跑进1s。我太菜了,并不会什么常数优化。

两个月后的PS:注意n个点确定一个n-1次多项式。这是因为,对多项式求点值表达,相当于将一个范德蒙德矩阵乘上系数矩阵(前文有图)。而范德蒙德矩阵是可逆的,所以在已知y的情况下,a也是唯一确定的。因此n个点一定唯一确定一个n-1次多项式。

五个月后的PS:qwq 借用了不少大佬的东西,侵删。

FFT求卷积(多项式乘法)的更多相关文章

  1. [笔记]ACM笔记 - 利用FFT求卷积(求多项式乘法)

    卷积 给定向量:, 向量和: 数量积(内积.点积): 卷积:,其中 例如: 卷积的最典型的应用就是多项式乘法(多项式乘法就是求卷积).以下就用多项式乘法来描述.举例卷积与DFT. 关于多项式 对于多项 ...

  2. FFT模板(多项式乘法)

    FFT模板(多项式乘法) 标签: FFT 扯淡 一晚上都用来捣鼓这个东西了...... 这里贴一位神犇的博客,我认为讲的比较清楚了.(刚好适合我这种复数都没学的) http://blog.csdn.n ...

  3. 【FFT求卷积】Problem D. Duel

    [AC] #include <stdio.h> #include <iostream> #include <string.h> #include <algor ...

  4. CodeForces - 528D Fuzzy Search (FFT求子串匹配)

    题意:求母串中可以匹配模式串的子串的个数,但是每一位i的字符可以左右偏移k个位置. 分析:类似于 UVALive -4671. 用FFT求出每个字符成功匹配的个数.因为字符可以偏移k个单位,先用尺取法 ...

  5. 【Learning】多项式乘法与快速傅里叶变换(FFT)

    简介: FFT主要运用于快速卷积,其中一个例子就是如何将两个多项式相乘,或者高精度乘高精度的操作. 显然暴搞是$O(n^2)$的复杂度,然而FFT可以将其将为$O(n lg n)$. 这看起来十分玄学 ...

  6. 多项式乘法,FFT与NTT

    多项式: 多项式?不会 多项式加法: 同类项系数相加: 多项式乘法: A*B=C $A=a_0x^0+a_1x^1+a_2x^2+...+a_ix^i+...+a_{n-1}x^{n-1}$ $B=b ...

  7. 【总结】对FFT的理解 / 【洛谷 P3803】 【模板】多项式乘法(FFT)

    题目链接 \(\Huge\text{无图,慎入}\) \(FFT\)即快速傅里叶变换,用于加速多项式乘法. 如果暴力做卷积的话就是一个多项式的每个单项式去乘另一个多项式然后加起来,时间复杂度为\(O( ...

  8. 多项式乘法(FFT)模板 && 快速数论变换(NTT)

    具体步骤: 1.补0:在两个多项式最前面补0,得到两个 $2n$ 次多项式,设系数向量分别为 $v_1$ 和 $v_2$. 2.求值:用FFT计算 $f_1 = DFT(v_1)$ 和 $f_2=DF ...

  9. 洛谷 P3803 【模板】多项式乘法(FFT)

    题目链接:P3803 [模板]多项式乘法(FFT) 题意 给定一个 \(n\) 次多项式 \(F(x)\) 和一个 \(m\) 次多项式 \(G(x)\),求 \(F(x)\) 和 \(G(x)\) ...

随机推荐

  1. informix 把数据从一个表倒到另外一个表中

    drop table zrjReinUnClaimTmpT; create table zrjReinUnClaimTmpT ( mainid SERIAL not null, RepayNo var ...

  2. sws_scale函数的用法-具体应用

    移植ffmpeg过程中,遇到swscale的用法问题,所以查到这篇文章.文章虽然已经过去很长时间,但是还有颇多可以借鉴之处.谢谢“咕咕鐘". 转自:http://guguclock.blog ...

  3. LeetCode:Add Digits - 非负整数各位相加

    1.题目名称 Add Digits (非负整数各位相加) 2.题目地址 https://leetcode.com/problems/add-digits/ 3.题目内容 英文:Given a non- ...

  4. Maven 将jar导入本地maven仓库

    目录 环境变量配置maven 执行一下命令即可 诚邀访问我的个人博客:我在马路边 更好的阅读体验点击查看原文:Maven将jar倒入本地maven仓库 原创博客,转载请注明出处 @ 在Java项目开发 ...

  5. tcpdump示例

    今天有需求要用tcpdump,给一个我使用的例子: sudo /usr/sbin/tcpdump  dst 10.20.137.24 and tcp port 8080 -A -s0  -w nous ...

  6. JZOJ 1003【东莞市选2007】拦截导弹——dp

    题目:https://jzoj.net/senior/#main/show/1003 只要倒推一下第一次上升的最长和第一次下降的最长就行了.不用n^2logn,枚举了 j 还要用树状数组找值比自己大的 ...

  7. 第 六 课 GO语言常量

    http://www.runoob.com/go/go-constants.html 一 常量 是一个简单值的标识符,在程序运行时,不会被修改的量. 常量中的数据类型只可以是布尔型.数字型(整数型.浮 ...

  8. Python模块-logging模块(一)

    logging模块用来写日志文件 有5个级别,debug(),info(),warning(),error()和critical(),级别最高的为critical() debug()为调试模式,inf ...

  9. Python模块-requests(一)

    requests不是python自带的,使用前需要安装 发送请求 HTTP请求类型有GET,POST,PUT,DELETE,HEAD和OPTIONS 使用requests发送请求的方法如下: > ...

  10. python 基础 列表生成式

    data = {'a':'abc';'b':'bac','c':'cba'} [v for k,v in data] 结果 ['abc','bca','cba'] 格式 [x for x in  内容 ...