多项式乘法(FFT)

article/2025/8/15 4:16:58

1 前言

作为一名OI选手,至今未写过fft相关的博客,真是一大遗憾,这也导致我并没有真正推过fft的所有式子
这一篇fft的博客我将详细介绍多项式乘法,易于理解,主要是为了等我啥时候忘了回来看,当然,一些公式会有些枯燥,如果是初学者请耐心看完哦,还有,毕竟这是手写出来的,如果有错误,欢迎指正!

2 介绍

本栏用来普及一些知识和对FFT的思路进行描述
多项式乘法,顾名思义,首先是讲到多项式,那么什么是多项式呢?

2.1 多项式

首先是多项式的定义,想必大家都知道(你上过初中吧),而在这里,我们所说的多项式都是单个未知数x的
所以,在我们正常人眼中的一个次多项式就是形如 f ( x ) = a 0 x 0 + a 1 x 1 + ⋅ ⋅ ⋅ + a n − 1 x n − 1 f(x)=a_0x^0+a_1x^1+···+a_{n-1}x^{n-1} f(x)=a0x0+a1x1++an1xn1
没错,这就是大名鼎鼎的系数表示法
然后呢,由于在后面要用到,所以我在这里再介绍一种点值表示法
就是将n个不同的值 x 0 , x 1 ⋅ ⋅ ⋅ x n − 1 x_0,x_1···x_{n-1} x0,x1xn1分别带入 f ( x ) f(x) f(x),获得n个结果 y 0 , y 1 ⋅ ⋅ ⋅ y n − 1 y_0,y_1···y_{n-1} y0,y1yn1,这n对数 ( x 0 , y 0 ) , ( x 1 , y 1 ) ⋅ ⋅ ⋅ ( x n − 1 , y n − 1 ) (x_0,y_0),(x_1,y_1)···(x_{n-1},y_{n-1}) (x0,y0),(x1,y1)(xn1,yn1)就可以表示出这个多项式
看到这里,如果你是初学者,你一定会感到非常迷茫,这为什么对呢?
看到这里,如果你是个FFT高手,你可能会感到迷茫,这为什么对呢?
(dalao勿喷)
在这里插入图片描述
这张图片里系数表示法相当于是最右侧的那个矩阵
而点值表示法则包含了左边的两个矩阵,可以通过这两个矩阵计算出最右侧的那个矩阵,所以两种表示法是等价的
撒花
注:另外要说的是,由于算法需要,本博客所说的n次多项式都默认n是2的幂次(如果不足可以添加0来补)

2.2多项式的乘法

在做了那么久的各种数学题后,我对多项式乘法有了有了的理解
对于一个一般的给定系数表示法的多项式乘法问题
比如两个n次多项式A(x),B(x),给出系数表示法,求它们的乘积C(x)
分别枚举两个多项式中的每一项,分别是 O ( n ) 的 O(n)的 O(n),所以总复杂度为 O ( n 2 ) O(n^2) O(n2)
这是一个很方便的做法

你可以发现一件很有趣的事情,那就是如果给出的是点值表示法,并且两个多项式的x分别对应相等,那么把y对应相乘,就能 O ( n ) O(n) O(n)的获取乘积的点值表示法

2.3 快速傅立叶变换(FFT)

那么,FFT是用来干什么的呢?
对于一个多项式乘法问题,当给出系数表示法的时候, O ( n 2 ) O(n^2) O(n2)的复杂度有时候并不足够优越,而FFT就是一个能使多项式乘法做到 O ( n l o g n ) O(nlogn) Onlogn的一个算法,具体的原理其实非常清晰

  • 两个多项式的系数表示法
    求值,O(nlogn)
  • 两个多项式的点值表示法
    点值乘法,O(n)
  • 两个多项式乘积的点值表示法
    插值,O(nlogn)
  • 两个多项式乘积的系数表示法

是不是一目了然呢?当然,要具体实现,还需要细细说来

3 实现

现在你已经大致知道FFT要干什么了,现在你已经会在点值情况下 O ( n ) O(n) O(n)进行多项式乘法,剩下的就是要解决两个问题——求值与插值了

