前言

快速傅里叶变换(\(\text{Fast Fourier Transform,FFT}\) )是一种能在\(O(n \log n)\)的时间内完成多项式乘法的算法,在\(OI\)中的应用很多,是多项式相关内容的基础。下面从头开始介绍\(\text{FFT}\)。

前置技能:弧度制、三角函数、平面向量。

多项式

形如\(f(x)=a_0+a_1x+a_2x^2+...+a_nx^n\)的式子称为\(x\)的\(n\)次多项式。其中\(a_0,a_1,...,a_n\)称为多项式的系数。

系数表达法

上面定义中的表示就是系数表达法。其系数可看成\(n+1\)维向量\(\vec a=(a_0,a_1,...,a_n)\)。

点值表达法

把多项式看成一个函数,点值表示就用它图像上的\(n+1\)个不同的点\((x_0,y_0),...,(x_n,y_n)\)来确定这个多项式。多项式有不止一个点值表示,可以证明每个点值表示确定唯一的系数表达多项式。

复数

虚数单位

\(i\)被称为虚数单位。规定\(i=\sqrt {-1}\)。

复平面

复数的平面由\(x,y\)轴组成。\(x\)轴称为实轴,\(y\)轴称为虚轴。平面内的每一个从原点到某个点\((a,b)\)的向量\(\vec a=(a,b)\)表示复数\(a+bi\).

复数的模长:\(\sqrt {a^2+b^2}\).实轴到复数向量的转角\(\theta\)称为幅角。

复数的基本运算

  • 复数的加(减)法:\((a+bi)+(c+di)=(a+c)+(b+d)i\)
  • 复数的乘法:\((a+bi)(c+di)=(ac-bd)+(bc+ad)i\)
  • 一个结论:复数乘法,模长相乘,幅角相加。可以用下面将提到欧拉公式证明。

共轭复数

\(a+bi\)与\(a-bi\)互为共轭复数。

单位根

\(n\)次单位根是满足\(z^n=1\)的\(n\)个复数,它们均分复平面的单位圆。

这些复数满足模长为\(1\),幅角的\(n\)倍是\(2\pi\)的倍数

根据欧拉公式:

欧拉公式:\(e^{xi}=\cos x+i \sin x\),其中\(e\)为自然对数的底数,\(i\)为虚数单位。

(欧拉公式的证明可以使用泰勒级数)

可得\(n\)次单位根为\(e^{\frac{2\pi ki}{n}},k\in [0,n-1]\)

得:记\(\omega_n=e^{\frac{2\pi i}{n}},\)则\(n\)次单位根为\(\omega_n^0,...,\omega_n^{n-1}\)

单位根的性质

性质\(1\):根据定义得到:\(\omega_{2n}^{2k}=\omega_{n}^{k}\)(被叫做折半定理,是消去定理的特殊情形)

性质\(2\):\(\omega_{n}^{\frac{n}{2}+k}=-\omega_n^k\)

证明:

\(\omega_{n}^{\frac{n}{2}}=e^{\frac{2\pi i}{n} \frac{n}{2}}=e^{\pi i}=\cos \pi+i \sin \pi=-1\)

\(\omega_{n}^{\frac{n}{2}+k}=\omega_{n}^{\frac{n}{2}}\omega_{n}^{k}=-\omega_n^k\)

Fast Fourier Transform

多项式乘法

系数表达的多项式乘法:\(c(x)=a(x)b(x)\),则\(c(x)=\sum_{i=0}^{2n} c_i x^i\)

其中\(c_i=\sum_{j=0}^{n}a_jb_{i-j}\).

时间复杂度\(O(n^2)\)

点值表达的多项式乘法:

时间复杂度\(O(n)\)

因此多项式乘法的基本思路是先插值得到点值表达,再\(O(n)\)乘,最后求值得到系数表达。

DFT

把\(n\)次单位根\(\omega_n^0,...,\omega_n^{n-1}\)带入多项式\(A(x)=a_0+a_1x+...+a_nx^n\),

