浅谈快速傅里叶变换(FFT)
多项式乘法
我们定义两个次多项式,有:
其中表示两个多项式的系数
显然当一个多项式的系数被确定后这个多项式就被确定了,我们把这称之为多项式的系数表达
两个多项式可以相乘得到一个结果,比如:
我们令,因为中的只能由中的和中的相乘得到,所以我们有:
同样,这里的表示的系数
可以观察到按照这个定义式暴力计算的复杂度是级别的,很多时候难以接受,所以我们需要一种高效的算法
FFT
快速傅里叶变换(Fast Fourier Transfromation)简称FFT,可以实现在时间里计算多项式乘法
对于一个多项式,我们把它看作一个关于的函数,显然可以得到它的图像。带入一个值便可以得到一个对应的,便构成了一个有序数对表示点的坐标
结论:个点的坐标可以唯一确定一个次函数
感性证明:
对于一个次函数,我们带入个值得到有个方程的方程组,此时有个未知数(系数),解方程即可求出系数,系数确定函数(多项式)也随之确定
所以当我们有了对有序实数时,原多项式的表达式就唯一确定,这也就意味着我们可以用个有序实数对等效表示一个多项式,我们将这一组有序实数对称为这个多项式的点值表达
我们现在给出两个多项式,令
我们将数列分别带入,得到两组有序数对和
显然,而这个值就等于
将两组有序实数对都这样相乘,我们会得到一组新的有序实数对,而这组有序实数对就和对应,如此便可以求出的系数,从而确定的表达式。这就是FFT的核心思想:
将多项式转变为点值表达,相乘求出乘积多项式的点值表达,最后再转变为系数表达
将系数表达转化为点值表达的过程称为DFT
,将点值表达转化为系数表达的过程称为IDFT
复数
我们定义形如的数为复数,其中。我们定义为该复数的实部,为该复数的虚部
所有实数都可以在一条坐标轴上找到唯一一个对应的点,我们再添加一条竖着的数轴表示虚数,即可使所有复数同这个新形成的平面直角坐标系里的每个点一一对应:
比如上图中这个点即可表示复数
复数可以进行四则运算:
复数相加:实部和虚部分别相加
如:
复数相减:取相反数再相加
复数相乘:像多项式一样相乘
如:
注意这里
复数相除在FFT里暂时用不到,故暂不说明
这里我们需要注意复数相乘在刚才的平面直角坐标系里面的体现:
(该图来自command_block大佬/bx/bx)
我们定义该坐标系中一个点到原点的距离为这个复数的模长,这条连线与轴正半轴的夹角为这个复数的辐角
观察我们可以发现:复数相乘,模长相乘,辐角相加,证明很简单,代入计算即可
单位根
我们做DFT选择带入的值时,一般都会考虑一些常见好算的整数带入。理论上DFT的时候可以带入任意值,但是带入整数后的IDFT过程不好处理(详细证明见后),而傅里叶想到了带入单位根
我们定义方程的复数解为次单位根
根据“复数相乘,模长相乘,辐角相加”可以得到所有单位根的模长都是,换句话说就是所有单位根都在单位圆上。因此模长不会对结果造成影响,我们只考虑辐角
我们画出一个单位圆:
容易发现辐角大小为的复数都是次单位根
由代数基本定理:次方程在复数域内有且只有个根可知以上个复数即为所有单位根
显然所有单位根平分单位圆
我们用来表示逆时针看第个单位根,比如就表示这个点,所有次单位根为
当然我们也可以定义为顺时针旋转个圆周得到的单位根
令的辐角为,则该坐标显然为
单位根有几个性质:
1.对于任意都成立
显然
2.
证明:
根据“复数相乘,模长相乘,辐角相加”可知,每次乘上一个就相当于逆时针转动了个圆周,转动次就到达了第个单位根
3.
证明:
4.
证明:
5.,其中为常数
证明:
由于所有单位根平分单位圆,所以的辐角就是,与辐角相同
6.若为偶数,则
证明:
,显然辐角为,乘上它相当于旋转,到达关于原点的对称点,此时显然结果为,不懂可以画图看看
DFT
我们将一个次多项式(这里我们让,若不成立则在高位补)按照奇偶分成两块:
我们再定义两个多项式
显然有:
我们将带入上式:
我们再把代入上式:
发现什么?这两个式子只有一个符号的差距
所以说如果我们知道了和在的点值表示,代入上式即可在时间里求出的点值表示
我们再次观察上述过程,看到第一步:
我们将一个次多项式(这里)按照奇偶分成两块
这个过程是什么?分治
所以我们同样可以对和进行分治,递归处理,直到只剩下一项
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], tp[MAXN];
void dft(Complex *f, int len) {
if (len == 1) return;
Complex *fl = f, *fr = f + (len >> 1);
int i;
for (i = 0; i < len; ++i) tp[i] = f[i];
for (i = 0; i < (len >> 1); ++i) {
fl[i] = tp[i << 1];
fr[i] = tp[i << 1 | 1];
}
dft(fl, len >> 1);
dft(fr, len >> 1);
Complex w(cos(2 * pi / len), sin(2 * pi / len)), buf(1, 0);
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];
}
IDFT
IDFT就是把点值表示再转化为系数表示,也就是DFT的逆过程
我们令得到的点值数列为,那么有:
证明:
显然根据DFT
定义有:
所以有:
我们分类讨论:
若
则有:
若
令
则有:
令
则有
所以有:
所以式的值为,对结果不产生贡献
综上可知,式的值为,故原等式成立
由此我们再代入即可完成IDFT
此时我们再反观一开始的问题:为什么要代入单位根?因为单位根的性质使得DFT
和IDFT
变得十分简单,代入别的数难以达到这个效果
这个IDFT
的过程可以被理解为范德蒙德矩阵求逆,这也衍生出单位根反演,在此不多做赘述
(和DFT
几乎一样)
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], tp[MAXN];
void idft(Complex *f, int len) {
if (len == 1) return;
Complex *fl = f, *fr = f + (len >> 1);
int i;
for (i = 0; i < len; ++i) tp[i] = f[i];
for (i = 0; i < (len >> 1); ++i) {
fl[i] = tp[i << 1];
fr[i] = tp[i << 1 | 1];
}
idft(fl, len >> 1);
idft(fr, len >> 1);
Complex w(cos(2 * pi / len), -sin(2 * pi / len)), buf(1, 0);
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];//除以n的事放到主函数里面做
}
综合在一起,我们就得到了最初版本的FFT
:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], a[MAXN], b[MAXN], tp[MAXN];
void dft(Complex *f, int len) {
if (len == 1) return;
Complex *fl = f, *fr = f + (len >> 1);
int i;
for (i = 0; i < len; ++i) tp[i] = f[i];
for (i = 0; i < (len >> 1); ++i) {
fl[i] = tp[i << 1];
fr[i] = tp[i << 1 | 1];
}
dft(fl, len >> 1);
dft(fr, len >> 1);
Complex w(cos(2 * pi / len), sin(2 * pi / len)), buf(1, 0);
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];
}
void idft(Complex *f, int len) {
if (len == 1) return;
Complex *fl = f, *fr = f + (len >> 1);
int i;
for (i = 0; i < len; ++i) tp[i] = f[i];
for (i = 0; i < (len >> 1); ++i) {
fl[i] = tp[i << 1];
fr[i] = tp[i << 1 | 1];
}
idft(fl, len >> 1);
idft(fr, len >> 1);
Complex w(cos(2 * pi / len), -sin(2 * pi / len)), buf(1, 0);
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];
}
int n, m;
signed main() {
scanf("%d%d", &n, &m);
int i;
for (i = 0; i <= n; ++i) scanf("%lf", &a[i].x);
for (i = 0; i <= m; ++i) scanf("%lf", &b[i].x);
for (m += n, n = 1; n <= m; n <<= 1)
;
dft(a, n), dft(b, n);
for (i = 0; i < n; ++i) f[i] = a[i] * b[i];
idft(f, n);
for (i = 0; i <= m; ++i) printf("%d ", (int)(f[i].x / n + 0.5));//在这里除以n
return 0;
}
由于DFT
和IDFT
的代码差不多,所以我们可以把他们写到一起:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 5000005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], a[MAXN], b[MAXN], tp[MAXN];
void fft(Complex *f, int len, int flag) {
if (len == 1) return;
Complex *fl = f, *fr = f + (len >> 1);
int i;
for (i = 0; i < len; ++i) tp[i] = f[i];
for (i = 0; i < (len >> 1); ++i) {
fl[i] = tp[i << 1];
fr[i] = tp[i << 1 | 1];
}
fft(fl, len >> 1, flag);
fft(fr, len >> 1, flag);
Complex w(cos(2 * pi / len), flag * sin(2 * pi / len)), buf(1, 0);
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];
}
int n, m;
signed main() {
scanf("%d%d", &n, &m);
int i;
for (i = 0; i <= n; ++i) scanf("%lf", &a[i].x);
for (i = 0; i <= m; ++i) scanf("%lf", &b[i].x);
for (m += n, n = 1; n <= m; n <<= 1)
;
fft(a, n, 1), fft(b, n, 1);
for (i = 0; i < n; ++i) f[i] = a[i] * b[i];
fft(f, n, -1);
for (i = 0; i <= m; ++i) printf("%d ", (int)(f[i].x / n + 0.5));
return 0;
}
以上代码可以通过洛谷P3803 【模板】多项式乘法(FFT)
优化
我们可以观察到上述代码使用递归实现FFT的速度并不尽如人意,所以我们要优化
首先观察分治的过程:
我们发现分治到最后某个数的下标就是原来的下标在二进制意义下翻转的结果,所以我们可以用一个类似数位dp的东西来预处理出每个数在分治序列中的下标,从而避免了递归处理
具体地说,我们令为递归后原数列中所在的位置。我们把拆成最后一位和前面所有位两部分,显然就是最后一位加上前面所有位的翻转
所以有:
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (len - 1))
同时我们观察这部分代码:
for (i = 0; i < (len >> 1); ++i) {
tp[i] = fl[i] + buf * fr[i];
tp[i + (len >> 1)] = fl[i] - buf * fr[i];
buf = buf * w;
}
for (i = 0; i < len; ++i) f[i] = tp[i];
拷贝数组的常数会比较大,所以我们可以一边做一边拷贝:
for (i = 0; i < (len >> 1); ++i) {
Complex t = buf * fr[i];
f[i + (len >> 1)] = fl[i] - t;
f[i] = fl[i] + t;//注意赋值的顺序
buf = buf * w;
}
同时还可以迭代实现:
void fft(Complex *f, int flag) {
int i, j, k;
for (i = 0; i < n; ++i)
if (i < r[i]) swap(f[i], f[r[i]]);//预处理位置
for (int len = 2; len <= n; len <<= 1) {//由小到大合并而不是由大到小递归
Complex w(cos(2 * pi / len), flag * sin(2 * pi / len));
for (j = 0; j < n; j += len) {
Complex buf(1, 0);
for (k = j; k < j + (len >> 1); ++k) {
Complex t = buf * f[k + (len >> 1)];
f[k + (len >> 1)] = f[k] - t;
f[k] = f[k] + t;
buf = buf * w;
}
}
}
}
完整版(也是最常见的)FFT代码:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 5000005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], a[MAXN], b[MAXN], tp[MAXN];
int n, m;
int r[MAXN];
void fft(Complex *f, int flag) {
int i, j, k;
for (i = 0; i < n; ++i)
if (i < r[i]) swap(f[i], f[r[i]]);
for (int len = 2; len <= n; len <<= 1) {
Complex w(cos(2 * pi / len), flag * sin(2 * pi / len));
for (j = 0; j < n; j += len) {
Complex buf(1, 0);
for (k = j; k < j + (len >> 1); ++k) {
Complex t = buf * f[k + (len >> 1)];
f[k + (len >> 1)] = f[k] - t;
f[k] = f[k] + t;
buf = buf * w;
}
}
}
}
signed main() {
scanf("%d%d", &n, &m);
int i, cnt = 0;
for (i = 0; i <= n; ++i) scanf("%lf", &a[i].x);
for (i = 0; i <= m; ++i) scanf("%lf", &b[i].x);
for (m += n, n = 1; n <= m; n <<= 1) ++cnt;
for (i = 0; i < n; ++i) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (cnt - 1));
fft(a, 1), fft(b, 1);
for (i = 0; i < n; ++i) f[i] = a[i] * b[i];
fft(f, -1);
for (i = 0; i <= m; ++i) printf("%d ", (int)(f[i].x / n + 0.5));
return 0;
}
可以看到优化后效果还是很明显的
例题
洛谷P1919 【模板】A*B Problem升级版
给出两个正整数,求的值
我们可以把一个位正整数看成一个次的的多项式,然后直接乘起来就可以了
#include <bits/stdc++.h>
using namespace std;
#define MAXN 3000005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f[MAXN], a[MAXN], b[MAXN], tp[MAXN];
int n, m;
int r[MAXN];
void fft(Complex *f, int flag) {
int i, j, k;
for (i = 0; i < n; ++i)
if (i < r[i]) swap(f[i], f[r[i]]);
for (int len = 2; len <= n; len <<= 1) {
Complex w(cos(2 * pi / len), flag * sin(2 * pi / len));
for (j = 0; j < n; j += len) {
Complex buf(1, 0);
for (k = j; k < j + (len >> 1); ++k) {
Complex t = buf * f[k + (len >> 1)];
f[k + (len >> 1)] = f[k] - t;
f[k] = f[k] + t;
buf = buf * w;
}
}
}
}
int idx = 0;
int ans[MAXN];
char s1[MAXN], s2[MAXN];
signed main() {
scanf("%s%s", s1, s2);
int i, cnt = 0;
n = strlen(s1), m = strlen(s2);
--n, --m;
for (i = n; i >= 0; --i) a[n - i].x = (s1[i] - 48) * 1.0;
for (i = m; i >= 0; --i) b[m - i].x = (s2[i] - 48) * 1.0;
for (m += n, n = 1; n <= m; n <<= 1) ++cnt;
for (i = 0; i < n; ++i) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (cnt - 1));
fft(a, 1), fft(b, 1);
for (i = 0; i < n; ++i) f[i] = a[i] * b[i];
fft(f, -1);
for (i = 0; i <= n; ++i) {
ans[i] += (int)(f[i].x / n + 0.5);
if (ans[i] >= 10) {
if (i == n) ++n;
ans[i + 1] += ans[i] / 10;
ans[i] %= 10;
}
}
while (!ans[n] && n) --n;
++n;
while (n--) printf("%d", ans[n]);
return 0;
}
[ZJOI2014]力
给出个数令,对于每个求出
令
我们把序列翻转一下,令新序列的
发现前后两个和式都是卷积的形式,直接FFT即可
#include <bits/stdc++.h>
using namespace std;
#define MAXN 1000005
const double pi = acos(-1);
class Complex {
public:
Complex(double xx = 0, double yy = 0) { x = xx, y = yy; }
double x, y;
Complex operator+(Complex const &B) const {
return Complex(x + B.x, y + B.y);
}
Complex operator-(Complex const &B) const {
return Complex(x - B.x, y - B.y);
}
Complex operator*(Complex const &B) const {
return Complex(x * B.x - y * B.y, x * B.y + y * B.x);
}
} f1[MAXN], f2[MAXN], a[MAXN], b[MAXN], tp[MAXN];
int n, m;
int r[MAXN];
void fft(Complex *f, int flag) {
int i, j, k;
for (i = 0; i < n; ++i)
if (i < r[i]) swap(f[i], f[r[i]]);
for (int len = 2; len <= n; len <<= 1) {
Complex w(cos(2 * pi / len), flag * sin(2 * pi / len));
for (j = 0; j < n; j += len) {
Complex buf(1, 0);
for (k = j; k < j + (len >> 1); ++k) {
Complex t = buf * f[k + (len >> 1)];
f[k + (len >> 1)] = f[k] - t;
f[k] = f[k] + t;
buf = buf * w;
}
}
}
}
signed main() {
scanf("%d", &n);
int i, cnt = 0;
for (i = 1; i <= n; ++i) {
scanf("%lf", &tp[i].x);
a[i].x = (double)(1.0 / i / i);
b[i].x = tp[i].x;
}
reverse(tp + 1, tp + n + 1);
int t = n;
for (n = 1; n < (t << 1); n <<= 1) ++cnt;
for (i = 0; i < n; ++i) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (cnt - 1));
fft(a, 1), fft(b, 1), fft(tp, 1);
for (i = 0; i <= n; ++i) {
f1[i] = a[i] * b[i];
f2[i] = a[i] * tp[i];
}
fft(f1, -1), fft(f2, -1);
for (i = 1; i <= t; ++i) {
printf("%lf\n", (f1[i].x / n) - (f2[t - i + 1].x / n));
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步