3.1 暴力算法( O ( n 3 ) O(n^3) O(n3)

要先做题,必先暴力
首先是求值,加入你现在随便找了n个互不相同的x,带入其中,是什么复杂度呢 O ( n 2 ) O(n^2) O(n2)
然后是插值,有一个非常妙的方法,假设所有的a都是未知数,那么这个问题就变成了经典的高斯消元问题,复杂度 O ( n 3 ) O(n^3) O(n3)
不好意思,这两个操作的复杂度都光荣的在 O ( n 2 ) O(n^2) O(n2)以上,使得当前这个算法的总复杂度为 O ( n 3 ) O(n^3) O(n3)比文章开始的那个 O ( n 2 ) O(n^2) On2都要差,不要灰心,既然复杂度不优,那就循序渐进的优化

3.2 离散傅里叶变换(通过优化使上面算法复杂度降到 O ( n 2 ) O(n^2) O(n2),请仔细看完,这是基础)

你会发现,点值表示法有一个很好的特性,就是那个代入的x可以自己选择
离散傅里叶变换的思路是将n个x的值取n个单位根(模长为一的复数)

复数(这是一个知识拓展框)

− 1 \sqrt{-1} 1 这个数,在实数范围内是不存在的,所以拓展出复数这一概念, i = − 1 i=\sqrt{-1} i=1 ,复数就是能够被表示为 z = x + y ∗ i z=x+y*i z=x+yi的数。所以对一个复数,可以用有序数对(x,y)表示,在坐标轴上有对应的点,而这个复数就是从(0,0)到(x,y)的一条有向线段(只会向量的同学可以把它看成向量),而这个复数的模长就等于(0,0)到(x,y)的距离
由于复数是数,所以也有各种运算
加法:(a+bi)+(c+di)=(a+c)+(b+d)i
减法:(a+bi)-(c+di)=(a-c)+(b-d)i
乘法:(a+bi)*(c+di)=(ac-bd)+(ad+bc)i
当然,C++有专门的complex变量可以声明,但是

不推荐使用!!!

为什么呢?因为FFT本身就有一定的常数,如果再用系统complex常数会更大,所以推荐自己手写struct

那么什么是单位根呢?

3.2.1 单位根

单位根所在的点是把单位圆(以原点为圆心,半径为1的圆)从(0,1)开始平均分成n份的分割点
如下图,这就是n=8时的单位圆,绿色圆上的红点就是单位根所在的点
在这里插入图片描述
从(0,1)开始逆时针将这n个点编号,所表示的单位根分别为 w n 1 , w n 1 ⋅ ⋅ ⋅ , w n n − 1 w_n^1,w_n^1···,w_n^{n-1} wn1,wn1,wnn1,特殊的, w n 1 w_n^1 wn1被称为n次单位根。容易发现每个单位根都非常好算,即 w n k = ( c o s k n 2 π , s i n k n 2 π ) w_n^k=(cos\frac{k}{n} 2π,sin\frac{k}{n} 2π) wnk=(cosnk2π,sinnk2π)
这个用三角函数的想法非常好证
知道了这个之后,你会发现很多性质

性质1: w n k = ( w n 1 ) k w_n^k=(w_n^1)^k wnk=(wn1)k

证明:
w n k ∗ w n 1 = ( c o s k n 2 π , s i n k n 2 π ) ∗ ( c o s 1 n 2 π , s i n 1 n 2 π ) = ( c o s k n 2 π ∗ c o s 1 n 2 π − s i n k n 2 π ∗ s i n 1 n 2 π , s i n k n 2 π ∗ c o s 1 n 2 π + c o s k n 2 π ∗ s i n 1 n 2 π ) = ( c o s k + 1 n 2 π , s i n k + 1 n 2 π ) = w n k + 1 \begin{aligned} w_n^k*w_n^1&=(cos\frac{k}{n} 2π,sin\frac{k}{n} 2π)*(cos\frac{1}{n} 2π,sin\frac{1}{n} 2π)\\ &=(cos\frac{k}{n} 2π*cos\frac{1}{n} 2π-sin\frac{k}{n} 2π*sin\frac{1}{n} 2π, sin\frac{k}{n} 2π*cos\frac{1}{n} 2π+cos\frac{k}{n} 2π*sin\frac{1}{n} 2π)\\ &=(cos\frac{k+1}{n} 2π,sin\frac{k+1}{n} 2π)\\ &=w_n^{k+1} \end{aligned} wnkwn1=(cosnk2π,sinnk2π)(cosn12π,sinn12π)=(cosnk2πcosn12πsinnk2πsinn12π,sinnk2πcosn12π+cosnk2πsinn12π)=(cosnk+12π,sinnk+12π)=wnk+1

如果你想问倒数第二个等号怎么等于过去的,请查看

和角公式百度链接:https://baike.baidu.com/item/和角公式/8782319?fr=aladdin

update:lyc建议这里补充一下,我就加一下吧
众所周知 ( a x ) y = a x ∗ y (a^x)^y=a^{x*y} (ax)y=axy
那么知道这个之后就有 ( w n x ) y = ( ( w n 1 ) x ) y = ( w n 1 ) x ∗ y = w n x ∗ y (w_n^x)^y=((w_n^1)^x)^y=(w_n^1)^{x*y}=w_n^{x*y} (wnx)y=((wn1)x)y=(wn1)xy=wnxy

性质2:对于任意一个正整数x, w n ∗ x k ∗ x = w n k w_{n*x}^{k*x}=w_n^k wnxkx=wnk

证明:
w n ∗ x k ∗ x = ( c o s k ∗ x n ∗ x 2 π , s i n k ∗ x n ∗ x 2 π ) = ( c o s k n 2 π , s i n k n 2 π ) = w n k \begin{aligned} w_{n*x}^{k*x}&=(cos\frac{k*x}{n*x} 2π,sin\frac{k*x}{n*x} 2π)\\ &=(cos\frac{k}{n} 2π,sin\frac{k}{n} 2π)\\ &=w_n^k \end{aligned} wnxkx=(cosnxkx2π,sinnxkx2π)=(cosnk2π,sinnk2π)=wnk
没错,约分大法好,这个等式说明,这两个数在单位圆上对应的点是同一个,这个性质,使 w 2 n 2 k = w n k w_{2n}^{2k}=w_n^k w2n2k=wnk

性质3:如果n是偶数,那么 w n k + n 2 = − w n k w_n^{k+\frac n2}=-w_n^k wnk+2n=wnk

证明:
w n k + n 2 = ( c o s k + n 2 n 2 π , s i n k + n 2 n 2 π ) = ( − c o s k n 2 π , − s i n k n 2 π ) = − ( c o s k n 2 π , s i n k n 2 π ) = − w n k \begin{aligned} w_n^{k+\frac n2}&=(cos\frac{k+\frac n2}{n} 2π,sin\frac{k+\frac n2}{n} 2π)\\ &=(-cos\frac{k}{n} 2π,-sin\frac{k}{n} 2π)\\ &=-(cos\frac{k}{n} 2π,sin\frac{k}{n} 2π)\\ &=-w_n^k \end{aligned} wnk+2n=(cosnk+2n2π,sinnk+2n2π)=(cosnk2π,sinnk2π)=(cosnk2π,sinnk2π)=wnk
诱导公式大法好,
s i n ( π + α ) = - s i n α sin(π+α)= -sinα sinπ+α=sinα
c o s ( π + α ) = - c o s α cos(π+α)=-cosα cosπ+α=cosα
理解一下,相当于这两者是单位圆上相对的两个点,值自然是取相反数的啦

3.2.2代入单位根带来的性质

你现在已经知道单位根是什么啦
那么,我们回头看这个离散傅里叶变换,它是求值的时候把x的值分别取 w n 0 , w n 1 ⋅ ⋅ ⋅ w n n − 1 w_n^0,w_n^1···w_n^{n-1} wn0,wn1wnn1这n个数,究竟这么做有什么好处呢?
答案是——你可以比较方便的实现插值!!!
哇塞,这真是很牛逼的呢,插值是暴力算法的瓶颈,如果能优化,那就可以优化总复杂度了
那么如何优化呢?

现在定义对函数f(x)的 离散傅里叶变换为将 w n 0 , w n 1 ⋅ ⋅ ⋅ w n n − 1 w_n^0,w_n^1···w_n^{n-1} wn0,wn1wnn1这n个数作为 x 0 , x 1 ⋅ ⋅ ⋅ x n − 1 x_0,x_1···x_{n-1} x0,x1xn1代入, 离散傅里叶变换的结果为 ( y 0 , y 1 ⋅ ⋅ ⋅ y n − 1 ) (y_0,y_1···y_{n-1}) (y0,y1yn1),容易发现,这是一个插值的过程
然后有一个结论:
一个多项式A(x)在进行离散傅里叶变换后,将离散傅里叶变换的结果的n个y作为系数组成多项式B(x),原来的n个单位根取倒数进行求值,结果的每个数除以n,其结果就是A(x)的各项系数
文字说的可能不太清晰,用数字来表达就是这样的:


A ( x ) = a 0 x 0 + a 1 x 1 + ⋅ ⋅ ⋅ + a n − 1 x n − 1 A(x)=a_0x^0+a_1x^1+···+a_{n-1}x^{n-1} A(x)=a0x0+a1x1++an1xn1
w n 0 , w n 1 ⋅ ⋅ ⋅ w n n − 1 w_n^0,w_n^1···w_n^{n-1} wn0,wn1wnn1作为x分别带入求值
得到 ( y 0 , y 1 ⋅ ⋅ ⋅ y n − 1 ) (y_0,y_1···y_{n-1}) (y0,y1yn1)
将这些y作为系数,产生一个新的多项式B(x)
B ( x ) = y 0 x 0 + y 1 x 1 + ⋅ ⋅ ⋅ + y n − 1 x n − 1 B(x)=y_0x^0+y_1x^1+···+y_{n-1}x^{n-1} B(x)=y0x0+y1x1++yn1xn1
w n − 0 , w n − 1 ⋅ ⋅ ⋅ w n − ( n − 1 ) w_n^{-0},w_n^{-1}···w_n^{-(n-1)} wn0,wn1wn(n1)作为x分别带入求值
得到的 ( z 0 , z 1 ⋅ ⋅ ⋅ z n − 1 ) (z_0,z_1···z_{n-1}) (z0,z1zn1)
对于每个 z k z_k zk,有 z k = a k ∗ n z_k=a_k*n zk=akn

证明

z k = ∑ i = 0 n − 1 y i ∗ ( w n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ∗ ( w n i ) j ) ∗ ( w n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ∗ ( w n i ) j ∗ ( w n − k ) i = ∑ j = 0 n − 1 ∑ i = 0 n − 1 a j ∗ w n i ∗ j ∗ w n − k ∗ i = ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( w n j − k ) i ) \begin{aligned} z_k&=\sum_{i=0}^{n-1}y_i*(w_n^{-k})^i\\ &=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j*(w_n^i)^j)*(w_n^{-k})^i\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j*(w_n^i)^j*(w_n^{-k})^i\\ &=\sum_{j=0}^{n-1}\sum_{i=0}^{n-1}a_j*w_n^{i*j}*w_n^{-k*i}\\ &=\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(w_n^{j-k})^i) \end{aligned} zk=i=0n1yi(wnk)i=i=0n1(j=0n1aj(wni)j)(wnk)i=i=0n1j=0n1aj(wni)j(wnk)i=j=0n1i=0n1ajwnijwnki=j=0n1aj(i=0n1(wnjk)i)
然后对于 ∑ i = 0 n − 1 ( w n j − k ) i \sum_{i=0}^{n-1}(w_n^{j-k})^i i=0n1(wnjk)i,容易发现
如果 j − k = 0 j-k= 0 jk=0那么 w n j − k w_n^{j-k} wnjk就是 w n 0 = 1 w_n^0=1 wn0=1,所以 n n n 1 1 1结果为 n n n
若果 j − k ≠ 0 j-k\neq0 jk̸=0那么通过等比数列求和( x 0 + x 1 + ⋅ ⋅ ⋅ + x n − 1 = x n − 1 x − 1 x^0+x^1+···+x^{n-1}=\frac {x^n-1}{x-1} x0+x1++xn1=x1xn1)可以发现,结果 ( w n j − k ) n − 1 w n j − k − 1 = ( w n n ) j − k − 1 w n j − k − 1 = 1 j − k − 1 w n j − k − 1 = 0 \frac {(w_n^{j-k})^n-1}{w_n^{j-k}-1}=\frac {(w_n^n)^{j-k}-1}{w_n^{j-k}-1}=\frac {1^{j-k}-1}{w_n^{j-k}-1}=0 wnjk1(wnjk)n1=wnjk1(wnn)jk1=wnjk11jk1=0
所以说,这个系数只有在 j − k = 0 j-k=0 jk=0 j = k j=k j=k时才为n,其它都为0
所以 z k = a k ∗ n z_k=a_k*n zk=akn
证毕


