【FFT实战篇】C++实现:利用快速傅里叶变换快速计算(多项式)乘法

本文使用C++语言实现了快速傅里叶变换FFT并运用其进行多项式乘法计算,适合供学习过《数字信号处理》的读者学习使用FFT快速计算乘法。当然也可供对快速乘法感兴趣的没有系统学习过数字信号处理(从CFT到DTFT到DFT)的读者参考,提供一种不同于纯数学的推导思路。
另外本文着重探讨了实现二进制倒位序(bit-reversal)的方法。

快速傅里叶变换概念

快速傅里叶变换(FFT)是离散傅里叶变换(DFT)的快速算法实现,得以加速DFT是因为使用了分治的思想,本文的讲解采用典型的库利-图基(Cooley-Turkey)算法,即以时间抽选的FFT算法(DIT-FFT)。

但其实我们要关注的是FFT实现的目的:
加速离散傅里叶变换DFT( O ( n 2 ) O(n^2) O(n2))的计算,FFT的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

其中DFT中的离散是在离散时间傅里叶变换(DTFT)的基础上对频域( 2 π 2\pi 2π,周期)幅值进行离散得到的离散傅里叶级数DFS的基础上得到的(由于DFS时域频域都被一个周期内截断得到DFT,只考虑 0 ∼ N − 1 0\sim N-1 0N1的值,但也隐含周期性)所以,得到的是数字频率 2 π N \frac{2\pi}{N} N2π N N N即为DFT的点数,在时域上也有重要性质,具体可查阅系列博客。

数字频率体现在表达式上就是 X ( k ) = ∑ n = 0 N − 1 x ( n ) W N n k , k = 0 , 1 , 2 , ⋯   , N − 1 X(k)=\sum_{n=0}^{N-1}x(n)W_N^{nk},k=0,1,2,\cdots,N-1 X(k)=n=0N1x(n)WNnk,k=0,1,2,,N1
这也就是DFT的表达式,其中 W N n k = e − j 2 π N k W_N^{nk}=e^{-j\frac{2\pi}{N}k} WNnk=ejN2πk是一个简略记号
可以看到离散的频率分量 k k k也被限制在了一个周期范围内

同样的也有逆变换IDFT
x ( n ) = I D F T [ X ( k ) ] = 1 N ∑ n = 0 N − 1 X ( k ) W N − n k , n = 0 , 1 , . . . , N − 1 x(n)=IDFT[X(k)]=\frac{1}{N}\sum_{n=0}^{N-1}X(k)W_N^{-nk},n=0,1,...,N-1 x(n)=IDFT[X(k)]=N1n=0N1X(k)WNnk,n=0,1,...,N1

通过DFT我们就将离散序列从时域形式转化到了频域形式,这个频域由 2 π 2\pi 2π N N N点抽样,目标就是求出周期内所有抽样点处的频域分量 X ( k ) X(k) X(k)

想要了解原理,可参考【FFT理论篇】

快速傅里叶变换意义

通过 N N N点DFT,将序列转化成了 N N N个频域点表示。而通过IDFT,又可以将频域点重新转化成序列。
如果对卷积稍有了解的话应该知道连续情况下的卷积定理,时域上的卷积相当于频域上的相乘。
在DFT的情况下引入了圆周卷积和,情况稍有改变,但需要知道的是在一定条件下可以由圆周卷积和求出线性卷积和。

与多项式的关系

多项式 P ( x ) = ∑ n = 0 N − 1 a i x n P(x)=\sum_{n=0}^{N-1}a_ix^n P(x)=n=0N1aixn可由其系数向量 [ a N − 1 , a N − 2 , ⋯   , a 0 ] [a_{N-1},a_{N-2},\cdots,a_0] [aN1,aN2,,a0]表示
两多项式相乘可通过系数向量相乘得出结果多项式的系数进而确定多项式

考虑 P ( x ) P(x) P(x) Q ( x ) = ∑ n = 0 M − 1 b i x n Q(x)=\sum_{n=0}^{M-1}b_ix^n Q(x)=n=0M1bixn做多项式乘法
结果多项式的度数为 N + M − 1 N+M-1 N+M1,对多项式系数进行乘法,相当于竖式乘法(每一项与另外一式中的所有系数相乘,最后相同幂次的结果相加),发现这其实就是计算卷积和中的“对位相乘相加法”[2017,程佩青,数字信号处理教程,清华大学出版社],因此竖式乘法/多项式乘法可以当做卷积和运算来处理。

下面是一个例子,通过图1(下方)更好地去体会
请添加图片描述

结论一:多项式乘法并不是按照幂的次数来两两相乘多项式系数的数乘,而是多项式系数序列的卷积和

在时域上计算卷积和需要 O ( n 2 ) O(n^2) O(n2)的时间复杂度,因此考虑利用卷积定理将其转化到频域( O ( n l o g n ) O(nlogn) O(nlogn))上的数乘进行计算。也就是先对两个多项式系数序列进行FFT转成频域表示,然后将FFT的结果进行 O ( n ) O(n) O(n)的相乘,再将数乘结果作IFFT转成时域表示得到了多项式乘法的结果。

离散傅里叶变换的圆周卷积和定理

既然目标是计算两个多项式系数序列的卷积(线性卷积和),现在关注如何通过FFT来快速计算。
引出DFT的圆周卷积和定理