得到点值向量\(\vec y=(A(\omega_n^0),A(\omega_n^1),...,A(\omega_n^{n-1}))\),

称为系数向量\(\vec a=(a_0,a_1,...,a_n)\)的离散傅里叶变换(\(\text{Discrete Fourier Transform, DFT}\)),写作\(\vec y=\text{DFT}_n(\vec a)\)。

直接求\(\text{DFT}\)是\(O(n^2)\)的。\(\text{FFT}\)的常用算法\(\text{Cooley-Tukey}\)使用分治方法做到\(O(n\log n)\).

以下讨论基于\(n=2^m,m \in N^*\),若不足则高位系数补\(0\).

考虑点值向量的第\(k+1\)维:(注意这里最高次是\(n-1\))

\(A(\omega_{n}^{k})=\sum_{i=0}^{n-1}a_i(\omega_{n}^{k})^{i}=\sum_{i=0}^{n-1}a_i\omega_{n}^{ki}\)

\(=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\omega_{n}^{2ki}+\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\omega_{n}^{2ki+k}\)

\(=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\omega_{n}^{2ki}+\omega_{n}^{k}\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\omega_{n}^{2ki}\)

利用性质1:\(\omega_{2n}^{2k}=\omega_{n}^{k}\)

当\(k<\frac{n}{2}\)时:

\(A(\omega_{n}^{k})=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\omega_{\frac{n}{2}}^{ki}+\omega_{n}^{k}\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\omega_{\frac{n}{2}}^{ki}\)

利用性质2:\(\omega_{n}^{\frac{n}{2}+k}=-\omega_n^k\)

可以推出:

\(A(\omega_{n}^{k+\frac{n}{2}})=(-1)^{\frac{n}{2}}\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\omega_{\frac{n}{2}}^{ki}+(-1)^{\frac{n}{2}}\omega_{n}^{k+\frac{n}{2}}\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\omega_{\frac{n}{2}}^{ki}\)

\(A(\omega_{n}^{k+\frac{n}{2}})=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\omega_{\frac{n}{2}}^{ki}-\omega_n^k\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\omega_{\frac{n}{2}}^{ki}\)

上面把求和分成\(0,2,...,n-2\)与\(1,3,...,n-1\)两部分,把大小为\(n\)的问题转化成两个规模为\(\frac{n}{2}\)的子问题,可以进行分治求解了。

IDFT

求值过程使用离散傅里叶逆变换(\(\text{Inverse Discrete Fourier Transform, IDFT}\))

结论:只要把\(\text{DFT}\)的\(\omega_n\)都取倒数(共轭复数),最后除以\(n\)即可。

证明:

设\(\vec Y=(y_0,y_1,...,y_n)\)为\(\vec A = (a_0,a_1,...,a_n)\)的离散傅里叶变换。

考虑一个向量:\(\vec C=(c_0,c_1,...,c_n)\)满足\(c_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i\)

(即\(\vec C\)是多项式\(\vec Y\)在\(\omega_n^0,\omega_n^{-1},...,\omega_n^{-(n-1)}\)处的点值)

将上式展开:

\(c_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i\)

\(=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^i)^j)(\omega_n^{-k})^i\)

\(=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^j)^i)(\omega_n^{-k})^i\)

\(=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega_n^j)^i(\omega_n^{-k})^i\)

\(=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega_n^{j-k})^i\)

\(=\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i)\)

考虑一个前缀和\(S(\omega_n^k)=1+\omega_n^k+(\omega_n^k)^2+...+(\omega_n^k)^{n-1}\)。

当\((\omega_n^k)\not = 1\)即\(k\not = 0\)时,使用等比数列求和方法:

\(\omega_n^kS(\omega_n^k)=\omega_n^k+(\omega_n^k)^2+(\omega_n^k)^3+...+(\omega_n^k)^{n}\)

\(\omega_n^kS(\omega_n^k)-S(\omega_n^k)=(\omega_n^k)^{n}-1\)