对于这个结论,你会发现,如果你用离散傅里叶变换,你的插值就变成了一次求值,你现在的瓶颈也就变成了只有求值这个操作了,NICE!
现在暴力带入的求值的复杂度为 O ( n 2 ) O(n^2) O(n2),所以整个算法的复杂度也为 O ( n 2 ) O(n^2) O(n2)

3.3 快速傅里叶变换(使整个算法复杂度优化到 O ( n l o g n ) O(nlogn) O(nlogn)

现在的复杂度变成 O ( n 2 ) O(n^2) O(n2)了,你可能会说,这不是和暴力一样的复杂度嘛,学了老半天,还是个大常数 O ( n 2 ) O(n^2) O(n2),真没用
别着急,现在算法瓶颈在于求值,只要优化它的复杂度,算法就能变优
然后快速傅里叶变换就来了

Q:傅里叶就可以为所欲为吗?
A:没错,傅里叶就是可以为所欲为!
解释:来一个傅里叶百度百科的链接,他作为一名数学家、物理学家,在计算机发明100+年前就弄出了这个傅里叶变换!!!太巨了orz

现在要做的是,对于一个多项式 A ( x ) = a 0 x 0 + a 1 x 1 + ⋅ ⋅ ⋅ + a n − 1 x n − 1 A(x)=a_0x^0+a_1x^1+···+a_{n-1}x^{n-1} A(x)=a0x0+a1x1++an1xn1,我们需要快速的获得代入 w n 0 , w n 1 ⋅ ⋅ ⋅ w n n − 1 w_n^0,w_n^1···w_n^{n-1} wn0,wn1wnn1的结果(如果这个ok那么代入 w n − 0 , w n − 1 ⋅ ⋅ ⋅ w n − ( n − 1 ) w_n^{-0},w_n^{-1}···w_n^{-(n-1)} wn0,wn1wn(n1)也行)
这个快速傅里叶的一个思路就是分治
首先把这个多项式按次数奇偶分组
A ( x ) = ( a 0 x 0 + a 2 x 2 + ⋅ ⋅ ⋅ + a n − 2 x n − 2 ) + ( a 1 x 1 + a 3 x 3 + ⋅ ⋅ ⋅ + a n − 1 x n − 1 ) A(x)=(a_0x^0+a_2x^2+···+a_{n-2}x^{n-2})+(a_1x^1+a_3x^3+···+a_{n-1}x^{n-1}) A(x)=(a0x0+a2x2++an2xn2)+(a1x1+a3x3++an1xn1)

A 1 ( x ) = ( a 0 x 0 + a 2 x 1 + ⋅ ⋅ ⋅ + a n − 2 x n 2 − 1 ) A_1(x)=(a_0x^0+a_2x^1+···+a_{n-2}x^{\frac n2-1}) A1(x)=(a0x0+a2x1++an2x2n1)
A 2 ( x ) = ( a 1 x 0 + a 3 x 1 + ⋅ ⋅ ⋅ + a n − 1 x n 2 − 1 ) A_2(x)=(a_1x^0+a_3x^1+···+a_{n-1}x^{\frac n2-1}) A2(x)=(a1x0+a3x1++an1x2n1)
那么有
A ( x ) = A 1 ( x 2 ) + x ∗ A 2 ( x 2 ) A(x)=A_1(x^2)+x*A_2(x^2) A(x)=A1(x2)+xA2(x2)
对于所有的值
如果小于 n 2 \frac n2 2n,那么直接带入这个 k k k,有
A ( w n k ) = A 1 ( w n 2 k ) + w n k ∗ A 2 ( w n 2 k ) = A 1 ( w n 2 k ) + w n k ∗ A 2 ( w n 2 k ) \begin{aligned} A(w_n^k)&=A_1(w_n^{2k})+w_n^k*A_2(w_n^{2k})\\ &=A_1(w_{\frac n2}^k)+w_n^k*A_2(w_{\frac n2}^k) \end{aligned} A(wnk)=A1(wn2k)+wnkA2(wn2k)=A1(w2nk)+wnkA2(w2nk)
如果大于等于 n 2 \frac n2 2n,同样带入,用 k + n 2 k+\frac n2 k+2n表示,有
A ( w n k + n 2 ) = A 1 ( w n 2 k + n ) + w n k + n 2 ∗ A 2 ( w n 2 k + n ) = A 1 ( w n 2 k ∗ w n n ) + w n k + n 2 ∗ A 2 ( w n 2 k ∗ w n n ) = A 1 ( w n 2 k ) − w n k ∗ A 2 ( w n 2 k ) \begin{aligned} A(w_n^{k+\frac n2})&=A_1(w_n^{2k+n})+w_n^{k+\frac n2}*A_2(w_n^{2k+n})\\ &=A_1(w_n^{2k}*w_n^n)+w_n^{k+\frac n2}*A_2(w_n^{2k}*w_n^n)\\ &=A_1(w_n^{2k})-w_n^k*A_2(w_n^{2k}) \end{aligned} A(wnk+2n)=A1(wn2k+n)+wnk+2nA2(wn2k+n)=A1(wn2kwnn)+wnk+2nA2(wn2kwnn)=A1(wn2k)wnkA2(wn2k)
然后现在如果知道 A 1 ( x ) A_1(x) A1(x) A 2 ( x ) A_2(x) A2(x)代入 w n 2 0 , w n 2 1 ⋅ ⋅ ⋅ w n 2 n 2 − 1 w_{\frac n2}^0,w_{\frac n2}^1···w_{\frac n2}^{\frac n2-1} w2n0,w2n1w2n2n1的值,那 A ( x ) A(x) A(x)代入 w n 0 , w n 1 ⋅ ⋅ ⋅ w n n − 1 w_n^0,w_n^1···w_n^{n-1} wn0,wn1wnn1的值也可以 O ( n ) O(n) O(n)计算出来了,然后递归解决问题
递归会有终止条件,当n=1的时候带入 w 1 0 w_1^0 w10的值就是那个多项式的 a 0 a_0 a0,就可以直接return了
考虑时间复杂度的分析 T ( n ) = 2 ∗ T ( n 2 ) + O ( n ) T(n)=2*T(\frac n2)+O(n) T(n)=2T(2n)+O(n)
总复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)的,完成!
在FFT说完之际,贴一个经典的揭示暴力多项式乘法和FFT不同的图:在这里插入图片描述
然后你就会写FFT了,贴一个FFT的递归写法:

