Prerequisite: Complex numbers.
This article introduces an algorithm that computes the product of two polynomials in time, where each polynomial has degree , more efficiently than the naive algorithm. Since the multiplication of two integers can also be regarded as polynomial multiplication, this algorithm can accelerate multiplication of large integers.
Introduction
We now introduce two polynomials and :
The product of the two polynomials can be computed in time (where is the degree of or ):
Clearly, polynomial has coefficients satisfying . For this naive algorithm, computing each term takes time, and there are terms, so the total time complexity is .
Can we accelerate this and reduce its time complexity? Using the fast Fourier transform, we can reduce the complexity to .
Fourier Transform
The Fourier transform is a method for analyzing signals: it can analyze a signal into its components and also synthesize a signal from those components. Many waveforms can serve as signal components; the Fourier transform uses sine waves as its components.
Let be a function of time . The Fourier transform detects how strongly the period of frequency appears in :
Its inverse transform is
The inverse transform has a form very similar to the forward transform; the denominator is exactly the period of the exponential function.
The Fourier transform is equivalent to taking the continuous inner product of a time-domain function with complex exponential functions of period . The inverse transform is also an inner product.
The Fourier transform has a corresponding convolution theorem, which converts convolution in the time domain into multiplication in the frequency domain, and convolution in the frequency domain into multiplication in the time domain.
Discrete Fourier Transform
The discrete Fourier transform (DFT) is the form of the Fourier transform that is discrete in both the time and frequency domains, transforming time-domain samples of a signal into frequency-domain samples of its DTFT (discrete-time Fourier transform).
The Fourier transform is a continuous inner product of functions in integral form, while the discrete Fourier transform is an inner product in summation form.
Let be a sequence satisfying the finiteness condition. Its discrete Fourier transform (DFT) is:
Here is the base of the natural logarithm, and is the imaginary unit. This transform is usually denoted by , namely:
Analogously to the integral form, its inverse discrete Fourier transform (IDFT) is:
It can be written as:
In fact, the normalization coefficients before the sums in the DFT and IDFT formulas are not important. In the definition above, the coefficients before the DFT and IDFT are and , respectively. Sometimes we change both coefficients to .
The discrete Fourier transform is still a transformation from the time domain to the frequency domain. Because of the special nature of the summation form, it admits other interpretations.
If the sequence is regarded as the coefficient of polynomial 's term, then the computed is exactly the value of polynomial at the root of unity , namely .
This gives another interpretation of the convolution theorem: a special evaluation operation on polynomials. The discrete Fourier transform is exactly the evaluation of a polynomial at roots of unity.
For example, compute:
Define the function as:
Then we find that substituting the fourth root of unity into gives the following sequence:
Thus the following sum exactly cancels the other terms:
Therefore, the answer to this mathematical problem is:
This mathematical problem evaluates at roots of unity, which exactly constitutes the discrete Fourier transform.
Matrix formula
Since the discrete Fourier transform is a linear operator, it can be described by matrix multiplication. In matrix form, the discrete Fourier transform is:
where .
Fast Fourier Transform
FFT is an efficient algorithm for implementing the DFT, called the fast Fourier transform (Fast Fourier Transform, FFT). It does not make new discoveries about the theory of the Fourier transform, but it was a major step forward for applying the discrete Fourier transform in computer or digital systems. The fast number-theoretic transform (NTT) is an implementation of the fast Fourier transform (FFT) based on number theory.
In 1965, Cooley and Tukey published the fast Fourier transform algorithm. In fact, FFT had been discovered earlier, but modern computers did not yet exist, so people did not realize its importance. Some researchers believe FFT was discovered by Runge and König in 1924. In fact, Gauss had invented the algorithm as early as 1805, but never published it.
Divide-and-conquer implementation
The basic idea of the FFT algorithm is divide and conquer. For the DFT, it recursively computes the value at of . The divide-and-conquer idea of radix-2 FFT is to process the odd-degree and even-degree terms of a polynomial separately.
For example, consider a polynomial with terms:
Split the terms into two groups according to the parity of their degrees, and factor out an from the right group:
Construct new functions from the odd- and even-degree terms separately:
Then the original can be represented using the new functions as:
Using the property of roots of unity , and the fact that and are even functions, we know that at and , the corresponding values of and on the complex plane are the same. We obtain:
and:
Thus, after computing and , we can simultaneously compute and . We then recursively perform the DFT on and , respectively.
Since divide-and-conquer DFT can only handle polynomials whose lengths are , otherwise the two sides have different lengths during the division and the right side has no coefficient to access. Therefore, before the first DFT, pad the sequence upward to length (padding high-degree coefficients with ), so that the highest degree is .
When substituting values, because we need to substitute distinct values, we substitute , a total of distinct values.
For implementation, the STL provides a complex-number template, though it can also be implemented manually. The difference is that STL complex can call the exp function to obtain . In fact, using the imaginary number obtained from Euler's formula to obtain is equivalent.
This completes the introduction to the DFT part of the FFT algorithm: it converts a polynomial from coefficient representation to point-value representation.
It is worth noting that because these are roots of unity, we need to pad the high-order part of an -term polynomial with zeros so that .
Recursive FFT
#include <cmath>
#include <complex>
using Comp = std::complex<double>; // STL complex
constexpr Comp I(0, 1); // i
constexpr int MAX_N = 1 << 20;
Comp tmp[MAX_N];
// rev=1, DFT; rev=-1, IDFT
// Pay attention to the normalization coefficient after applying this function
void DFT(Comp* f, int n, int rev) {
if (n == 1) return;
for (int i = 0; i < n; ++i) tmp[i] = f[i];
// Put even terms on the left and odd terms on the right
for (int i = 0; i < n; ++i) {
if (i & 1)
f[n / 2 + i / 2] = tmp[i];
else
f[i / 2] = tmp[i];
}
Comp *g = f, *h = f + n / 2;
// Recursive DFT
DFT(g, n / 2, rev), DFT(h, n / 2, rev);
// cur is the current root of unity; for k = 0, the corresponding root of unity is omega^0_n = 1.
// step is the ratio between two consecutive roots of unity, satisfying omega^k_n = step*omega^{k-1}*n,
// which is equivalently defined as exp(I*(-2*M_PI/n*rev))
Comp cur(1, 0), step(cos(2 * M_PI / n), sin(-2 * M_PI * rev / n));
for (int k = 0; k < n / 2;
++k) { // F(omega^k_n) = G(omega^k*{n/2}) + omega^k*n\*H(omega^k*{n/2})
tmp[k] = g[k] + cur * h[k];
// F(omega^{k+n/2}*n) = G(omega^k*{n/2}) - omega^k_n*H(omega^k\_{n/2})
tmp[k + n / 2] = g[k] - cur * h[k];
cur *= step;
}
for (int i = 0; i < n; ++i) f[i] = tmp[i];
}
The time complexity is .
Iterative doubling implementation
This algorithm can be further optimized from the perspective of divide and conquer. For radix-2 FFT, at each step we separate the odd- and even-degree coefficients of the entire polynomial, continuing until only one coefficient remains. However, this recursive process requires more memory. Therefore, we can first “split” these coefficients in the original array by “mimicking recursion”, and then “double” the segment length to merge the computed values.
For the “split”, we can use a bit-reversal permutation.
For the “merge”, butterfly-operation optimization can complete the work using only extra space.
Bit-reversal permutation
Using an -term polynomial as an example, simulate the splitting process:
- The initial sequence is .\n- After one bisection: .\n- After two bisections: .\n- After three bisections: .
The pattern is as follows: in the original sequence, write each number in binary and reverse the binary digits symmetrically; the result is the index of its final position. For example, is 001, whose reversal is 100, namely 4, and its final position is indeed 4. We call this transformation the bit-reversal permutation; the proof is left to the reader.
By its definition, we can compute the transformed position of every number in time:
Bit-reversal permutation implementation()
/*
* Perform the reversal transformation before FFT and IFFT
* Swap position i with the position obtained by reversing the binary digits of i
* len must be a power of 2
*/
void change(Complex y[], int len) {
// Initially i is 0...01 and j is 10...0; they are symmetric opposites in binary.
// Then i gradually increases by one while j remains the binary reverse of i, until i = 1...11.
for (int i = 1, j = len / 2, k; i < len - 1; i++) {
// Swap elements whose indices are reverses of each other; i < j ensures each pair is swapped once
if (i < j) swap(y[i], y[j]);
// i performs an ordinary +1, while j performs a reversal-type +1, keeping i and j as reverses.
// Here k denotes the highest position at which 0 appears. First subtract the number whose high bits are all 1 from j,
// until reaching a 0, then add it back.
// Considering the number of bit flips in j, the highest bit flips n times,
// the second-highest bit flips n/2 times, and so on, so the time complexity is:
// T(n) = n + n/2 + n/4 + ... = O(n)
k = len / 2;
while (j >= k) {
j = j - k;
k = k / 2;
}
j += k;
}
}
The bit-reversal permutation can also be computed recursively in increasing order in time. Let , where is the length of the binary numbers, and let denote the number obtained by reversing the -digit binary representation of (padding with leading s). We need .
First, .
We compute in increasing order. Thus, when computing , the value of is known. We shift right by one bit (divide by ), then reverse it and shift right by one bit again; this gives the reversal of all bits of except the least significant (binary) bit.
Consider the reversal of the least significant bit: if it is , the highest bit after reversal is . If it is , the highest bit after reversal is , so we must add . Therefore:
For example, let and . To reverse :
- Consider . We know ; shifting right by one bit gives .\n2. Consider the least significant bit. If it is , it is reversed to the highest bit, so add to the reversed number; if it is , make no change.
Bit-reversal permutation implementation()
// Again, ensure that len is a power of 2
// Let rev[i] be the value obtained by reversing i
void change(Complex y[], int len) {
for (int i = 0; i < len; ++i) {
rev[i] = rev[i >> 1] >> 1;
if (i & 1) { // if the last bit is 1, reverse it to len/2
rev[i] |= len >> 1;
}
}
for (int i = 0; i < len; ++i) {
if (i < rev[i]) { // ensure that each pair is reversed only once
swap(y[i], y[rev[i]]);
}
}
return;
}
Butterfly-operation optimization
After obtaining and , use the following two formulas to compute and :
After applying the bit-reversal permutation, for a given pair :
- The value of is stored at array index , and the value of is stored at array index .\n- The value of will be stored at array index , and the value of will be stored at array index .
Therefore, we can overwrite the values directly at array indices and without allocating an extra array to save them. This method is called the butterfly operation, or more precisely, the radix-2 butterfly operation.
Here is a more detailed explanation of how to use butterfly operations to merge all segments of length :
- Let the segment length be .\n2. Enumerate simultaneously the sequence and its left endpoint , and the sequence and its left endpoint .\n3. When merging two segments, enumerate ; at this time, is stored at array index , and is stored at array index .\n4. Use the butterfly operation to compute and , then overwrite the original positions directly.
Inverse Fast Fourier Transform
The inverse Fourier transform can be expressed using the Fourier transform. There are two ways to understand this.
From the perspective of linear algebra
The IDFT (inverse Fourier transform) converts the point-value representation of the target polynomial into its coefficient representation. Since the DFT itself is a linear transformation, we can regard the target polynomial as a vector and left-multiply it by a matrix to obtain the transformed vector, simulating the process of substituting roots of unity into the polynomial:
We have now obtained the result on the far left. The middle values also correspond one-to-one in the point-value representation of the target polynomial. Therefore, by basic matrix theory, we only need to left-multiply both sides of the equation by the inverse of the large matrix in the middle.
Because the entries of this matrix are very special, its inverse also has a special property: taking the reciprocal of each entry and then dividing by the transform length gives its inverse matrix.
Note that the length of the Fourier transform is not the length of the polynomial; the transform length should be longer than the length of the product polynomial. If the polynomials to be multiplied are not long enough, pad their high-degree terms with .
To make the computed result the reciprocal of the original, Euler's formula gives:
Therefore, we can try taking the root of unity to be . This makes the computed result the reciprocal of the original; the only extra operation is then to divide by its length , while all other operations are exactly the same as in the DFT. We can define a function with a parameter or multiplied by . Passing means DFT, and passing means IDFT.
Periodicity of roots of unity
The periodicity of roots of unity gives another way to understand the relationship between IDFT and DFT.
Consider the original polynomial . The IDFT restores the coefficient representation from the point-value representation.
Consider the construction method. We know and want to find . Construct the following polynomial:
This is equivalent to taking as the coefficient representation of polynomial .
At this point, there are two derivations, corresponding to two implementation methods.
Method 1
Let . Then the point-value representation of polynomial at is .
By transforming the definition of , we can express as
Let .
When , .
When , subtract the equations after shifting one term:
That is,
Substituting back into the original equation gives
In other words, given the points , the point-value representation of is
In summary, take the reciprocal of the root of unity, run FFT on , and divide by to obtain the coefficient representation of .
Method 2
Directly substitute into .
The derivation is much the same as in Method 1; ultimately we obtain .
We have if and only if , and otherwise. Therefore, .
This means that after applying the DFT to and dividing by , by reversing the last elements we can restore the coefficient representation of .
Code implementation
Thus our FFT function can combine DFT and IDFT. The implementation is as follows:
Iterative FFT (corresponding to Method 1)
/*
* Perform FFT
* len must have the form 2^k
* on == 1 means DFT, and on == -1 means IDFT
*/
void fft(Complex y[], int len, int on) {
// Bit-reversal permutation
change(y, len);
// Simulate the merge process, starting by merging length one into length two and continuing up to length len.
for (int h = 2; h <= len; h <<= 1) {
// wn: the interval between the current roots of unity: w^1_h
Complex wn(cos(2 * PI / h), sin(on * 2 * PI / h));
// Merge, for a total of len / h times.
for (int j = 0; j < len; j += h) {
// Compute the current root of unity; initially it is 1 = w^0_n, then it increases with wn as the interval: w^1_n
// ...
Complex w(1, 0);
for (int k = j; k < j + h / 2; k++) {
// The left and right parts are the solutions to the subproblems
Complex u = y[k];
Complex t = w * y[k + h / 2];
// This adds the results of the two divide-and-conquer parts
y[k] = u + t;
y[k + h / 2] = u - t;
// The ω in the second half of the “step” is necessarily the opposite of the one in the “first half”
// A point on the “red circle” returns after one full turn and becomes its opposite after a half turn
// The square of a number's opposite equals the square of the number itself
w = w * wn;
}
}
}
// For IDFT, each element of its inverse matrix is not only the reciprocal of the original element; it must also be divided by the length len.
if (on == -1) {
for (int i = 0; i < len; i++) {
y[i].x /= len;
y[i].y /= len;
}
}
}
Iterative FFT (corresponding to Method 2)
/*
* Perform FFT
* len must have the form 2^k
* on == 1 means DFT, and on == -1 means IDFT
*/
void fft(Complex y[], int len, int on) {
change(y, len);
for (int h = 2; h <= len; h <<= 1) { // Simulate the merge process
Complex wn(cos(2 * PI / h), sin(2 * PI / h)); // Compute the current root of unity
for (int j = 0; j < len; j += h) {
Complex w(1, 0); // Compute the current root of unity
for (int k = j; k < j + h / 2; k++) {
Complex u = y[k];
Complex t = w * y[k + h / 2];
y[k] = u + t; // This adds the results of the two divide-and-conquer parts
y[k + h / 2] = u - t;
// The ω in the second half of the “step” is necessarily the opposite of the one in the “first half”
// A point on the “red circle” returns after one full turn and becomes its opposite after a half turn
// The square of a number's opposite equals the square of the number itself
w = w * wn;
}
}
}
if (on == -1) {
reverse(y + 1, y + len);
for (int i = 0; i < len; i++) {
y[i].x /= len;
y[i].y /= len;
}
}
}
FFT template (HDU 1402 - A * B Problem Plus)
#include <cmath>
#include <cstring>
#include <iostream>
const double PI = acos(-1.0);
struct Complex {
double x, y;
Complex(double _x = 0.0, double _y = 0.0) {
x = _x;
y = _y;
}
Complex operator-(const Complex &b) const {
return Complex(x - b.x, y - b.y);
}
Complex operator+(const Complex &b) const {
return Complex(x + b.x, y + b.y);
}
Complex operator*(const Complex &b) const {
return Complex(x * b.x - y * b.y, x * b.y + y * b.x);
}
};
/*
* Perform the reversal transformation before FFT and IFFT
* Swap position i with the position obtained by reversing the binary digits of i
*len must be a power of 2
*/
void change(Complex y[], int len) {
int i, j, k;
for (int i = 1, j = len / 2; i < len - 1; i++) {
if (i < j) std::swap(y[i], y[j]);
// Swap elements whose indices are reverses of each other; i<j ensures each pair is swapped once
// i performs an ordinary +1, while j performs a reversal-type +1, keeping i and j as reverses
k = len / 2;
while (j >= k) {
j = j - k;
k = k / 2;
}
if (j < k) j += k;
}
}
/*
* Perform FFT
*len must have the form 2^k
*on == 1 means DFT, and on == -1 means IDFT
*/
void fft(Complex y[], int len, int on) {
change(y, len);
for (int h = 2; h <= len; h <<= 1) {
Complex wn(cos(2 * PI / h), sin(on * 2 * PI / h));
for (int j = 0; j < len; j += h) {
Complex w(1, 0);
for (int k = j; k < j + h / 2; k++) {
Complex u = y[k];
Complex t = w * y[k + h / 2];
y[k] = u + t;
y[k + h / 2] = u - t;
w = w * wn;
}
}
}
if (on == -1) {
for (int i = 0; i < len; i++) {
y[i].x /= len;
}
}
}
constexpr int MAXN = 200020;
Complex x1[MAXN], x2[MAXN];
char str1[MAXN / 2], str2[MAXN / 2];
int sum[MAXN];
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
while (cin >> str1 >> str2) {
int len1 = strlen(str1);
int len2 = strlen(str2);
int len = 1;
while (len < len1 * 2 || len < len2 * 2) len <<= 1;
for (int i = 0; i < len1; i++) x1[i] = Complex(str1[len1 - 1 - i] - '0', 0);
for (int i = len1; i < len; i++) x1[i] = Complex(0, 0);
for (int i = 0; i < len2; i++) x2[i] = Complex(str2[len2 - 1 - i] - '0', 0);
for (int i = len2; i < len; i++) x2[i] = Complex(0, 0);
fft(x1, len, 1);
fft(x2, len, 1);
for (int i = 0; i < len; i++) x1[i] = x1[i] * x2[i];
fft(x1, len, -1);
for (int i = 0; i < len; i++) sum[i] = int(x1[i].x + 0.5);
for (int i = 0; i < len; i++) {
sum[i + 1] += sum[i] / 10;
sum[i] %= 10;
}
len = len1 + len2 - 1;
while (sum[len] == 0 && len > 0) len--;
for (int i = len; i >= 0; i--) cout << char(sum[i] + '0');
cout << '\n';
}
return 0;
}