长度为 N 1 N_1 N1的序列 x 1 ( n ) x_1(n) x1(n)与长度为 N 2 N_2 N2的序列 x 2 ( n ) x_2(n) x2(n) L L L点圆周卷积和为
y ( n ) = x 1 ( n ) Ⓛ x 2 ( n ) = x 2 ( n ) Ⓛ x 1 ( n ) = [ ∑ m = 0 l − 1 x 1 ( m ) x 2 ( ( n − m ) ) L ] R L ( n ) , L ≥ m a x [ N 1 , N 2 ] \begin{aligned}y(n)=&x_1(n)Ⓛx_2(n)=x_2(n)Ⓛx_1(n)\\=&[\sum_{m=0}^{l-1}x_1(m)x_2((n-m))_L]R_L(n),L\ge max[N_1,N_2]\end{aligned} y(n)==x1(n)x2(n)=x2(n)x1(n)[m=0l1x1(m)x2((nm))L]RL(n),Lmax[N1,N2]

L L L为圆周卷积和的点数。通过表达式看出圆周卷积和计算公式与DFT的形式类似,都是在有限点数内进行截断得到的结果。

之所以与连续卷积情况不同是因为DFT本身是在一个周期内截断并且具有周期性的结果,产生了与圆周(周期)有关的系列性质。

我们要求的线性卷积和的长度为 N 1 + N 2 − 1 N_1+N_2-1 N1+N21,只有当取 L ≥ N 1 + N 2 − 1 L\ge N_1+N_2-1 LN1+N21时,才能从圆周中恢复出完整的线性卷积和序列。为了满足FFT要求的点数,我们要使DFT(也就是FFT)的点数,同时也是圆周卷积和的点数 N = L = 2 m ≥ N 1 + N 2 − 1 , m 为正整数 N=L=2^m\ge N_1+N_2-1,m为正整数 N=L=2mN1+N21,m为正整数

圆周卷积和定理

y ( n ) = x 1 ( n ) Ⓛ x 2 ( n ) y(n)=x_1(n)Ⓛx_2(n) y(n)=x1(n)x2(n)
Y ( k ) = X 1 ( k ) X 2 ( k ) , L 点 Y(k)=X_1(k)X_2(k),L点 Y(k)=X1(k)X2(k),L
上面我们已经知道了由圆周卷积和可以得到我们要求的线性卷积和,根据圆周卷积定理公式我们得到第二个结论

结论二:为了计算线性卷积和,需要将两个序列补到一个为2的整次幂的点数 N = 2 m ≥ N 1 + N 2 − 1 , N=2^m\ge N_1+N_2-1, N=2mN1+N21,并对两个序列作 N N N点DFT,以通过圆周卷积和定理计算 N N N点圆周卷积。对 N N N点圆周卷积作 N 1 + N 2 − 1 N_1+N_2-1 N1+N21点截断就是线性卷积和。

情境分析

Luogu P3803 【模板】多项式乘法(FFT)

题目描述

给定一个 n n n 次多项式 F ( x ) F(x) F(x),和一个 m m m 次多项式 G ( x ) G(x) G(x)

请求出 F ( x ) F(x) F(x) G ( x ) G(x) G(x) 的卷积。

输入格式

第一行两个整数 n , m n,m n,m

接下来一行 n + 1 n+1 n+1 个数字,从低到高表示 F ( x ) F(x) F(x) 的系数。

接下来一行 m + 1 m+1 m+1 个数字,从低到高表示 G ( x ) G(x) G(x) 的系数。

输出格式

一行 n + m + 1 n+m+1 n+m+1 个数字,从低到高表示 F ( x ) ⋅ G ( x ) F(x) \cdot G(x) F(x)G(x) 的系数。

样例 #1

样例输入 #1
1 2
1 2
1 2 1
样例输出 #1
1 4 5 2

提示

保证输入中的系数大于等于 0 0 0 且小于等于 9 9 9

对于 100 % 100\% 100% 的数据: 1 ≤ n , m ≤ 10 6 1 \le n, m \leq {10}^6 1n,m106

分析

