@dxbdly
2023-07-16T11:51:19.000000Z
字数 8113
阅读 293
OI-算法
是一种用来实现 (离散傅里叶变换)的高效算法。
它一般用于加速多项式乘法,可使其达到 的速度,有时也用于加速大整数乘法。
注: 算法涉及较多前置数学知识,本文仅会介绍重要的前置知识,其他不清楚的请读者自行百度。
通常,我们表示多项式的方式是系数表示法,即使用一个系数数列来表示多项式。
也就是形如:
但如果依照这种表示方式进行多项式乘法,乘积的系数数列会十分复杂,难以研究。
所以我们考虑使用其他方式表示多项式。
我们把多项式看成一个函数,由于我们可以使用 个此函数上的点,来唯一表示此函数。
也就是说,这 个点同样可以唯一表示此多项式。
也就是当:
就有:
这样表示有何好处呢?
我们会发现,在点值表示法下,多项式的乘积将很好表示。
设:有两多项式
与
那么乘积 可表示为:
也就是说,如果我们能将 与 表示为点值表示法,那么就能用 的时间求出 的点值表示。
那么留给我们的就是两个问题:
如何将 与 从系数表示变为点值表示
如何将 从点值表示变回系数表示
我们发现,在解决这两个问题之前,我们需要先找出 个点
并且它们适于我们进行转换,换句话说,我们希望这 个点满足某种规律,且对所有多项式都适用。
在这里,我们需要引入一个概念—— 单位复根
PS:不知道复数是什么的读者请看。
下面附上手写的复数类型,包括复数的三种运算:
//The code is from dxbdlystruct Complex {double x, y;Complex(double x0 = 0, double y0 = 0) { x = x0, y = y0; }}g1[maxn], g2[maxn], sum[maxn];Complex operator + (Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }Complex operator - (Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }Complex operator * (Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
我们称 在复数意义下的解为 次复根。
显然,这样的复根有 个,我们记它们为
我们考虑这个方程的几何意义:

