Fast Fourier Transform
This is a classic problem in CS – to multiple two degree-n polynomials. The brute force way is obvious, but would cost us O(n2) time. Using the idea of FFT would enable us to do this in O(nlogn) time.
In this blog entry, I will be focusing on the theoretic proof of the FFT algorithm that is so widely used, serving to deepen my own understanding about the topic. Of course, it will be heartening to see if anyone find this article helpful in any way.
Catalog
Prerequisite knowledge
Polynomials in point value form
For a polynomial of degree-n f(x)= ∑ i = 0 n a i x i \sum_{i=0}^na_ix^i ∑i=0naixi, we choose n arbitrary values of x and use the n pairs of (xi,f(xi)) to represent the polynomial. Recall from the gauss’s system of linear equations, the n pair of values is just sufficient for us to figure out what the original polynomial is exactly.
You may have noticed that using the point value form of polynomial greatly reduced the number of operations required for multiplication. Suppose we need to calculate F(x) which is the product of two polynomials f(x) and g(x),then if we have the n pairs of (xi,f(xi)) and (xi,g(xi)), we can easily derive the corresponding point value form of F(x) which would be (xi,f(xi)g(xi)) in O(n) time. But at what cost? Computing the n values of f(x) alone would require O(n2) time, and we still need F(x) in the coefficient form, and solving the n simultaneous equation would require us O(n3) time by Gauss’s method.
Root of unity
However, notice that we are free to choose any n arbitrary value of x to substitute into the polynomials, and of course we are to choose some special values. In this case, we are to choose n evenly spaced out point on the unit circle of the complex plane, which are the n complex roots of the equation xn=1(recall that in the multiplication of complex numbers can be seen as rotation of the point about the origin on the complex plane, and these n points after rotating n times by angle equal to their argument will end up on the positive side of the real axis with modulus of 1).Let
ω
n
=
e
2
π
i
n
\omega_n=e^{\frac{2\pi i}{n}}
ωn=en2πi, it is not hard to see this is the first root above the positive real axis, and we can express the whole set of the n roots as
{
ω
n
k
∣
k
=
0
,
1
⋯
,
n
−
1
{\omega_n^k\mid k=0,1\cdots,n-1}
ωnk∣k=0,1⋯,n−1}, we call them the nth roots of unity, and they satisfy these beautiful symmetric properties:
- ω n k = ω n k + n \omega_n^k=\omega_{n}^{k+n} ωnk=ωnk+n(going another full circle will not affect anything)
- ω n k = ω 2 n 2 k \omega_n^k=\omega_{2n}^{2k} ωnk=ω2n2k (dividing the unit circle into n equal portions and take the kth crack is equivalent to the 2kth crack if the unit circle is divided into 2n equivalent portions instead)
- − ω 2 n k = ω 2 n k + n -\omega_{2n}^k=\omega_{2n}^{k+n} −ω2nk=ω2nk+n(taking the negative value is just reflecting the point through the origin, and the two would differ by an angle of π \pi π, thus we need n 2 n \frac{n}{2n} 2nn of the circle to make same equal)
We are to use these n values in our transformation.
Discrete Fourier Transform
The first step is to convert our f(x) and g(x) from coefficient form into point value form. Take f(x) as an example, the idea is to separate the odd and even powers, and use the idea of Divide and Conquer.
For example, for a polynomial with degree n-1(we assume n is a power of 2 here, as for the missing power we can just assume the coefficient is 0)
suppose we have
f
(
x
)
=
∑
i
=
0
n
a
i
x
i
let
o
d
d
(
x
)
=
∑
i
=
0
n
2
a
2
∗
i
+
1
x
i
,
e
v
e
n
(
x
)
=
∑
i
=
0
n
2
a
2
∗
i
x
i
,
Then we have
f
(
x
)
=
e
v
e
n
(
x
2
)
+
x
∗
o
d
d
(
x
2
)
\begin{aligned} \text{suppose we have }f(x)=&\sum_{i=0}^n a_ix^i\\ \text{let }odd(x)=&\sum_{i=0}^{\frac{n}{2}}a_{2*i+1}x^i,\\ even(x)=&\sum_{i=0}^{\frac{n}{2}}a_{2*i}x^i,\\ \text{Then we have }f(x)=&even(x^2)+x*odd(x^2)\\ \end{aligned}
suppose we have f(x)=let odd(x)=even(x)=Then we have f(x)=i=0∑naixii=0∑2na2∗i+1xi,i=0∑2na2∗ixi,even(x2)+x∗odd(x2)
Now we can substitute in our nth roots of unity:
f
(
ω
n
k
)
=
e
v
e
n
(
(
ω
n
k
)
2
)
+
ω
n
k
∗
o
d
d
(
(
ω
n
k
)
2
)
=
e
v
e
n
(
ω
n
2
k
)
+
ω
n
k
∗
o
d
d
(
ω
n
2
k
)
=
e
v
e
n
(
ω
n
2
k
)
+
ω
n
k
∗
o
d
d
(
ω
n
2
k
)
...from(2)
f
(
ω
n
k
+
n
2
)
=
e
v
e
n
(
(
ω
n
k
+
n
2
)
2
)
+
ω
n
k
+
n
2
∗
o
d
d
(
(
ω
n
k
+
n
2
)
2
)
=
e
v
e
n
(
ω
n
2
k
+
n
)
+
ω
n
k
+
n
2
∗
o
d
d
(
ω
n
2
k
+
n
)
=
e
v
e
n
(
ω
n
2
k
)
−
ω
n
k
∗
o
d
d
(
ω
n
2
k
)
...from(1),(3)
=
e
v
e
n
(
ω
n
2
k
)
−
ω
n
k
∗
o
d
d
(
ω
n
2
k
)
...from(2)
\begin{aligned} f(\omega_n^k)&=even((\omega_n^k)^2)+\omega_n^k*odd((\omega_n^k)^2)\\ &=even(\omega_n^{2k})+\omega_n^k*odd(\omega_n^{2k})\\ &=even(\omega_{\frac{n}{2}}^k)+\omega_n^k*odd(\omega_{\frac{n}{2}}^k)\text{...from(2)}\\ f(\omega_n^{k+\frac{n}{2}})&=even((\omega_n^{k+\frac{n}{2}})^2)+\omega_n^{k+\frac{n}{2}}*odd((\omega_n^{k+\frac{n}{2}})^2)\\ &=even(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}*odd(\omega_n^{2k+n})\\ &=even(\omega_n^{2k})-\omega_n^k*odd(\omega_n^{2k})\text{...from(1),(3)}\\ &=even(\omega_{\frac{n}{2}}^k)-\omega_n^k*odd(\omega_{\frac{n}{2}}^k)\text{...from(2)} \end{aligned}
f(ωnk)f(ωnk+2n)=even((ωnk)2)+ωnk∗odd((ωnk)2)=even(ωn2k)+ωnk∗odd(ωn2k)=even(ω2nk)+ωnk∗odd(ω2nk)...from(2)=even((ωnk+2n)2)+ωnk+2n∗odd((ωnk+2n)2)=even(ωn2k+n)+ωnk+2n∗odd(ωn2k+n)=even(ωn2k)−ωnk∗odd(ωn2k)...from(1),(3)=even(ω2nk)−ωnk∗odd(ω2nk)...from(2)
Thus, we only need to compute
e
v
e
n
(
ω
n
2
k
)
even(\omega^k_\frac{n}{2})
even(ω2nk) and
o
d
d
(
ω
n
2
k
)
odd(\omega^k_\frac{n}{2})
odd(ω2nk) to derive the values of
f
(
ω
n
k
)
f(\omega_n^k)
f(ωnk) and
f
(
ω
n
k
+
n
2
)
f(\omega_n^{k+\frac{n}{2}})
f(ωnk+2n), and we can carry out this process recursively for the odd and even functions as well. For each recursion, we convert the computation of a degree-n polynomial into 2 degree-
n
2
\frac{n}{2}
2n polynomials, and we only compute half the number of point values (
ω
n
k
\omega_n^k
ωnk) for both the 2 degree-
n
2
\frac{n}{2}
2n polynomials since the other half (
ω
n
k
+
n
2
\omega_n^{k+\frac{n}{2}}
ωnk+2n) just differ by a plus/minus sign. Thus, we need
2
∗
1
2
∗
n
=
n
2*\frac{1}2*n=n
2∗21∗n=n operations for each round of recursion, and after
l
o
g
2
n
log_2n
log2n rounds, we will end up with n degree-1 polynomials and we only need to substitute 1 into each one of them. This would give us a time complexity of O
(
n
l
o
g
n
)
(nlogn)
(nlogn).
This process is known as the Discrete Fourier Transform(DFT).
Inverse Discrete Fourier Transform
The second part of the FFT algorithm would be reverting back from point value form to coefficient form. As you might have guessed, the process of Inverse Discrete Fourier Transform(IDFT) is largely similar to that of DFT.
In fact, if we see DFT as a linear transformation from the vector of coefficients {
a
0
,
a
1
,
a
2
,
.
.
.
,
a
n
−
1
a_0,a_1,a_2,...,a_{n-1}
a0,a1,a2,...,an−1} to the vector of the n point values {
f
(
x
0
)
,
f
(
x
1
)
,
f
(
x
2
)
,
.
.
.
,
f
(
x
n
−
1
)
f(x_0),f(x_1),f(x_2),...,f(x_{n-1})
f(x0),f(x1),f(x2),...,f(xn−1)}, we are essentially multiplying the coefficient vector by an n
×
\times
×n matrix. In fact, the brute force way of substituting each of the n values one by one is essentially matrix multiplication, and our DFT just simplifies the process by using some special properties of root of unity(and by Divide and Conquer).
[
f
(
x
0
)
f
(
x
1
)
f
(
x
2
)
f
(
x
3
)
⋮
f
(
x
n
−
1
)
]
=
[
1
1
1
1
⋯
1
1
ω
n
1
ω
n
2
ω
n
3
⋯
ω
n
n
−
1
1
ω
n
2
ω
n
4
ω
n
6
⋯
ω
n
2
(
n
−
1
)
1
ω
n
3
ω
n
6
ω
n
9
⋯
ω
n
3
(
n
−
1
)
⋮
⋮
⋮
⋮
⋱
⋮
1
ω
n
n
−
1
ω
n
2
(
n
−
1
)
ω
n
3
(
n
−
1
)
⋯
ω
n
(
n
−
1
)
2
]
[
a
0
a
1
a
2
a
3
⋮
a
n
−
1
]
\begin{bmatrix}f(x_0 )\\ f(x_1) \\ f(x_2) \\ f(x_3 )\\ \vdots \\ f(x_{n-1}) \end{bmatrix}= \begin{bmatrix}1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n-1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n-1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \omega_n^{3(n-1)} & \cdots & \omega_n^{(n-1)^2} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n-1} \end{bmatrix}
⎣⎢⎢⎢⎢⎢⎢⎢⎡f(x0)f(x1)f(x2)f(x3)⋮f(xn−1)⎦⎥⎥⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡1111⋮11ωn1ωn2ωn3⋮ωnn−11ωn2ωn4ωn6⋮ωn2(n−1)1ωn3ωn6ωn9⋮ωn3(n−1)⋯⋯⋯⋯⋱⋯1ωnn−1ωn2(n−1)ωn3(n−1)⋮ωn(n−1)2⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤⎣⎢⎢⎢⎢⎢⎢⎢⎡a0a1a2a3⋮an−1⎦⎥⎥⎥⎥⎥⎥⎥⎤
Conversely, if we have the point value vector, and we want to compute the coefficient vector, we just need to multiply the inverse matrix of the bulky
n
×
n
n\times n
n×n matrix on the left for both sides.
Again, inverse matrices are generally slow to compute, but in this case we can check that the following matrix is the inverse matrix.
1
n
[
1
1
1
1
⋯
1
1
ω
n
−
1
ω
n
−
2
ω
n
−
3
⋯
ω
n
−
(
n
−
1
)
1
ω
n
−
2
ω
n
−
4
ω
n
−
6
⋯
ω
n
−
2
(
n
−
1
)
1
ω
n
−
3
ω
n
−
6
ω
n
−
9
⋯
ω
n
−
3
(
n
−
1
)
⋮
⋮
⋮
⋮
⋱
⋮
1
ω
n
−
(
n
−
1
)
ω
n
−
2
(
n
−
1
)
ω
n
−
3
(
n
−
1
)
⋯
ω
n
−
(
n
−
1
)
2
]
\frac{1}{n} \begin{bmatrix}1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^{-1} & \omega_n^{-2} & \omega_n^{-3} & \cdots & \omega_n^{-(n-1)} \\ 1 & \omega_n^{-2} & \omega_n^{-4} & \omega_n^{-6} & \cdots & \omega_n^{-2(n-1)} \\ 1 & \omega_n^{-3} & \omega_n^{-6} & \omega_n^{-9} & \cdots & \omega_n^{-3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{-(n-1)} & \omega_n^{-2(n-1)} & \omega_n^{-3(n-1)} & \cdots & \omega_n^{-(n-1)^2} \end{bmatrix}
n1⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡1111⋮11ωn−1ωn−2ωn−3⋮ωn−(n−1)1ωn−2ωn−4ωn−6⋮ωn−2(n−1)1ωn−3ωn−6ωn−9⋮ωn−3(n−1)⋯⋯⋯⋯⋱⋯1ωn−(n−1)ωn−2(n−1)ωn−3(n−1)⋮ωn−(n−1)2⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤
We can easily check that the product of the two matrices is the identity matrix. Thus, the IDFT process would be very similar to DFT except for we are using the “negative” roots of unity now. In fact, we can reuse the code of DFT for IDFT.
Sample Code
#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define il inline
const int N=3e5+5;
const double pi=acos(-1);
struct comp//complex number
{
double x,y;
comp operator + (const comp &c)const{
return {x+c.x,y+c.y};
}
comp operator - (const comp &c)const{
return {x-c.x,y-c.y};
}
comp operator * (const comp &c)const{
return {x*c.x-y*c.y,x*c.y+y*c.x};
}
}a[N],b[N];
int n,m,tot=1,bit=0,rev[N];
void fft(comp c[],int inv)//IFFT if inv=1
{
for(int i=0;i<tot;i++){
if(i<rev[i])swap(c[i],c[rev[i]]);
}
for(int mid=1;mid<tot;mid<<=1){
comp w1={cos(pi/mid),inv*sin(pi/mid)};
for(int i=0;i<tot;i+=mid*2){
comp wk={1,0};
for(int j=0;j<mid;j++,wk=wk*w1){
comp x=c[i+j],y=wk*c[i+j+mid];
c[i+j]=x+y,c[i+j+mid]=x-y;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)scanf("%lf",&a[i].x);
for(int i=0;i<=m;i++)scanf("%lf",&b[i].x);
while(tot<n+m+1){bit++,tot<<=1;}
for(int i=0;i<tot;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
fft(a,1),fft(b,1);
for(int i=0;i<tot;i++)a[i]=a[i]*b[i];
fft(a,-1);
for(int i=0;i<=n+m;i++)printf("%d ",(int)(a[i].x/tot+0.2))//round off;
}