0%

FFT学习笔记

FFT 的中文名称为快速傅里叶变换,英文全称为 Fast Fourier Transform。

主要用于快速计算多项式乘法。

系数表示法

我们知道,每一个多项式都有唯一的一组系数,每一组系数也对应着唯一的一个多项式,所以我们可以用一组系数来表示一个多项式:

点值表示法

我们可以把这个多项式看作平面直角坐标系上的一个函数,那么我们就可以从上面选 个点,这些点一定可以唯一表示这个多项式。

为什么这么说呢?我们假设这 个点分别是

那么我们可以由此列出方程: 我们将这些 带入方程组中,便得到了一个有 个方程和 个未知数的方程组。只要所给出的 互不相同,那么我们正好可以解得唯一的一组 ,也就可以表示这个多项式了。

FFT的基本理论

FFT 在 OI 中干的事情就是快速求出两个多项式 的乘积 ,下面我们来看他是怎么做到的。

首先,我们还是把 由系数表示法变为点值表示法,这个过程被叫为 DFT。

假设函数 对应的点坐标分别为 ,函数 对应的点坐标分别为 。(没错,他们的横坐标要相等)

那么我们就可以得到

那么现在我们知道了 的点值表示法,现在我们就可以将他表示为系数表示法,这个过程被叫为 IDFT。

FFT 的理论就是这个样子,但是我们也知道 DFT 暴力的时间复杂度为 ,IDFT 采用高斯消元的做法时间复杂度也为 ,使用拉格朗日插值可以做到 。显然,FFT 的效率还不如暴力高。那么我们的主要问题就为优化 FFT 了。

当然,上述做法有一些漏洞。假如 的最高次项系数都为 ,那么 的最高次项系数就是 。此时肯定是无法用 个点表达 这个多项式了,所以实际上我们需要不止 个点参与计算。

坐标的取值

我们会发现无论如何,似乎 FFT 的时间复杂度都无法得到有效的优化,那么如果我们取的横坐标是一些特殊的值呢?

复数

我们知道,方程 的解在实数域下只有 个,即 ,但是这个方程在复数域下却有 个解。

,那么 这个方程的解便为

另一方面我们可以根据欧拉公式,也可以得到

放一张 oi-wiki 上的图片,有助于理解。

复数的性质

对于任意的正整数 及整数 ,有:

三个公式放到上面那张图里面都不难理解。

复数的运算

,下面我们来推导复数的计算。 复数的除法也是可以推导的,不过我们暂时用不到。

我们可以打出一份复数的结构体。

DFT

首先我们要将一个函数由系数表示法变为点值表示法,我们设这个函数为 ,且第 项的系数为

为了方便起见,我们设 的项数 的整数次幂,如果项数不够可以补齐。

首先我们设:

那么就会有

我们现在要得到 个点的坐标,他们的横坐标分别为 ,下面我们来推式子(颓柿子) 可以发现什么? 经过化简后第一项相同,第二项互为相反数。这时我们是否可以快速进行 DFT 呢?

设对一个项数为 的多项式进行 DFT 的过程时间复杂度为

根据主定理分析可得时间复杂度为

那么我们就可以把 DFT 的代码打出来。

这个代码还可以进行一些常数优化,不过我们后面一起进行优化。

IDFT

这个时候我们得到了答案多项式的点值表示法,我们设这个函数的第 项的系数为 个点的坐标分别为

首先我们可以换一个方式表示这些点与系数的关系。 我们把这些式子写成矩阵的形式。 我们再来看一看最左边的这个矩阵有什么性质: 右边的那个矩阵恰好是单位矩阵的 倍,那么我们就可以把最上面的那个矩阵进行转化。 我们有什么好方法?照样采用 DFT 的方法,对函数按照奇偶进行计算。

首先我们知道 ,那么我们再对上面的式子进行转化。

还是像上面一样,我们设: 那么就有: 我们又可以发现什么? 经过化简后第一项相同,第二项互为相反数。这时我们是否可以快速进行 IDFT 呢?

设对一个项数为 的多项式进行 IDFT 的过程时间复杂度为

分析可得时间复杂度为

那么我们就可以把 IDFT 的代码打出来。

没错这一整段是从上面那里复制之后改了一下。

FFT的优化

首先解决一个问题:我们在进行 DFT 与 IDFT 时都默认 为一个形如 的数字,但是我们在实际应用中并不会全是这种情况。

其实只需要取一个比这个数大并且的最小的 就可以了。

合二为一

我们可以发现 DFT 与 IDFT 的函数中只有一句不一样,那么我们就可以把这两个函数合二为一。

现在我们已经打得差不多了,我们来提交试一下。

代码:

提交记录

蝴蝶变换

虽然这份代码 AC 了,但是它显然还不够快,我们还需要进行亿点点常数优化。

可以注意到一个过程中常数比较大的地方:就是 DFT 过程中数组的打乱。不难发现,这个部分的时间复杂度为 ,而且我们这里还多开了大约 个复数(就是那个叫 mem 的 vector)。

我们来找一下这个地方数字变化的规律,这里以 举例,而且数字的下标以二进制书写,便于发现规律。

我们可以发现什么?

没错,交换前后每个位置上的数字的二进制位全部是相反的。

当然这是我们发现的结论,现在我们来证明。

采用数学归纳法:

首先我们知道当 时,这个结论显然成立,因为这个时候只有一个数字。

现在我们要证明当 时结论成立,我们假设当 时结论成立。

为第 个位置上的数在 时经过交换之后到的地方,一个数字 二进制下的最低位为

那么

显然, 的最后一位放到了最前面,前面的 位经过翻转后放到了后面,这个结论显然是正确的。

现在要做的就是快速计算这个翻转后的值,显然也是可以快速进行计算的。

这部分时间复杂度为 ,这种 FFT 比起之前的常数小了一些。

此外还有一些别的优化,例如提前计算 step 的值,将递归改为循环等,由于比较简单,这里不详细展开。

我们就直接重写代码,看一下提交结果。

代码:

提交记录

三次变两次

三次变两次是通过一种特殊的方法,少进行一次 DFT 的优化(由原来的 次 DFT 和 IDFT变为各有一次)。

假设我们现在想要求两个多项式 的乘积,不妨设复多项式

那么就有

它的虚部不就正好是我们要求的乘积的两倍吗?

代码:

提交记录

可以看到,快了不少。