#include<cstdio>
#include<cctype>
#include<cmath>
namespace fast_IO
{const int IN_LEN=10000000,OUT_LEN=10000000;char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf,*lastin=ibuf+IN_LEN,*lastout=obuf+OUT_LEN-1;inline char getchar_(){return (ih==lastin)&&(lastin=(ih=ibuf)+fread(ibuf,1,IN_LEN,stdin),ih==lastin)?EOF:*ih++;}inline void putchar_(const char x){if(oh==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;*oh++=x;}inline void flush(){fwrite(obuf,1,oh-obuf,stdout);}
}
using namespace fast_IO;
#define getchar() getchar_()
#define putchar(x) putchar_((x))
typedef long long LL;
#define rg register
template <typename T> inline void read(T&x)
{char cu=getchar();x=0;bool fla=0;while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}while(isdigit(cu))x=x*10+cu-'0',cu=getchar();if(fla)x=-x;  
}
template <typename T> void printe(const T x)
{if(x>=10)printe(x/10);putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{if(x<0)putchar('-'),printe(-x);else printe(x);
}
const int maxn=2097153;
const double Pi=acos(-1.0);
struct complex
{double x,y;inline complex operator +(const complex b)const{return (complex){x+b.x,y+b.y};}inline complex operator *(const complex b)const{return (complex){x*b.x-y*b.y,x*b.y+y*b.x};}inline complex operator -(const complex b)const{return (complex){x-b.x,y-b.y};}
}a[maxn],b[maxn];
int n,m,allsum;
void FFT(int lenth,complex*A,const int fla)
{if(lenth==1)return;complex A1[lenth>>1],A2[lenth>>1];for(rg int i=0;i<lenth;i+=2)A1[i>>1]=A[i],A2[i>>1]=A[i+1];FFT(lenth>>1,A1,fla),FFT(lenth>>1,A2,fla);const complex w=(complex){cos(Pi*2.0/lenth),sin(Pi*2.0/lenth)*fla};complex k=(complex){1,0};lenth>>=1;for(rg int i=0;i<lenth;i++,k=k*w){A[i]=A1[i]+k*A2[i];A[i+lenth]=A1[i]-k*A2[i];}
}
int main()
{read(n),read(m),allsum=n+m;for(rg int i=0;i<=n;i++)read(a[i].x);for(rg int i=0;i<=m;i++)read(b[i].x);rg int lenth=1;while(lenth<=n+m)lenth<<=1;FFT(lenth,a,1),FFT(lenth,b,1);for(rg int i=0;i<=lenth;i++)a[i]=a[i]*b[i];FFT(lenth,a,-1);for(rg int i=0;i<=n+m;i++)print((int)(a[i].x/lenth+0.5)),putchar(' ');return flush(),0;
}

对代码的一些解释:
那个FFT()函数是用来求值的,前面已经证明过插值就是把单位根取倒数,所以单位根的标号传-1就好了
另外的部分都是模拟

4 优化

4.1 递归转迭代优化

之前贴的代码在某评测网站上运行最大数据点所花的时间为2493ms,题目数据范围是 n ≤ 1 e 6 n\leq1e6 n1e6,可见这个FFT的速度有一定的常数,这个时候就要考虑一些优化

首先想到的优化常数的算法自然是把递归转迭代了,而这种优化也是最为常见的

某一个写FFT的人发现如下性质:对于 a x a_x ax这个数,递归到最后所在的位置刚好是x的二进制位全部翻转的那一位,比如说4的二进制是(100),最后到了1(001),更多的感兴趣可以自己手模
考虑到你可能连前面的都没看懂,没有能力手模,我还是把它画出来吧

0,1,2,3,4,5,6,7
0,2,4,6
1,3,5,7
0,4
2,6
1,5
3,7
0
4
2
6
1
5
3
7
000
100
010
110
001
101
011
111
000
001
010
011
100
101
110
111

你可以自行对比最后两排看看是不是这样
然后预处理二进制翻转的数组,然后就可以非递归的从底层一层一层往上做了,在我的代码中处理的是Reverse数组
贴一波代码

#include<cstdio>
#include<cctype>
#include<cmath>
namespace fast_IO
{const int IN_LEN=10000000,OUT_LEN=10000000;char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf,*lastin=ibuf+IN_LEN,*lastout=obuf+OUT_LEN-1;inline char getchar_(){return (ih==lastin)&&(lastin=(ih=ibuf)+fread(ibuf,1,IN_LEN,stdin),ih==lastin)?EOF:*ih++;}inline void putchar_(const char x){if(oh==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;*oh++=x;}inline void flush(){fwrite(obuf,1,oh-obuf,stdout);}
}
using namespace fast_IO;
#define getchar() getchar_()
#define putchar(x) putchar_((x))
typedef long long LL;
#define rg register
template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline void read(T&x)
{char cu=getchar();x=0;bool fla=0;while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}while(isdigit(cu))x=x*10+cu-'0',cu=getchar();if(fla)x=-x;  
}
template <typename T> void printe(const T x)
{if(x>=10)printe(x/10);putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{if(x<0)putchar('-'),printe(-x);else printe(x);
}
const int maxn=2097153;const double PI=acos(-1.0);
int n,m;
struct complex
{double x,y;inline complex operator +(const complex b)const{return (complex){x+b.x,y+b.y};}inline complex operator -(const complex b)const{return (complex){x-b.x,y-b.y};} inline complex operator *(const complex b)const{return (complex){x*b.x-y*b.y,x*b.y+y*b.x};}
}a[maxn],b[maxn];
int lenth=1,Reverse[maxn];
inline void init(const int x)
{rg int tim=0;while(lenth<=x)lenth<<=1,tim++;for(rg int i=0;i<lenth;i++)Reverse[i]=(Reverse[i>>1]>>1)|((i&1)<<(tim-1));
}
inline void FFT(complex*A,const int fla)
{for(rg int i=0;i<lenth;i++)if(i<Reverse[i])swap(A[i],A[Reverse[i]]);for(rg int i=1;i<lenth;i<<=1){const complex w=(complex){cos(PI/i),fla*sin(PI/i)};for(rg int j=0;j<lenth;j+=(i<<1)){complex K=(complex){1,0};for(rg int k=0;k<i;k++,K=K*w){const complex x=A[j+k],y=A[j+k+i]*K;A[j+k]=x+y;A[j+k+i]=x-y;}}}
}
int main()
{read(n),read(m);init(n+m);for(rg int i=0;i<=n;i++)read(a[i].x);for(rg int i=0;i<=m;i++)read(b[i].x);FFT(a,1),FFT(b,1);for(rg int i=0;i<lenth;i++)a[i]=a[i]*b[i];FFT(a,-1);for(rg int i=0;i<=n+m;i++)print((int)(a[i].x/lenth+0.5)),putchar(' ');return flush(),0;
}

跑的最慢的点是607ms,常数大大变小了了,大约是 1 4 \frac 14 41的关系
这个版本就是比较常见的了

4.2 其它优化

update by 2019.2.24:
一般的FFT题中可能会用到多次,由于单位根的计算和乘法都比较慢,这个时候我们可以预处理所有的单位根,开一个数组即可,能够提升FFT的精度(在某写情况下会用到)和速度
另外我们发现,对于DFT和IDFT的判断会占据比较多的判定时间,如果不想写两个的话其实还有一个技巧,由于IDFT是代入反的单位根,所以将数组非 0 0 0部分翻转后做一遍DFT即可

5 总结

这一篇FFT的博客奋战完成了,我也彻底的理解了FFT的具体过程,希望你也能从中获益我可是写的很仔细的,毕竟我以后自己要看
对于FFT其实代码并不长,能解决的问题却有很多,之后我会写相关的博客来进行总结
此外不得不提到的是,FFT全程都是double运算,所以就有一些精度上的问题需要注意,为了解决这个问题,有一个叫NTT的算法,是特殊模数的模意义下的多项式乘法,大致和FFT差不多,学有余力可以去学习
如果你已经把NTT也学完了,可以观测一下我的博客多项式运算学习,学会NTT后,你能在那篇博客学到更多的多项式相关操作
撒花结束!
(字数10000+的一篇博客,如果发现有错误欢迎指正!)^ _ ^
update by 2018.11.28: 对部分的公式的对齐进行了改善
update by 2018.12.10:lyc的帮助下,对文章进行了部分修正


http://chatgpt.dhexx.cn/article/ZefSRuck.shtml

相关文章

分治算法-03多项式乘法问题

多项式乘法 简介 多项式的运算表示是一个很常见的算法问题。 问题描述 给予两个多项式A(x)与B(x)&#xff0c;得出C(x)A(x)B(x)。例如&#xff0c;A(x)32x3x24x3&#xff0c;B(x)2x2&#xff0c;C(x)64x9x210x33x44x^5。 问题分析 一般情况下&#xff0c;使用系数表示多项式&a…

【数据结构】——多项式乘法

题目要求 从字符文件输入两个多项式的非零系数及对应的指数&#xff0c;建立多项式的链式存储结构&#xff0c;计算这两个多项式的乘积&#xff0c;输出乘积多项式的全部非零系数及对应的指数到另一字符文件中。 算法原理 两个多项式的乘法&#xff0c;可以借助两个多项式的…

多项式乘法(FFT)详解

本文只探讨多项式乘法(FFT)在信息学中的应用 如有错误或不明欢迎指出或提问&#xff0c;在此不胜感激 多项式 1. 系数表示法 一般应用最广泛的表示方式 用A(x)表示一个x-1次多项式&#xff0c;a[i]为 xi x i 的系数&#xff0c;则A(x) ∑n−10 ∑ 0 n − 1 a[i] * xi x i…

2.2 多项式乘法与加法运算(线性结构,C)

多项式乘法与加法运算 设计函数分别求两个一元多项式的乘积与和题意理解题意理解和积 求解思路多项式表示两种表示方式在事先已经知道具体多少项的时候&#xff0c;本题较好的实现方法&#xff1a;动态数组链表表示多项式的方法 程序框架如何读入多项式读入多项式的完整程序 加…

多项式乘法问题

多项式乘法问题 实验目的&#xff1a;设计一个一元稀疏多项式简单计算器。 实验内容与要求&#xff1a; 一元稀疏多项式简单计算器的基本功能是&#xff1a; &#xff08;1&#xff09;输入并建立多项式&#xff1b; &#xff08;2&#xff09;输出多项式&#xff0c;序列…

网络协议之视频直播核心技术讲解

网络视频直播存在已有很长一段时间&#xff0c;随着移动上下行带宽提升及资费的下调&#xff0c;视频直播被赋予了更多娱乐和社交的属性&#xff0c;人们享受随时随地进行直播和观看&#xff0c;直播的打开时间和延迟变成了影响产品功能发展重要指标。 那么&#xff0c;问题来了…

直播软件搭建技术原理:CDN 与直播

直播软件搭建技术原理&#xff1a;CDN 与直播 很多直播都是基于 CDN 来实现的。而通过声网的服务&#xff0c;或基于声网SDK与 CDN 结合&#xff0c;还可以实现在直播中的连麦互动、白板同步等强调实时性的场景。本文源自社区投稿&#xff0c;介绍了该场景下的一些基础知识。如…

暑期实习+秋招面经合集(更新ing)

大纲 开篇 自我介绍 &#xff1a;面试官你好&#xff0c;我叫林飞武&#xff0c;是一名通信工程大三学生&#xff0c;对计算机专业有着浓厚兴趣并且未来有志于在互联网的测试领域有深入发展。全套学习了计算机专业的专业课&#xff0c;计算机网络&#xff0c;操作系统等&#…

DNSPod十问王征:为什么你的网站无人问津?

&#x1f4e2; DNSPod十八周年庆将至&#xff0c;下期十问交给你来提问——《你&#xff0c;十问DNSPod》&#xff01;评论区留下你想问DNSPod团队的问题&#xff0c;一旦提问被选中&#xff0c;将得到十八周年纪念T恤&#xff01;详情请移步至DNSPod公众号十八周年庆推送。 广…

阿里云ACP认证考试笔记

课件&#xff1a;https://gitee.com/HanFerm/technical-documentation/tree/master/阿里云acp教材 本文档为公开内容 一、ACP是干嘛的 内容范围&#xff1a; 历史 二、阿里云综述 技术架构 优势 三、弹性计算 ECS ECS的组成与功能 ECS是由多个并列又相互关联的产品概念组成…

在互联网上,没有人知道你是一条狗?

1993 年&#xff0c;《纽约客》&#xff08;The New Yorker&#xff09;杂志刊登一则由彼得施泰纳&#xff08;Peter Steiner&#xff09;创作的漫画&#xff1a;标题是【On the Internet, nobody knows you’re a dog.】 这则漫画中有两只狗&#xff1a;一只黑狗站在电脑椅上…

分库分表和NewSQL如何选择?分库分表真的适合你的系统吗?

曾几何时&#xff0c;“并发高就分库&#xff0c;数据大就分表”已经成了处理 MySQL 数据增长问题的圣经。 面试官喜欢问&#xff0c;博主喜欢写&#xff0c;候选人也喜欢背&#xff0c;似乎已经形成了一个闭环。 但你有没有思考过&#xff0c;分库分表真的适合你的系统吗&am…

分库分表不一定适合你的系统,聊聊分库分表和NewSQL如何选择

曾几何时&#xff0c;“并发高就分库&#xff0c;数据大就分表”已经成了处理 MySQL 数据增长问题的圣经。 面试官喜欢问&#xff0c;博主喜欢写&#xff0c;候选人也喜欢背&#xff0c;似乎已经形成了一个闭环。 但你有没有思考过&#xff0c;分库分表真的适合你的系统吗&am…

分库分表真的适合你的系统吗?聊聊分库分表和NewSQL如何选择

曾几何时&#xff0c;“并发高就分库&#xff0c;数据大就分表”已经成了处理 MySQL 数据增长问题的圣经。 面试官喜欢问&#xff0c;博主喜欢写&#xff0c;候选人也喜欢背&#xff0c;似乎已经形成了一个闭环。 但你有没有思考过&#xff0c;分库分表真的适合你的系统吗&am…

分库分表和 NewSQL 到底怎么选?

文章来源&#xff1a;【公众号&#xff1a;CoderW】 目录 背景分表分库分库分表的成本NewSQLNewSQL 平滑接入方案NewSQL 真的有那么好吗&#xff1f;NewSQL 的应用分库分表和 NewSQL 到底怎么选&#xff1f; 背景 曾几何时&#xff0c;“并发高就分库&#xff0c;数据大就分表”…

软考初级-信息处理技术员

22年下半年也是顺利通过了软考初级-信息处理技术员&#xff0c;明年再报一次软设&#xff0c;今年上半年软设下午题没过&#xff0c;总结还是代码敲的少&#xff0c;想的太多hh&#xff0c;下面来看看我总结的上午题考点&#xff0c;非常感谢我好友老郑教我指点了我excel的函数…

学术交流 | InForSec 2023年网络空间安全国际学术研究成果分享及青年学者论坛

隐私计算研习社 InForSec定于2023年4月8日&#xff5e;9日&#xff08;周六、日&#xff09;在南方科技大学举办“InForSec 2023年网络空间安全国际学术研究成果分享及青年学者论坛”。本次学术活动将邀请在网络空间安全顶级会议上发表论文的研究者分享他们的最新研究成果&…

用matlab绘制光滑曲线(plot画出的为折线)

x[0 0.1 0.16 0.27 0.41 0.48 0.59 0.8] y[5 9 70 118 100 17 0 5]; 那么用plot画出的函数为折线&#xff0c;如下图&#xff1a; 要想把那个折点平滑掉。像论文中那样&#xff0c;具体采用样条函数&#xff1a;下面是样条函数的定义&#xff1a; spline function 一类分段&…

matlab plot画曲线/直线/椭圆

本博文源于matlab基础&#xff0c;每个图像一个案例引入&#xff0c;大家简单看&#xff0c;直接照猫画虎去套用就行了。 画直线 例子&#xff1a;画y2*x3 范围为[1,10] 代码&#xff1a; >> x1:10; >> y2*x3; >> plot(y) >> 画曲线 在区间[-Π,Π…

Matlab图形绘制(一)三维曲线

文章目录 1.三维曲线常用函数第一个例子第二个例子第三个例子&#xff1a;&#xff08;更改线性&#xff0c;颜色&#xff09;第四个例子&#xff1a;&#xff08;有返回值的情况&#xff09; 1.三维曲线常用函数 plot3函数&#xff0c;用于绘制3D图形的一个非常常用的函数。 …