在复平面上作一个单位圆,则此单位圆上的复数向量模长都为
所以我们可以将 理解成复平面上的一个单位向量,它的俯角的 倍正好是
就是 单位复根,且这 次复根将这个圆周 等分。
所以,我们可以利用俯角求出 :
单位复根有三个重要的性质:
对于任意正整数 与整数 ,有:
解释:相当于转一周回到原点。
解释:在 份中取 份与 在 中取 份显然相同
解释:相当于转半圈,坐标取反
解释:分别取 份与 份显然就是共取 份
后续的 推导过程中,需要反复运用这三个式子,请好好理解。
确定了要带入的 个点后,我们先来解决系数表示变点值表示的问题。
这个过程称为 。
如果直接带入点,复杂度仍是 的,不能接受。
由于我们取的点是存在规律的,我们考虑能否一次性将所有点的函数值求出。
很自然的想到了分治。
这里积累一种套路:如果要对多项式分治,可以考虑尝试将奇数项与偶数项分开,构造子问题。
举个例子:
设:
奇偶分组:
可以发现两部分都是 的子结构。
设:
用 与 表示 :
接下来,考虑带入点 :
由于要在 的时间内,所以每次需要将求解的范围缩小。
考虑单位复根性质 ,则可尝试表示
这样,我们就可以用 与 同时算出两个值,保证了 的复杂度。
上面的分治算法虽以达到 的复杂度。
可实际上由于要使用递归,并且需传输大量的数组,指针,实现起来常数巨大。
我们考虑将其用迭代实现,这样,我们就可以从最后的状态倍增的向上合并。
也就是说,我们首先要想办法求出最后分组的状态,也就是确定合并的顺序。
举个例子:
原数列:
最终数列:
而这个序列是有规律的:
对于原来的序列,将每个数用二进制表示,然后将二进制翻转,就是对应的最终位置的下标
我们称这种翻转为位逆序置换,又称蝴蝶变换
根据定义,很容易想到用 的方式求解每个数变换后的结果。
但是这还不够,实际上,我们可以通过递推求出 到 的位逆序置换值。
设: 表示 在 到 中的位逆序置换值
边界:
考虑转移,将 的翻转分为其个位的翻转与个位前的翻转,即: 的翻转与 的翻转
则有:
注意:
是因为去掉个位反转后,首位补的 会翻转到个位,要去掉。
则是如果个位为 ,那么首位为 ,否则补 。
位逆序置换部分代码:
//The code is from dxbdlyinline void Change(Complex f[], int len) {for(register int i = 0; i < len; ++i) {rev[i] = rev[i >> 1] >> 1;if(i & 1) rev[i] |= (len >> 1);}for(register int i = 0; i < len; ++i)if(i < rev[i]) swap(f[i], f[rev[i]]);}
这样就可以用 的时间预处理,然后按照 的过程将其合并就好,具体实现后面会讲。
,即傅里叶反变换算法,它可以解决将点值表示变回系数表示的问题。
它与 算法极为类似,实现时一般也写在一起(后面细讲)。
可以发现, 本质是一个线性变换,我们尝试将这个过程用矩阵表示出来:
现在我们相当于知道 点值矩阵 与 点矩阵,要求 系数矩阵。
同时乘上 的逆矩阵,也就是这样:
这就是 的矩阵表示,可以发现,这是个类似 的矩阵。
所以,我们只需要将所有 改为 就好了。
而根据定义:
所以,我们只要在 中带一个标记,就可以实现 的功能。
自己写的时候容易晕,注解一下各个循环在枚举什么,先上代码:
//The code is from dxbdlyinline void DFT(Complex f[], int len, int op) {Change(f, len);for(register int h = 2; h <= len; h <<= 1) {Complex step(cos(2 * Pi / h), op * sin(2 * Pi / h));for(register int i = 0; i < len; i += h) {Complex w(1, 0);for(register int k = i; k < i + h / 2; ++k) {Complex G = f[k], H = w * f[k + h / 2];f[k] = G + H, f[k + h / 2] = G - H;w = w * step;}}}if(op == -1) {for(register int i = 0; i < len; ++i) f[i].x /= len;}}
注意:
为标志变量,用于区分 与 。
第一层循环, 枚举的是当前合并层每一组的数量,其中 为单位复数。
第二层循环, 枚举的是每一组的编号起点
第三层循环, 枚举的是当前要求解的下标(另一个是 )
注意这里用了滚动数组,并没有新记录 函数与 函数。
一道模板题:
代码为了图方便封装过了的。
代码:
//The code is from dxbdly#include<bits/stdc++.h>using namespace std;inline int read() {int x = 0;char c = getchar();bool f = 0;while(!isdigit(c)) f |= (c == '-'), c = getchar();while(isdigit(c)) x = (x * 10) + (c ^ 48), c = getchar();return f ? -x : x;}namespace FFT {const double Pi = acos(-1);const int maxn = 3e6 + 5;int rev[maxn];struct Complex {double x, y;Complex(double x0 = 0, double y0 = 0) { x = x0, y = y0; }}g1[maxn], g2[maxn], sum[maxn];Complex operator + (Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }Complex operator - (Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }Complex operator * (Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }inline void Change(Complex f[], int len) {for(register int i = 0; i < len; ++i) {rev[i] = rev[i >> 1] >> 1;if(i & 1) rev[i] |= (len >> 1);}for(register int i = 0; i < len; ++i)if(i < rev[i]) swap(f[i], f[rev[i]]);}inline void DFT(Complex f[], int len, int op) {Change(f, len);for(register int h = 2; h <= len; h <<= 1) {Complex step(cos(2 * Pi / h), op * sin(2 * Pi / h));for(register int i = 0; i < len; i += h) {Complex w(1, 0);for(register int k = i; k < i + h / 2; ++k) {Complex G = f[k], H = w * f[k + h / 2];f[k] = G + H, f[k + h / 2] = G - H;w = w * step;}}}if(op == -1) {for(register int i = 0; i < len; ++i) f[i].x /= len;}}inline void Work(int n, int m, int num1[], int num2[]) {int len = 1;while(len < n * 2 || len < m * 2) len <<= 1;for(register int i = 0; i < n; ++i) g1[i] = Complex(num1[i], 0);for(register int i = 0; i < m; ++i) g2[i] = Complex(num2[i], 0);for(register int i = n; i < len; ++i) g1[i] = Complex(0, 0);for(register int i = m; i < len; ++i) g2[i] = Complex(0, 0);DFT(g1, len, 1), DFT(g2, len, 1);for(register int i = 0; i < len; ++i) sum[i] = g1[i] * g2[i];DFT(sum, len, -1);for(register int i = 0; i < n + m - 1; ++i) printf("%d ", (int)(sum[i].x + 0.5));}}const int maxn = 1e6 + 5;int n, m, num1[maxn], num2[maxn];int main() {n = read() + 1, m = read() + 1;for(register int i = 0; i < n; ++i) num1[i] = read();for(register int i = 0; i < m; ++i) num2[i] = read();FFT::Work(n, m, num1, num2);return 0;}
几个要注意的点:
几乎所有的操作都要求在 为 的整数幂下进行,所以需要对多项式先补
的复数类型很慢,这里直接自己写的,三种运算,应该算基础吧。
最后关于精度,四舍五入输出,大部分时候 够用。
刚学的 ,没做什么题,文章有不对的地方请私信博主。
最后, 是一种很重要的多项式算法,之后的许多多项式有关的算法都可以用 优化。
其中有许多算法思想更是值得我们学习。
解决 问题的算法还有很多,例如 快速数论变换(NTT),(任意模数 ),(快速沃尔什变换)
但这些算法都是以 为基础衍生而来,所以读者需要将 理解清楚后进行深入的学习
希望本文对大家有所帮助,感谢观看。