- 概念引入
- 点值表示
对于一个$n - 1$次多项式$A(x)$,可以通过确定$n$个点与值(即$x$和$y$)来表示这唯一的$A(x)$
- 复数
对于一元二次方程
$$x^2 + 1 = 0$$
在实数范围内无解,那么我们将实数范围扩充,就得到了复数,再令$i$为该方程的解,即
$$i^2 = - 1$$
那么就定义$z = a + bi$的数为复数,则有
当$b = 0$时,$z$为实数
当$b
eq 0$时,$z$为虚数
当$a = 0 && b
eq 0$时,$z$为纯虚数
其中,复数满足加法交换律、结合律、乘法分配率等
复数相乘:$z_1 = a_1 + b_1i, z_2 = a_2 + b_2i$,则$z_1 × z_2 = (a_1 + b_1i)(a_2 + b_2i) = (a_1a_2 - b_1b_2) + (a_1b_2 + b_1a_2)i$,它们相乘还是一个复数,在复平面上理解,就是模长相乘,幅角相加
共轭复数:当两个复数实部相同,虚部为相反数时,两个复数被称为共轭复数
对于一个复数$z = a + bi$,还可以在复数平面上用向量表示出来,即有$overrightarrow{OZ}$一一对应每个$z = a + bi$,那么就有复数的模等于$overline{Z}$,即$|z| = sqrt{a^2 + b^2}$
复数直接比较大小没有意义,除非它是一个实数
- $FFT$作用
那么,有了点值表示,对于多项式$A(x)$和$B(x)$相乘,就不需要$O(n^2)$,而只需要$O(n)$,因为$C(x_i) = A(x_i) * B(x_i)$,然后枚举$x_i$即可
于是现在的复杂度症结就在于将多项式转化成点值表示的$O(n^2)$了,于是就有$FFT$,可以实现在$O(n logn)$的时间内转化
- 离散傅里叶变换
于是傅里叶规定,点值中的$x$为模长为$1$的复数
至于为什么要取复数而不是实数,因为它有很神奇的性质
那么对于这$n$个复数的取法,取的是复平面上半径为$1$的一个圆,将其$n$等分后得到的点,令第$i$个点表示为$omega_n^k(0 le k le n - 1)$,那么这个点在复平面中的表示即为$(cosfrac{k}{n}2pi, sinfrac{k}{n}2pi)$,那么根据复数乘法在复平面上的意义为模长相乘,幅长相加,即$omega_n^k$相当于$(omega_n^1)^k$,那么称$omega_n^1$为单位根
我们就把这$omega_n^0, omega_n^1, ..., omega_n^{n - 1}$作为点值表示的$x$,称作离散傅里叶变换的结果
先给出关于傅里叶逆变换的结论:
- 将多项式$A(x)$的离散傅里叶变换结果作为系数代入多项式$B(x)$,再将每个离散傅里叶变换结果的倒数,即$omega_n^0, omega_n^{- 1}, ..., omega_n^{- (n - 1)}$作为$x$代入$B(x)$得到点值表示,那么这些表示除以$n$就得到了$A(x)$的原系数 [至于证明:不存在的]
这就是傅里叶变换神奇的性质
- $FFT$
有了傅里叶变换,虽然多项式与点值表示相互转化已经很轻松了,但是复杂度仍然不理想,就有了快速傅里叶变换
结论:
- $omega_{2n}^{2k} = omega_n^k$ [证明:代进原公式显然]
- $omega_n^{k + frac{n}{2}} = - omega_n^k$ [证明:关于复平面原点中心对称]
对于多项式$A(x) = sumlimits_{i = 0}^{n - 1} a_ix^i$,将它的奇偶项拆开,并将$x$转为$x^2$得到(此处先假定$n$为偶数)
当然此时代入的是$N(x^2), M(x^2)$,则有
此时就需要把$omega_n^k$代入,分情况讨论:
若$k < frac{n}{2}$,则有
反之
于是,我们只要求得${omega_{frac{n}{2}}^0, omega_{frac{n}{2}}^1, ..., omega_{frac{n}{2}}^{frac{n}{2} - 1}}$,就可以得到$A(x)$的所有关于所有离散傅里叶变换结果的点值表示了,可用分治实现,复杂度$O(n logn)$
- 代码(分治FFT)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 #include <cmath> 6 7 using namespace std; 8 9 const int MAXN = 2e06 + 10; 10 11 const double PI = acos (- 1.0); 12 13 int N, M; 14 15 struct mcomplex { 16 double x, y; 17 18 mcomplex () {} 19 mcomplex (double fx, double fy) : 20 x (fx), y (fy) {} 21 22 mcomplex operator + (const mcomplex& p) const { 23 return mcomplex (x + p.x, y + p.y); 24 } 25 mcomplex operator - (const mcomplex& p) const { 26 return mcomplex (x - p.x, y - p.y); 27 } 28 mcomplex operator * (const mcomplex& p) const { 29 return mcomplex (x * p.x - y * p.y, x * p.y + y * p.x); 30 } 31 } ; 32 mcomplex omega (int n, int k, int inv) { 33 return mcomplex (cos (2.0 * PI * (double) k / (double) n), 1.0 * inv * sin (2.0 * PI * (double) k / (double) n)); // 当一个复数的模长为1时,它的共轭复数即为它的倒数 34 } 35 mcomplex A[MAXN], B[MAXN]; 36 37 void FFT (mcomplex* a, int n, int inv) { // inv表示是否取倒数 38 if (n == 1) 39 return ; 40 mcomplex a1[n >> 1], a2[n >> 1]; 41 int m = n >> 1; 42 for (int i = 0; i <= n; i += 2) { 43 a1[i >> 1] = a[i]; 44 a2[i >> 1] = a[i + 1]; 45 } 46 FFT (a1, m, inv); 47 FFT (a2, m, inv); 48 for (int i = 0; i < m; i ++) { 49 mcomplex x = omega (n, i, inv); 50 a[i] = a1[i] + x * a2[i]; 51 a[i + m] = a1[i] - x * a2[i]; 52 } 53 } 54 55 int getnum () { 56 int num = 0; 57 char ch = getchar (); 58 59 while (! isdigit (ch)) 60 ch = getchar (); 61 while (isdigit (ch)) 62 num = (num << 3) + (num << 1) + ch - '0', ch = getchar (); 63 64 return num; 65 } 66 67 int main () { 68 N = getnum (), M = getnum (); 69 for (int i = 0; i <= N; i ++) 70 A[i].x = (double) getnum (); 71 for (int i = 0; i <= M; i ++) 72 B[i].x = (double) getnum (); 73 74 int n; 75 for (n = 1; n <= N + M; n <<= 1); 76 FFT (A, n, 1); 77 FFT (B, n, 1); 78 for (int i = 0; i <= n; i ++) 79 A[i] = A[i] * B[i]; 80 FFT (A, n, - 1); 81 for (int i = 0; i <= N + M; i ++) { 82 if (i) 83 putchar (' '); 84 printf ("%d", (int) (A[i].x / n + 0.5)); 85 } 86 puts (""); 87 88 return 0; 89 } 90 91 /* 92 1 2 93 1 2 94 1 2 1 95 */ 96 97 /* 98 5 5 99 1 7 4 0 9 4 100 8 8 2 4 5 5 101 */
- 蝴蝶操作
于是这样还是会超时,那么还需要优化
根据表格,有一个考眼力的性质
$|0 1 2 3|$
$|0 2 | 1 3|$
$|0 | 2 | 1 | 3|$
会发现每个数字的目标位置的二进制是原数的二进制翻转的结果,比如$1 = (01)_2, 2 = (10)_2$恰好是相对应的,于是就可以根据这个性质先将每个数排列到最终位置,再逐一合并
- 代码(迭代优化$FFT$)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 #include <cmath> 6 7 using namespace std; 8 9 const int MAXN = (1 << 22); 10 11 const double PI = acos (- 1.0); 12 13 int N, M; 14 15 struct mcomplex { 16 double x, y; 17 18 mcomplex () {} 19 mcomplex (double fx, double fy) : 20 x (fx), y (fy) {} 21 22 mcomplex operator + (const mcomplex& p) const { 23 return mcomplex (x + p.x, y + p.y); 24 } 25 mcomplex operator - (const mcomplex& p) const { 26 return mcomplex (x - p.x, y - p.y); 27 } 28 mcomplex operator * (const mcomplex& p) const { 29 return mcomplex (x * p.x - y * p.y, x * p.y + y * p.x); 30 } 31 } ; 32 mcomplex omega (int n, int k, int inv) { 33 return mcomplex (cos (2.0 * PI * (double) k / (double) n), 1.0 * inv * sin (2.0 * PI * (double) k / (double) n)); 34 } 35 mcomplex A[MAXN], B[MAXN]; 36 37 int oppo[MAXN]; 38 int limit; 39 void FFT (mcomplex* a, int inv) { 40 for (int i = 0; i < limit; i ++) 41 if (i < oppo[i]) 42 swap (a[i], a[oppo[i]]); 43 for (int mid = 1; mid < limit; mid <<= 1) { // 枚举区间长度的一半 44 mcomplex ome = mcomplex (cos (PI / (double) mid), inv * sin (PI / (double) mid)); // 单位根 45 for (int n = mid << 1, j = 0; j < limit; j += n) { 46 mcomplex x = mcomplex (1, 0); 47 for (int k = 0; k < mid; k ++, x = x * ome) { 48 mcomplex a1 = a[j + k], xa2 = x * a[j + k + mid]; // 蝴蝶操作 49 a[j + k] = a1 + xa2; 50 a[j + k + mid] = a1 - xa2; 51 } 52 } 53 } 54 } 55 56 int getnum () { 57 int num = 0; 58 char ch = getchar (); 59 60 while (! isdigit (ch)) 61 ch = getchar (); 62 while (isdigit (ch)) 63 num = (num << 3) + (num << 1) + ch - '0', ch = getchar (); 64 65 return num; 66 } 67 68 int main () { 69 N = getnum (), M = getnum (); 70 for (int i = 0; i <= N; i ++) 71 A[i].x = (double) getnum (); 72 for (int i = 0; i <= M; i ++) 73 B[i].x = (double) getnum (); 74 75 int n, lim = 0; 76 for (n = 1; n <= N + M; n <<= 1, lim ++); 77 for (int i = 0; i <= n; i ++) 78 oppo[i] = (oppo[i >> 1] >> 1) | ((i & 1) << (lim - 1)); 79 // 原来是左移,反转后改成右移,并且处理一下奇数原末尾的1 80 limit = n; 81 FFT (A, 1); 82 FFT (B, 1); 83 for (int i = 0; i <= n; i ++) 84 A[i] = A[i] * B[i]; 85 FFT (A, - 1); 86 for (int i = 0; i <= N + M; i ++) { 87 if (i) 88 putchar (' '); 89 printf ("%d", (int) (A[i].x / n + 0.5)); 90 } 91 puts (""); 92 93 return 0; 94 } 95 96 /* 97 1 2 98 1 2 99 1 2 1 100 */ 101 102 /* 103 5 5 104 1 7 4 0 9 4 105 8 8 2 4 5 5 106 */
- 总结
这样的$FFT$可以在$O(n logn)$的时间内求出多项式乘法的各项系数,主要流程:将两个多项式分别转化成点值表示 $dashrightarrow$ 通过点值表示将两个多项式合并 $dashrightarrow$ 通过离散傅里叶逆变换将点值表示转化成系数表示,即得解