\(S(\omega_n^k)=\frac{(\omega_n^k)^{n}-1}{\omega_n^k-1}\)

分母不为\(0\),分子\((\omega_n^k)^{n}-1=(\omega_n^n)^{k}-1=1^{k}-1=0\)

因此\(k\not = 0\)时,\(S(\omega_n^k)=0\)

\(k=0\)时,\(S(\omega_n^k)=n(\omega_n^k)^0=n\)

继续考虑刚刚那个式子

\(c_k=\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i)\)

只有\(j-k=0\)时\(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i\)才为\(n\),否则为\(0\)。

\(c_k=a_kn\)

得到结论:\(a_k=\frac{c_k}{n}\)。

再次总结一下,离散傅里叶逆变换就是先求出多项式在\(\omega_n^0,\omega_n^{-1},...,\omega_n^{-(n-1)}\)处的点值表示,再每一项除以\(n\)。

递归版代码

按照如上所述的方法可以轻松写出一份递归代码。

//Luogu P3803 多项式乘法
#include <complex>
#include <cstdio>
using namespace std;

typedef complex<double> comp;

const int N = (1 << 20) + 10 << 1;
const double PI2 = 2.0 * acos(-1.0);

int read() {
    int x = 0; char c = getchar();
    for(; c < '0' || c > '9'; c = getchar()) ;
    for(; c >= '0' && c <= '9'; c = getchar())
        x = x * 10 + (c & 15);
    return x;
}

int n, m;
comp a[N], b[N];

void fft(int n, comp * a, int type) {
    if(n == 1) return ;
    comp a1[n >> 1], a2[n >> 1];
    for(int i = 0; i < n; i += 2)
        a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];
    fft(n >> 1, a1, type), fft(n >> 1, a2, type);
    comp w(1, 0), wn(cos(PI2 / n), type * sin(PI2 / n));
    for(int i = 0; i < n >> 1; i ++, w *= wn)
        a[i] = a1[i] + w * a2[i],
        a[i + (n >> 1)] = a1[i] - w * a2[i];
}

int main() {
    n = read(), m = read();
    for(int i = 0; i <= n; i ++) a[i] = read();
    for(int i = 0; i <= m; i ++) b[i] = read();

    int lim = 1;
    for(; lim <= n + m; lim <<= 1) ;

    fft(lim, a, 1), fft(lim, b, 1);
    for(int i = 0; i <= lim; i ++) a[i] *= b[i];
    fft(lim, a, -1);

    for(int i = 0; i <= n + m; i ++)
        printf("%d ", (int)(0.5 + a[i].real() / lim));
    return 0;
}

迭代优化

本来\(\text{double}\)常数就大,加上递归就卡爆了啊(\(qwq\),因此考虑使用迭代写法。

通过观察得到:多项式的\(i\)次项到分治边界时下标为\(r[i]\),\(r[i]\)为\(i\)二进制翻转后的数

然后就可以自底向上迭代做,常数大概是递归版的\(1/4\)

//Luogu P3803 多项式乘法 - 迭代FFT
#include <complex>
#include <cstdio>
using namespace std;

typedef complex<double> comp;

const int N = (1 << 21) + 10;
const double PI = acos(-1);

int read() {
    int x = 0; char c = getchar();
    for(; c < '0' || c > '9'; c = getchar()) ;
    for(; c >= '0' && c <= '9'; c = getchar())
        x = x * 10 + (c & 15);
    return x;
}

int n, m, lim, r[N];
comp a[N], b[N];

void fft(comp * a, int type) {
    for(int i = 0; i < lim; i ++)
        if(i < r[i]) swap(a[i], a[r[i]]);
    for(int i = 1; i < lim; i <<= 1) {
        comp x(cos(PI / i), type * sin(PI / i));
        for(int j = 0; j < lim; j += (i << 1)) {
            comp y(1, 0);
            for(int k = 0; k < i; k ++, y *= x) {
                comp p = a[j + k], q = y * a[j + k + i];
                a[j + k] = p + q; a[j + k + i] = p - q;
            }
        }
    }
}

int main() {
    n = read(), m = read();
    for(int i = 0; i <= n; i ++) a[i] = read();
    for(int i = 0; i <= m; i ++) b[i] = read();

    int l = 0;
    for(lim = 1; lim <= n + m; lim <<= 1) ++ l;
    for(int i = 0; i < lim; i ++)
        r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));

    fft(a, 1), fft(b, 1);
    for(int i = 0; i <= lim; i ++) a[i] *= b[i];
    fft(a, -1);

    for(int i = 0; i <= n + m; i ++)
        printf("%d ", (int)(0.5 + a[i].real() / lim));
    return 0;
}

