📐 Complex Numbers and FFT: Expert Level
This topic covers working with complex numbers in competitive programming and one of the most important algorithms in computer science – the Fast Fourier Transform (FFT).
1. Complex Numbers
Complex numbers are an extension of the real numbers. They have the form \(z = a + bi\), where: * \(a\) is the real part (\(\text{Re}(z)\)). * \(b\) is the imaginary part (\(\text{Im}(z)\)). * \(i\) is the imaginary unit, for which \(i^2 = -1\).
1.1. Arithmetic Operations
If we have two numbers \(z_1 = a_1 + b_1 i\) and \(z_2 = a_2 + b_2 i\): * Addition: \((a_1 + a_2) + (b_1 + b_2)i\). * Subtraction: \((a_1 - a_2) + (b_1 - b_2)i\). * Multiplication: \((a_1 a_2 - b_1 b_2) + (a_1 b_2 + a_2 b_1)i\) (because \(i^2 = -1\)).
1.2. Geometric Interpretation
In the complex plane (Argand diagram), the number \(z = x + iy\) is represented as a point \((x, y)\). * Modulus (length): \(|z| = \sqrt{x^2 + y^2}\). * Argument (angle): \(\arg(z) = \theta\) is the angle with the positive x-axis. * Polar form: \(z = r(\cos \theta + i \sin \theta)\), where \(r = |z|\). * Euler's formula: \(e^{i\theta} = \cos \theta + i \sin \theta\). Therefore \(z = r e^{i\theta}\).
Multiplication of complex numbers in polar form is very intuitive: \(z_1 \cdot z_2 = (r_1 e^{i\theta_1}) (r_2 e^{i\theta_2}) = (r_1 r_2) e^{i(\theta_1 + \theta_2)}\). * Moduli are multiplied. * Angles are added.
1.3. C++ std::complex
C++ provides a built-in <complex> class.
#include <iostream>
#include <complex>
#include <cmath>
using namespace std;
typedef complex<double> cd;
const double PI = acos(-1);
int main() {
cd a(3, 4); // 3 + 4i
cd b(1, -2); // 1 - 2i
cd sum = a + b;
cd prod = a * b;
cout << "Sum: " << sum.real() << " + " << sum.imag() << "i\n";
cout << "Abs of a: " << abs(a) << "\n"; // 5
cout << "Arg of a: " << arg(a) << "\n"; // in radians
// Creation via polar coordinates
cd c = polar(1.0, PI / 2); // 1 * e^(i*pi/2) = i
cout << "Polar (0, 1): " << c << "\n";
return 0;
}
2. Polynomials
A polynomial of degree \(n-1\) is defined as: \(A(x) = a_0 + a_1 x + a_2 x^2 + \dots + a_{n-1} x^{n-1}\)
There are two main ways to represent a polynomial: 1. Coefficient form: A list of coefficients \([a_0, a_1, \dots, a_{n-1}]\). * Addition: \(O(n)\). * Multiplication: \(O(n^2)\) (each with each). * Value calculation (Horner's method): \(O(n)\). 2. Point-Value form: A set of \(n\) pairs \({\{(x_0, y_0), \dots, (x_{n-1}, y_{n-1})\}}\), where \(y_k = A(x_k)\). Every polynomial of degree \(n-1\) is uniquely determined by \(n\) points. * Addition: \(O(n)\) (values for the same \(x\) are added). * Multiplication: \(O(n)\) (values are multiplied: \(C(x_k) = A(x_k) \cdot B(x_k)\))!
The idea of FFT: We want to multiply two polynomials quickly. 1. Transform them from coefficient to point-value form (DFT). 2. Multiply them in \(O(n)\) in point-value form. 3. Transform the result back to coefficient form (Inverse DFT).
If we choose arbitrary \(x\), the transformation is slow (\(O(n^2)\)). But if we choose special points – the complex roots of unity, we can do it in \(O(n \log n)\).
3. Fast Fourier Transform (FFT)
3.1. Roots of Unity
The complex \(n\)-th roots of unity are solutions to the equation \(x^n = 1\). There are exactly \(n\) such numbers, evenly spaced on the unit circle. \(\\omega_n^k = e^{i \frac{2\pi k}{n}} = \cos\left(\frac{2\pi k}{n}\right) + i \sin\left(\frac{2\pi k}{n}\right)\)
The principal (primitive) root is \(\\omega_n = e^{i \frac{2\pi}{n}}\). The others are its powers: \(\\omega_n^0, \\omega_n^1, \dots, \\omega_n^{n-1}\).
Important properties: 1. \(\\omega_n^n = 1\). 2. \(\\omega_n^{n/2} = -1\). 3. \(\\omega_{2n}^{2k} = \\omega_n^k\).
3.2. Cooley-Tukey Algorithm
Let \(n\) be an even number. We split the polynomial \(A(x)\) into two parts – even and odd indices: \(A(x) = (a_0 + a_2 x^2 + \dots) + x(a_1 + a_3 x^2 + \dots)\) \(A(x) = A_{even}(x^2) + x A_{odd}(x^2)\)
To calculate \(A(x)\) at points \(x = \\omega_n^k\), we notice symmetry: For \(0 \le k < n/2\): \(x = \\omega_n^k \implies x^2 = \\omega_n^{2k} = \\omega_{n/2}^k\) \(A(\omega_n^k) = A_{even}(\\omega_{n/2}^k) + \\omega_n^k A_{odd}(\\omega_{n/2}^k)\)
For the "second half" \(k' = k + n/2\): \(\\omega_n^{k + n/2} = -\\omega_n^k\) \(A(\omega_n^{k+n/2}) = A_{even}(\\omega_{n/2}^k) - \\omega_n^k A_{odd}(\\omega_{n/2}^k)\)
Thus the problem for size \(n\) reduces to two subproblems for size \(n/2\). Complexity: \(T(n) = 2T(n/2) + O(n) \implies O(n \log n)\).
3.3. Recursive Implementation
#include <vector>
#include <complex>
#include <cmath>
#include <algorithm>
using namespace std;
typedef complex<double> cd;
const double PI = acos(-1);
void fft(vector<cd> & a, bool invert) {
int n = a.size();
if (n == 1) return;
vector<cd> a0(n / 2), a1(n / 2);
for (int i = 0; 2 * i < n; i++) {
a0[i] = a[2 * i];
a1[i] = a[2 * i + 1];
}
fft(a0, invert);
fft(a1, invert);
double ang = 2 * PI / n * (invert ? -1 : 1);
cd w(1), wn(cos(ang), sin(ang));
for (int i = 0; 2 * i < n; i++) {
a[i] = a0[i] + w * a1[i];
a[i + n / 2] = a0[i] - w * a1[i];
if (invert) {
a[i] /= 2;
a[i + n / 2] /= 2;
}
w *= wn;
}
}
3.4. Iterative Implementation (Optimized)
Recursion can be slow. The iterative approach uses "Bit-reversal permutation" – the indices are rearranged according to the mirror representation of their bits.
void fft_iterative(vector<cd> & a, bool invert) {
int n = a.size();
// Bit-reversal permutation
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1)
j ^= bit;
j ^= bit;
if (i < j) swap(a[i], a[j]);
}
// Butterfly operations
for (int len = 2; len <= n; len <<= 1) {
double ang = 2 * PI / len * (invert ? -1 : 1);
cd wlen(cos(ang), sin(ang));
for (int i = 0; i < n; i += len) {
cd w(1);
for (int j = 0; j < len / 2; j++) {
cd u = a[i + j], v = a[i + j + len / 2] * w;
a[i + j] = u + v;
a[i + j + len / 2] = u - v;
w *= wlen;
}
}
}
if (invert) {
for (cd & x : a) x /= n;
}
}
3.5. Polynomial Multiplication
To multiply two polynomials \(A\) and \(B\): 1. Extend the vectors with zeros to the nearest power of 2 that is at least \(\deg(A) + \deg(B) + 1\). 2. Apply FFT to \(A\) and \(B\). 3. Multiply element-wise \(A[i] = A[i] \cdot B[i]\). 4. Apply Inverse FFT to the result. 5. (Optional) Round the real parts to the nearest integer.
vector<int> multiply(vector<int> const& a, vector<int> const& b) {
vector<cd> fa(a.begin(), a.end()), fb(b.begin(), b.end());
int n = 1;
while (n < (int)(a.size() + b.size()))
n <<= 1;
fa.resize(n);
fb.resize(n);
fft_iterative(fa, false);
fft_iterative(fb, false);
for (int i = 0; i < n; i++)
fa[i] *= fb[i];
fft_iterative(fa, true);
vector<int> result(n);
for (int i = 0; i < n; i++)
result[i] = round(fa[i].real());
// Remove leading zeros if needed
return result;
}
4. Number Theoretic Transform (NTT)
FFT uses complex numbers (double), which leads to rounding errors. In tasks requiring a result modulo some number, we use NTT.
This is the equivalent of FFT in finite fields (Modular Arithmetic).
* Instead of complex roots of unity, we use primitive roots modulo \(P\).
* Requires \(P\) to be of a special form: \(P = c \cdot 2^k + 1\).
* Example: \(998244353 = 119 \cdot 2^{23} + 1\). Primitive root \(g=3\).
Advantage: Works with integers (long long), no loss of precision.
Application of NTT
Everything is the same as FFT, but: 1. All operations are modulo \(P\). 2. \(\\omega_n\) is replaced by \(g^{(P-1)/n} \pmod P\). 3. \(\\omega_n^{-1}\) is replaced by the modular inverse.
const int mod = 998244353;
const int root = 3; // Primitive root
// Implementation requires modular exponentiation and modular inverse
5. Fast Walsh-Hadamard Transform (FWHT)
FWHT is a variant of FFT used for bitmask operations. If we want to find a polynomial \(C\) such that: \(C[k] = \sum_{i \oplus j = k} A[i] B[j]\) (XOR convolution) Or for AND/OR operations.
For XOR FWHT, the transformation matrix is very simple: \(\\begin{pmatrix} 1 & 1 \\ 1 & -1 \\end{pmatrix}\) This allows multiplication in \(O(N \log N)\) instead of \(O(N^2)\).
6. Practice Tasks
- SPOJ POLARIVAL: Evaluating a polynomial value.
- SPOJ MUL: Multiplication of large numbers (consider the digits as coefficients).
- Codeforces 993E: Nikita and Order Statistics (counting subarrays).
- CSES Problem Set: Polynomial Queries.
FFT is a powerful tool not only for polynomials but for any tasks involving combinatorics and sums with a fixed index (convolutions).
```