按照上面FFT与多项式原理的介绍,将输入的两个多项式序列作FFT,再对FFT的结果数乘后作IFFT得到结果。
X ( k ) = D F T [ x ( n ) ] = ∑ n = 0 N − 1 x ( n ) W N n k ⏟ N 点长序列 = ∑ n = 0 N − 1 x ( n ) W N n k ⏟ n 为偶数 + ∑ n = 0 N − 1 x ( n ) W N n k ⏟ n 为奇数 = ∑ r = 0 N 2 − 1 x ( 2 r ) W N 2 r k + W N k ∑ r = 0 N 2 − 1 x ( 2 r + 1 ) W N 2 r k = ∑ r = 0 N 2 − 1 x 1 ( r ) W N r k 1 + W N k ∑ r = 0 N 2 − 1 x 2 ( r ) W N r k 1 , k 1 = 2 k = 0 , 2 , 4 , ⋯   , 2 ( N − 1 ) = ∑ r = 0 N 2 − 1 x 1 ( r ) W N / 2 r k ⏟ N 2 点长序列 + W N k ∑ r = 0 N 2 − 1 x 2 ( r ) W N / 2 r k ⏟ N 2 点长序列 , k = 0 , 1 , 2 , ⋯   , N − 1 = { ∑ r = 0 N 2 − 1 x 1 ( r ) W N / 2 r k + W N k ∑ r = 0 N 2 − 1 x 2 ( r ) W N / 2 r k , k = 0 , 1 , 2 , ⋯   , N 2 − 1 ∑ r = 0 N 2 − 1 x 1 ( r ) W N / 2 r k − W N k ∑ r = 0 N 2 − 1 x 2 ( r ) W N / 2 r k , k = N 2 , N 2 + 1 , ⋯   , N − 1 \begin{aligned}X(k)=&DFT[x(n)]=\begin{matrix}\underbrace{\sum_{n=0}^{N-1}x(n)W_N^{nk}}\\ N点长序列\end{matrix}=\begin{matrix} \underbrace{\sum_{n=0}^{N-1} x(n)W_N^{nk}} \\ n为偶数 \end{matrix}+\begin{matrix} \underbrace{\sum_{n=0}^{N-1} x(n)W_N^{nk}} \\ n为奇数 \end{matrix}\\=&\sum_{r=0}^{\frac{N}{2}-1}x(2r)W_N^{2rk}+W_N^k\sum_{r=0}^{\frac{N}{2}-1}x(2r+1)W_N^{2rk}\\=&\sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_N^{rk_1}+W_N^k\sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_N^{rk_1},k_1=2k=0,2,4,\cdots,2(N-1)\\=&\begin{matrix}\underbrace{\sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_{N/2}^{rk}}\\ \frac{N}{2}点长序列 \end{matrix}+W_N^{k}\begin{matrix}\underbrace{\sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_{N/2}^{rk}}\\ \frac{N}{2}点长序列 \end{matrix},k=0,1,2,\cdots,N-1\\=&\begin{cases}\sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_{N/2}^{rk}+W_N^k\sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_{N/2}^{rk},k=0,1,2,\cdots,\frac{N}{2}-1\\\sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_{N/2}^{rk}-W_N^k\sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_{N/2}^{rk},k=\frac{N}{2},\frac{N}{2}+1,\cdots,N-1\end{cases}\end{aligned} X(k)=====DFT[x(n)]= n=0N1x(n)WNnkN点长序列= n=0N1x(n)WNnkn为偶数+ n=0N1x(n)WNnkn为奇数r=02N1x(2r)WN2rk+WNkr=02N1x(2r+1)WN2rkr=02N1x1(r)WNrk1+WNkr=02N1x2(r)WNrk1,k1=2k=0,2,4,,2(N1) r=02N1x1(r)WN/2rk2N点长序列+WNk r=02N1x2(r)WN/2rk2N点长序列,k=0,1,2,,N1 r=02N1x1(r)WN/2rk+WNkr=02N1x2(r)WN/2rk,k=0,1,2,,2N1r=02N1x1(r)WN/2rkWNkr=02N1x2(r)WN/2rk,k=2N,2N+1,,N1

Radix-2 DIT-FFT的思路就是将计算 N N N点DFT分解成计算两个 N 2 \frac{N}{2} 2N点DFT之和,如此分解进行下去直到…计算出2点DFT(不可再分解),再按照最后的公式一层层地往回代出结果。
这样子一层层将一次 O ( n 2 ) O(n^2) O(n2)的运算分解成缩小的 n n n的两次运算,将大问题分解成了子问题,由于进行到底,每一层只有 O ( n ) O(n) O(n)的运算,这也是分治法(Divide And Conquer)的基本思路。
请添加图片描述

分解的原理是按照点数(时间)的奇偶对DFT计算式进行重排, N N N点序列的 N N N点DFT变成了两个 N 2 \frac{N}{2} 2N点长的序列的 N N N点DFT,由于 N 2 \frac{N}{2} 2N点DFT只有 N 2 \frac{N}{2} 2N个不重复的频率,推导的最后变成了计算两个 N 2 \frac{N}{2} 2N点序列的 N 2 \frac{N}{2} 2N点DFT,但是要分段计算。
要分段是因为按照时间抽选,从奇数序列DFT的 x ( 2 r + 1 ) W N ( 2 r + 1 ) k x(2r+1)W_N^{(2r+1)k} x(2r+1)WN(2r+1)k中提出 W N k W_N^k WNk,留下 ∑ r = 0 N 2 − 1 x ( 2 r + 1 ) W N 2 r k \sum_{r=0}^{\frac{N}{2}-1}x(2r+1)W_N^{2rk} r=02N1x(2r+1)WN2rk使得频率分量与偶数序列保持一致。
这个提出来的复数系数 W N k = e − j 2 π N k W_N^k=e^{-j\frac{2\pi}{N}k} WNk=ejN2πk有时也被称为旋转因子(twiddle factor)
而分段是用到了 W N k W_N^k WNk的一个性质: W N / 2 k + N 2 = − W N / 2 k W_{N/2}^{k+\frac{N}{2}}=-W_{N/2}^k WN/2k+2N=WN/2k N 2 \frac{N}{2} 2N之后要变号

在上面完整的推导过程中还用到了 W N n k W_N^{nk} WNnk的可约性: W N i n = W N i n W_N^{in}=W_{\frac{N}{i}}^{n} WNin=WiNn W i N i n = W N n W_{iN}^{in}=W_{N}^{n} WiNin=WNn

这里顺便注意一个点,如果在第一段计算旋转因子时记录下 W N k ∑ r = 0 N 2 − 1 x 2 ( r ) W N / 2 r k W_N^k\sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_{N/2}^{rk} WNkr=02N1x2(r)WN/2rk的值,在计算第二段时可以直接使用,省去了一次乘法。 ∑ r = 0 N 2 − 1 x 1 ( r ) W N / 2 r k \sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_{N/2}^{rk} r=02N1x1(r)WN/2rk同理。

这样下来分解一次DFT只需要进行两次复数加法,一次复数乘法。(注意 ∑ r = 0 N 2 − 1 x 1 ( r ) W N / 2 r k \sum_{r=0}^{\frac{N}{2}-1}x_1(r)W_{N/2}^{rk} r=02N1x1(r)WN/2rk ∑ r = 0 N 2 − 1 x 2 ( r ) W N / 2 r k \sum_{r=0}^{\frac{N}{2}-1}x_2(r)W_{N/2}^{rk} r=02N1x2(r)WN/2rk是DFT的结果由下一层在返回时已经给出为已知量,实际上是一次复数加法,一次复数减法,再加上旋转因子乘上 x 2 ( r ) x_2(r) x2(r)DFT结果的复数乘法)。
这一过程若画出蝴蝶图(butterfly diagram)能更好的看出特点,对于整体复杂度的分析也更容易把握,蝴蝶图请参阅理论篇。其实蝴蝶图就是横版的图2,理解蝴蝶运算需要把握三点:
一,序列的总长度不变,每一层的序列总点数是 N N N,且蝴蝶运算结的数量每层相等
二,序列的层数为 l o g 2 N log_2N log2N,这也是对应的下标二进制表示的位数
三,计算每一层的“父节点”的值只在上一层的下标的“中点“( N 2 \frac{N}{2} 2N)处考虑分段,且当前层的两个节点各长 N 2 \frac{N}{2} 2N要各自对父节点做出贡献,在图中这两个节点的元素之间的距离是随层数规律变化的

由于涉及到复频率,代码中需要使用复数,这里使用C++ STL中的complex,具体使用方法见代码

递归版FFT

在递归函数中直接开辟出新的两个数组保存重排结果并作为参数传递下去
旋转因子可以在循环中不断累乘得到

void FFT(complex<double> A[], int n) 
{
    if (n == 1)
        return;
    complex<double> AE[n / 2], AO[n / 2];
    for (int i = 0; i < n / 2; i++)
    {
        AE[i] = A[i * 2]; //偶数频率,这里奇偶指的是对于当前序列
        AO[i] = A[i * 2 + 1]; //奇数频率
    }
    FFT(AE, n / 2);
    FFT(AO, n / 2); //递归求解
    complex<double> W, Wk;
    W.real(cos(pi * 2 / n));
    W.imag(-sin(pi * 2 / n));
    Wk = 1; //旋转因子初始值
    for (int i = 0; i < n / 2; i++)
    {
        A[i] = AE[i] + Wk * AO[i];
        A[i + n / 2] = AE[i] - Wk * AO[i];
        Wk *= W;
    }
}

上面的代码比较朴素,实际上可以进行很多优化。
最致命的是这段代码的内存开销过大,在本题中 N = 1 e 6 N=1e6 N=1e6下在递归函数下(栈区)开辟一个 5 e 5 5e5 5e5大小的complex甚至会直接爆栈,为此我们引入迭代版FFT。
请添加图片描述

但是想要迭代循环实现FFT,我们有一个至关重要的问题函待解决。我们之所以进行递归,就是为了不断分解序列(二叉树的后序遍历)得到最底下一层的序列的排序(它们对应在根节点的排列下的下标),我们把这个顺序叫做倒位序,因为通过观察发现其二进制表示为自然序二进制的倒位。

为什么是自然序二进制的倒位呢,我们在每一层向下分解时实际上是按照奇偶进行分组,也就是以二进制的0和1作为父节点不断向下构建二叉树,从图4的下标生成树可以清晰理解二进制倒位序背后的原因
请添加图片描述

其实在自顶向下得到这个倒位序的过程中并没有进行运算,我们只需关心从倒位序向上合并得到结果的过程,所以要找到一种能够直接求出倒位序的方法。

二进制倒位序的算法

我们需要在原址(即输入序列的数组上)计算出其倒位序。考虑朴素的算法,使用递归或者循环,依次从右向左访问二进制的每一位并将其保存或与对称的对应位交换,这样要计算出从0到N-1的每个数的倒位序的时间复杂度是 O ( N 2 ) O(N^2) O(N2),速度太慢。
这里介绍几种容易理解但又比朴素方法高效的二进制倒位序算法。

Gold-Rader算法

雷德算法(Gold-Rader‘s algorithm)是最早提出也是运用最广泛的计算倒位序的方法[1969,B,Gold and C.M.Rader,Digital Processing of Signals,McGraw-Hill],广大《数字信号处理》教材对这种方法有初步的介绍。
雷德算法相较于朴素算法的改进为,不再依次正序枚举所有要进行倒位的数再逐位反转,而是考虑观察规律直接按序枚举倒位序数。观察倒位序数的排列发现,倒位序的产生规律与自然序数类似,只是进位方向相反,雷德算法模拟倒位序的进位规则产生下一个倒位序数。
请添加图片描述

由图5观察到一些进位规律(在十进制下):最高位对应 N 2 \frac{N}{2} 2N,若最高位为0,优先进最高位;若最高位为1,最高位归零去进下一位(直到从右向左找到不为一的首个零位,遇到的1全部归零),次高位对应 N 4 \frac{N}{4} 4N,然后是 N 8 \frac{N}{8} 8N,…,依次类推
下面给出雷德算法的实现

int Rader(int num,int len) //Rader算法产生num的下一个倒位序数
{
    int k = len >> 1; //len为DFT的点数,是十进制下的最大值加一
    while(num >= k){ //第一种情况,进位后当前位需要置零
        num -= k;![请添加图片描述](https://img-blog.csdnimg.cn/direct/18cecc24070a4231a9aca726d477ddfa.png)

        k >>= 1;
    }
    if(num < k)num += k; //第二种情况,只需要最高位进位
    return num;
}

倒位序序列与二进制位数的关系

雷德算法找到了相邻两个倒位序二进制数的联系,通过模拟进位的方法可以顺序求出下一个数,但是在一些情况下仍然需要遍历几乎全部数位。
其实不同长度之间二进制倒序序列是有联系的,现在考虑找出规律,由当前长度的倒序序列推出当前二进制位长度加一的倒序序列。

规律及证明

考虑二进制位的长度为 M M M,则十进制序列的长度(点数) N = 2 M N=2^M N=2M,列出倒序序列随 M M M变化的表格如下
在这里插入图片描述
其实这就是图4下标生成树的表格形式,我们容易看出:
十进制点长 N = 2 M N = 2^M N=2M 的倒序序列(以下称为目标序列)可由点长为 N 1 = N 2 N_1=\frac{N}{2} N1=2N(二进制长度为 M − 1 M-1 M1)的倒序序列(以下称为生成序列)按照如下规则生成(十进制视角):
1.生成序列的每个元素 n n n 不变
2.同时在每个元素 n n n后面插入一个新的元素 n + N 1 n +N_1 n+N1

刘大庆等人证明了这条性质[2018.刘大庆,林浩然,陈树越.快速傅里叶变换中计算倒序的新思路[J].电子与信息学报]

证明的简要思路如下:
1.将目标序列(二进制长度为 M M M)的 N N N个元素每相邻两个分成一组,共分成 N 1 = N 2 N_1=\frac{N}{2} N1=2N
2.第 n n n组的元素为 a 2 n a_{2n} a2n, a 2 n + 1 a_{2n+1} a2n+1 n = 0 , 1 , 2 , ⋯   , N 2 − 1 n=0,1,2,\cdots,\frac{N}{2}-1 n=0,1,2,,2N1,这里的下标是从左往右数的当前序列的下标
3.考虑十进制元素 n n n,在目标序列长度下的二进制自然序表达为 ( 0 , n M − 2 , n M − 3 , ⋯   , n 1 , n 0 ) (0,n_{M-2},n_{M-3},\cdots,n_{1},n_{0}) (0,nM2,nM3,,n1,n0) n < N 2 = 2 M n<\frac{N}{2}=2^M n<2N=2M)
4.则生成序列长度下中 n n n可自然序表达为 ( n M − 2 , n M − 3 , n M − 4 , ⋯   , n 1 , n 0 ) (n_{M-2},n_{M-3},n_{M-4},\cdots,n_{1},n_{0}) (nM2,nM3,nM4,,n1,n0),倒序表达为 ( n 0 , n 1 , n 2 , ⋯   , n M − 3 , n M − 2 ) (n_0,n_1,n_2,\cdots,n_{M-3},n_{M-2}) (n0,n1,n2,,nM3,nM2)
5.倒位序序列中的 n n n个元素 a n a_n an根据定义恰为 n n n的二进制倒位序,因为倒位序序列是自然序序列的倒序而自然序序列从 0 0 0 N − 1 N-1 N1 n n n包含在内
6.根据第5点观察,生成序列的第 n n n个元素 a n a_n an即为 ( n 0 , n 1 , n 2 , ⋯   , n M − 3 , n M − 2 ) (n_0,n_1,n_2,\cdots,n_{M-3},n_{M-2}) (n0,n1,n2,,nM3,nM2)
7.根据二进制移位的性质(移位后倒序),由生成序列的 a n a_n an移位得到的二进制 a 2 n a_{2n} a2n即为 ( 0 , n 0 , n 1 , n 2 , ⋯   , n M − 3 , n M − 2 ) (0,n_0,n_1,n_2,\cdots,n_{M-3},n_{M-2}) (0,n0,n1,n2,,nM3,nM2),这与 ( n 0 , n 1 , n 2 , ⋯   , n M − 3 , n M − 2 ) (n_0,n_1,n_2,\cdots,n_{M-3},n_{M-2}) (n0,n1,n2,,nM3,nM2)即目标序列下 a 2 n a_{2n} a2n的值是一致的,证明了生成规则的第1点
8.先移位后按照规律进位, a 2 n + 1 a_{2n+1} a2n+1 ( 1 , n 0 , n 1 , n 2 , ⋯   , n M − 3 , n M − 2 ) (1,n_0,n_1,n_2,\cdots,n_{M-3},n_{M-2}) (1,n0,n1,n2,,nM3,nM2),这是生成序列中没有的,转化成十进制表示即证明了生成规则的第2点

Rius等人也发现了类似的规律,他们是从同一二进制长度下分组的思路来实现的。[1995.Juan M.Rius and R.De Porrata-Dòria.New FFT Bit-Reversal Algorithm[J].IEEE Transactions on Signal Processing]
请添加图片描述

再次改进及代码实现

上述的方法虽然发现了倒位序序列与二进制长度之间的联系,但是没有精确到元素,需要对整段序列中的所有元素进行操作,产生了大量不同长度的中间元素,占用额外的内存,第一篇文章中的流程图说明了这一点。
我们深入挖掘第一种方法背后的原理发现其实可以使用递推的方法依次求出倒位序数,整个过程不需要额外的空间,是真正 O ( n ) O(n) O(n)的实现。

1.生成序列的每个元素 n n n 不变
2.同时在每个元素 n n n后面插入一个新的元素 n + N 1 n +N_1 n+N1

目标序列 b b b的十进制长度是生成序列 a a a的两倍,由证明过程可知 b 2 n = a n b_{2n}=a_n b2n=an, b 2 n + 1 = a n + N 1 b_{2n+1}=a_n+N_1 b2n+1=an+N1
十进制数 n n n的一半 n 2 \frac{n}{2} 2n的二进制长度减一,因此这两个序列可以合二为一为 R R R

在求倒位序第 n n n个元素 R [ n ] R[n] R[n]递推过程中已知 R [ n / 2 ] R[n/2] R[n/2]即倒位序的第 n 2 \frac{n}{2} 2n个元素。若是第1种情况,直接维持元素不变;若为第二种情况,需要加上 N / 2 N/2 N/2

R [ n ] = R [ n > > 1 ] > > 1 + ( R & 1 ) ∗ n / 2 R[n]=R[n>>1]>>1+(R\And 1)*n/2 R[n]=R[n>>1]>>1+(R&1)n/2

这是一种非常巧妙的方法,只需根据倒位序的含义就可以理解,但是要详细掌握第一种方法的推导过程,详情请见下面的代码。

迭代版FFT

在原址上倒位序后仅需自底向上进行蝴蝶运算,注意原址运算的先后覆盖问题,代码中使用了额外的变量来处理这一问题。
在蝴蝶运算中使用了两个不同的指针,这部分编写有一定的技巧。

void FFT_Iteration(complex<double> A[], int n)
{
    // bit reversal
    pos[0] = 0;//第0个元素倒位序已知
    for (int i = 1; i < n; i++)
        pos[i] = pos[i / 2] / 2 + (i % 2) * n / 2; //求倒位序
    // cout << endl;
    for (int i = 0; i < n; i++)
        if (i < pos[i])
            swap(A[i], A[pos[i]]); //根据倒位序原址交换
    // butterfly diagram
    for (int l = 2; l <= n; l *= 2)
    {
        complex<double> W, Wk;
        for (int i = 0; i < n; i += l)
        {
            Wk = 1;
            W.real(cos(pi * 2 / l));
            W.imag(-sin(pi * 2 / l));
            for (int j = 0; j < l / 2; j++)
            {
                // A[i+j]=A[i+j]+Wk*A[i+j+l/2]; 错误写法
                // A[i+j+l/2]=A[i+j]-Wk*A[i+j+l/2];
                complex<double> x = A[i + j], y = A[i + j + l / 2] * Wk;//使用额外变量
                A[i + j] = x + y;
                A[i + j + l / 2] = x - y;
                Wk *= W;
            }
        }
    }
}

多项式乘法完整代码(迭代版)

IFFT可以直接按照公式,将FFT中的 W N k W_N^k WNk换成 W N − k W_N^{-k} WNk,再在结果处乘上 1 N \frac{1}{N} N1即可。
或者为了保持IFFT函数的对应性,注意到 N = 2 L N=2^L N=2L L L L为FFT层数),在每层计算时结果累乘 1 2 \frac{1}{2} 21也不失为一种好方法。
实际上计算两个实数序列(本题)的FFT还可以进行优化到只需一次FFT,这里篇幅受限不再介绍。