结语

我可能有些过程写的比较详细和冗长,因为dalao们的博客总是省略一些步骤让我思考半天qwq

参考博客:

「学习笔记」Fast Fourier Transform的更多相关文章

  1. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  2. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  3. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  4. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  5. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  6. 「学习笔记」FFT及NTT入门知识

    前言 快速傅里叶变换(\(\text{Fast Fourier Transform,FFT}\) )是一种能在\(O(n \log n)\)的时间内完成多项式乘法的算法,在\(OI\)中的应用很多,是 ...

  7. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  8. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  9. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

随机推荐

  1. POJ 1577 Falling Leaves(二叉搜索树)

    思路:当时学长讲了之后,似乎有点思路----------就是倒着建一个  二叉搜索树 代码1:超时 详见超时原因 #include<iostream> #include<cstrin ...

  2. bzoj3573米特运输

    题意: 给定一棵树上的边和点权 改动点权使得每个父节点u容量为子节点容量的d[u](子节点个数)倍 考察点: 1.这是一道语文题 2.点权很大 直接算会爆 有一种优化办法:取log(醉 这是什么优化) ...

  3. bzoj 3439: Kpm的MC密码 Trie+动态开点线段树

    题目大意: http://www.lydsy.com/JudgeOnline/problem.php?id=3439 题解: 首先我们发现这道题要查的是后缀不是前缀. 如果查前缀就可以迅速查找到字符串 ...

  4. 【VS】VS开发中遇到的问题的总结

    1.  VS中经常会出现无法解析的外部符号,还有LINK ERROR 2019等 这类问题如果检查代码没有错误,很大概率就是lib文件错误.调试程序找出问题函数,再找出问题函数使用到的lib文件,在项 ...

  5. 如何自动生成和安装requirements.txt依赖

    在查看别人的Python项目时,经常会看到一个requirements.txt文件,里面记录了当前程序的所有依赖包及其精确版本号.这个文件有点类似与Rails的Gemfile.其作用是用来在另一台PC ...

  6. Operating System-Process(1)什么是进程&&进程的创建(Creation)&&进程的终止(Termination)&&进程的状态(State)

    本文阐述操作系统的核心概念之一:进程(Process),主要内容: 什么是进程 进程的创建(Creation) 进程的终止(Termination) 进程的状态(State) 一.什么是进程 1.1 ...

  7. 理解Promise

    一.Propmise基本用法 Promise用于发送一个异步完成的结果,是替代回调函数的另一种选择.可以把Promise理解为一种异步函数. 以下函数通过一个Promise来异步地返回一个结果 fun ...

  8. pip3 更改安装源

    经常在使用Python的时候需要安装各种模块,而pip是很强大的模块安装工具,但是由于国外官方pypi经常被墙,导致不可用,所以我们最好是将自己使用的pip源更换一下,这样就能解决被墙导致的装不上库的 ...

  9. python中如何定义main方法

    我们有时写的python模块需要自己测试, 简单方法就是定义main函数, 然后测试自己的模块接口. def main():      test_yourCode() if __name__ == & ...

  10. C++STL库中map容器常用应用

    #include<iostream> #include<cstdio> #include<map> //按键值大小构成二叉搜索树 using namespace s ...