#include <bits/stdc++.h>
using namespace std;
const int N = 4e6 + 10;
const double pi = acos(-1);
#define endl '\n'
complex<double> A[N], B[N], C[N];
int pos[N];
void FFT_Iteration(complex<double> A[], int n)
{
    // bit reversal
    pos[0] = 0;
    // cout << "after reversal ";
    for (int i = 1; i < n; i++)
    {
        pos[i] = pos[i / 2] / 2 + (i % 2) * n / 2;
        // cout << pos[i] << " ";
    }
    // cout << endl;
    for (int i = 0; i < n; i++)
        if (i < pos[i])
            swap(A[i], A[pos[i]]);
    // for (int i = 0; i < n; i++)
    // cout << A[i] << " ";
    // cout << endl;
    // butterfly diagram
    for (int l = 2; l <= n; l *= 2)
    {
        complex<double> W, Wk;
        for (int i = 0; i < n; i += l)
        {
            Wk = 1;
            W.real(cos(pi * 2 / l));
            W.imag(-sin(pi * 2 / l));
            for (int j = 0; j < l / 2; j++)
            {
                // A[i+j]=A[i+j]+Wk*A[i+j+l/2];
                // A[i+j+l/2]=A[i+j]-Wk*A[i+j+l/2];
                complex<double> x = A[i + j], y = A[i + j + l / 2] * Wk;
                A[i + j] = x + y;
                A[i + j + l / 2] = x - y;
                Wk *= W;
            }
        }
    }
}
void IFFT_Iteration(complex<double> A[], int n)
{
    // bit reversal
    pos[0] = 0;
    // cout << "after reversal ";
    for (int i = 1; i < n; i++)
    {
        pos[i] = pos[i / 2] / 2 + (i % 2) * n / 2;
        // cout << pos[i] << " ";
    }
    // cout << endl;
    for (int i = 0; i < n; i++)
        if (i < pos[i])
            swap(A[i], A[pos[i]]);
    // for (int i = 0; i < n; i++)
    // cout << A[i] << " ";
    // cout << endl;
    // butterfly diagram
    for (int l = 2; l <= n; l *= 2)
    {
        complex<double> W, Wk;
        for (int i = 0; i < n; i += l)
        {
            Wk = 1;
            W.real(cos(pi * 2 / l));
            W.imag(sin(pi * 2 / l));
            for (int j = 0; j < l / 2; j++)
            {
                // A[i+j]=A[i+j]+Wk*A[i+j+l/2];
                // A[i+j+l/2]=A[i+j]-Wk*A[i+j+l/2];
                complex<double> x = A[i + j], y = A[i + j + l / 2] * Wk;
                A[i + j] = x + y;
                A[i + j + l / 2] = x - y;
                Wk *= W;
            }
        }
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    for (int i = 0; i <= n; i++)
        cin >> A[i];
    for (int i = 0; i <= m; i++)
        cin >> B[i];
    int N = 1;
    while (N <= n + m + 1)
        N *= 2;
    FFT_Iteration(A, N);
    FFT_Iteration(B, N);
    for (int i = 0; i < N; i++)
        C[i] = A[i] * B[i];
    IFFT_Iteration(C, N);
    for (int i = 0; i < n + m + 1; i++)
        cout << int(C[i].real() / N + 0.5) << " ";
    // cout << round(C[i].real() / N ) << " "; 速度慢,且有-0问题
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/479828.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何真正改变自己? 《掌控习惯》

维持改变 1.心态 目标与体系&#xff0c;谁是真正通往成功的钥匙&#xff1f; 2.行动 习惯转变的3个层次 身份 你要成为谁&#xff1f; 你为成为他而幸福吗&#xff1f;过程结果 习惯的基本原理&#xff1a;要重视微小的改变 维持改变成两个方面入手 一、心态&#xff1a;忽略…

面向对象编程三大特征

基本介绍 面向对象编程有三大特征&#xff1a;封装、继承和多态。 1、封装介绍 2、继承介绍 3、多态介绍 向上转型与向下转型 多态常用案例 数值比较。

Oracle 写丢失保护/影子表空间(Lost Write Protection with Shadow Tablespace)

写丢失是Oracle数据库与独立I/O子系统交互时一种错误场景。假如Oracle发出的写磁盘命令&#xff0c;I/O子系统也返回成功写磁盘的消息&#xff08;但数据此时可能依然在I/O系统缓存中&#xff09;&#xff0c;如果在I/O系统实际写盘之前Oracle再次读取该数据&#xff0c;则I/O系…

机器人路径规划:基于红尾鹰算法(Red‑tailed hawk algorithm ,RTH)的机器人路径规划(提供MATLAB代码)

一、机器人路径规划介绍 移动机器人&#xff08;Mobile robot&#xff0c;MR&#xff09;的路径规划是 移动机器人研究的重要分支之&#xff0c;是对其进行控制的基础。根据环境信息的已知程度不同&#xff0c;路径规划分为基于环境信息已知的全局路径规划和基于环境信息未知或…

软考中级 --网络工程师真题试卷 2023下半年

在EIGRP协议中&#xff0c;某个路由器收到了两条路径到达目标网络&#xff0c;路径1的带宽为100Mbps&#xff0c;延迟2ms&#xff0c;路径2的带宽为50Mbps&#xff0c;迟为4ms&#xff0c;如果EIGRP使用带宽和延迟的综合度量标准&#xff0c;那么该路由器选择的最佳路径是(D)。…

Vue.js+SpringBoot开发教学过程管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

高架学习笔记之系统分析与设计

目录 一、结构化方法&#xff08;SASD&#xff09; 1.1. 结构化分析方法&#xff08;SA&#xff09; 1.1.1. 数据流图&#xff08;DFD&#xff09; 1.1.2. 实体联系图&#xff08;E-R图&#xff09; 1.1.3. 状态转换图(STD) 1.1.4. 数据字典 1.2. 结构化设计方法&#x…

基于python+vue拍卖行系统的设计与实现flask-django-nodejs-php

拍卖行系统的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&#xff0c;拍卖行…

Day42:WEB攻防-PHP应用MYSQL架构SQL注入跨库查询文件读写权限操作

目录 PHP-MYSQL-Web组成架构 PHP-MYSQL-SQL常规查询 手工注入 PHP-MYSQL-SQL跨库查询 跨库注入 PHP-MYSQL-SQL文件读写 知识点&#xff1a; 1、PHP-MYSQL-SQL注入-常规查询 2、PHP-MYSQL-SQL注入-跨库查询 3、PHP-MYSQL-SQL注入-文件读写 MYSQL注入&#xff1a;&#xff…

路径规划——搜索算法详解(一):Dijkstra算法详解与代码

前言&#xff1a; 本文为小陈同学原创&#xff0c;本人为路径规划方向的研狗一枚&#xff0c;转载请阅读文末的“授权说明”&#xff0c;学习笔记为连载&#xff0c;不定期分享路径规划的知识&#xff0c;欢迎转载、关注、点赞&#x1f44d; &#xff01; 纠结了很久还是打算…

原型、原型链

如图&#xff1a; 判断对错&#xff1a; Q1:在JS 中 proto 和 constructor 属性是对象和函数都有的属性 prototype属性仅是函数对象所独有的。 由于JavaScript中一切皆对象,即函数对象也是一种对象,所以函数也拥有__proto__和constructor属性。 Q2:通过 proto 属性来连接对象…

利用WebGL绘制简单几何

利用WebGL绘制最简单的几何图形 一、WebGL简介 WebGL是一种用于在网页上渲染交互式3D和2D图形的JavaScript API。它基于OpenGL ES 2.0&#xff0c;提供了一种在浏览器中使用硬件加速图形的方式。 二、图形系统绘图流程 图形系统的通用绘图流程会包括六个部分&#xff1a; …

langchain+chatglm3+BGE+Faiss Linux环境安装依赖

前言 本篇默认读者已经看过之前windows版本&#xff0c;代码就不赘述&#xff0c;本次讲述是linux环境配置 超短代码实现&#xff01;&#xff01;基于langchainchatglm3BGEFaiss创建拥有自己知识库的大语言模型(准智能体)本人python版本3.11.0&#xff08;windows环境篇&…

Golang案例开发之gopacket抓包三次握手四次分手(3)

文章目录 前言一、理论知识三次握手四次分手二、代码实践1.模拟客户端和服务器端2.三次握手代码3.四次分手代码验证代码完整代码总结前言 TCP通讯的三次握手和四次分手,有很多文章都在介绍了,当我们了解了gopacket这个工具的时候,我们当然是用代码实践一下,我们的理论。本…

Python计算机二级选择易错题(三)

选择题第02&#xff0c;03&#xff0c;04套 题目来源&#xff1a;python计算机二级真题&#xff08;选择题&#xff09; - 知乎 选择题第02套 选择题第03套 选择题第04套 time()获取当前时间&#xff0c;即计算机内部时间&#xff0c;浮点数&#xff1b;import time库&#x…

鸿蒙Harmony应用开发—ArkTS-@AnimatableExtend装饰器:定义可动画属性

AnimatableExtend装饰器用于自定义可动画的属性方法&#xff0c;在这个属性方法中修改组件不可动画的属性。在动画执行过程时&#xff0c;通过逐帧回调函数修改不可动画属性值&#xff0c;让不可动画属性也能实现动画效果。 可动画属性&#xff1a;如果一个属性方法在animation…

必知必会干货!1分钟搞定Python编程中捕获异常

​ 在Python编程中&#xff0c;异常就是程序在运行过程中出现的问题或错误&#xff0c;比如除数为零、文件找不到等等。而异常捕获&#xff0c;就是通过在代码中设置特定的语句&#xff0c;来捕捉这些异常&#xff0c;并对其进行处理&#xff0c;防止程序崩溃。 那么&#xff…

YOLOv5全网首发改进: 注意力机制改进 | 上下文锚点注意力(CAA) | CVPR2024 PKINet 遥感图像目标检测

💡💡💡本文独家改进:引入了CAA模块来捕捉长距离的上下文信息,利用全局平均池化和1D条形卷积来增强中心区域的特征,从而提升检测精度,CAA和C3进行结合实现二次创新,改进思路来自CVPR2024 PKINet,2024年前沿最新改进,抢先使用 💡💡💡小目标数据集,涨点近两个…

005——串口移植(基于鸿蒙liteos-a)

目录 一、 Liteos-a中串口的使用 1.1 内核里打印 1.2 APP控制台 ​编辑 1.2.1 /dev/console 1.2.2 /dev/serial 1.2.3 /dev/uartddev-0 1. 总体介绍 2. device_t 3. drvier_t 4. uartdev_fops 1.2.4 uart_ops 二、 鸿蒙串口内部的一些机制&#xff08;流水账&…

阿里云4核8G服务器优惠价格表,ECS u1实例

阿里云4核8G服务器优惠价格955元一年&#xff0c;配置为ECS通用算力型u1实例&#xff08;ecs.u1-c1m2.xlarge&#xff09;4核8G配置、1M到3M带宽可选、ESSD Entry系统盘20G到40G可选&#xff0c;CPU采用Intel(R) Xeon(R) Platinum处理器&#xff0c;阿里云活动链接 aliyunfuwuq…