Skip to Content

Transformada Rápida de Fourier (FFT)

En este artículo discutiremos un algoritmo que permite multiplicar dos polinomios de longitud nn en tiempo O(nlogn)O(n \log n), lo cual es mejor que la multiplicación trivial que toma tiempo O(n2)O(n^2). Obviamente, multiplicar dos números largos también se puede reducir a multiplicar polinomios, así que dos números largos también se pueden multiplicar en tiempo O(nlogn)O(n \log n) (donde nn es la cantidad de dígitos de los números).

El descubrimiento de la transformada rápida de Fourier (FFT) se atribuye a Cooley y Tukey, que publicaron un algoritmo en 1965. Pero de hecho la FFT se había descubierto repetidamente antes, aunque su importancia no se entendió hasta la invención de las computadoras modernas. Algunos investigadores atribuyen el descubrimiento de la FFT a Runge y König en 1924. Pero en realidad Gauss ya desarrolló un método de este tipo en 1805, aunque nunca lo publicó.

Nótese que el algoritmo de FFT que se presenta aquí corre en tiempo O(nlogn)O(n \log n), pero no sirve para multiplicar polinomios arbitrariamente grandes con coeficientes arbitrariamente grandes, ni para multiplicar enteros arbitrariamente grandes. Puede manejar sin problema polinomios de tamaño 10510^5 con coeficientes pequeños, o multiplicar dos números de tamaño 10610^6, lo cual suele ser suficiente para resolver problemas de programación competitiva. Más allá de la escala de multiplicar números de 10610^6 bits, el rango y la precisión de los números de punto flotante usados durante el cálculo no serán suficientes para dar resultados finales exactos, aunque existen variantes más complejas que pueden realizar multiplicaciones de polinomios/enteros arbitrariamente grandes. Por ejemplo, en 1971 Schönhage y Strasser desarrollaron una variante para multiplicar números arbitrariamente grandes que aplica la FFT de forma recursiva en estructuras de anillos y corre en O(nlognloglogn)O(n \log n \log \log n). Y recientemente (en 2019) Harvey y van der Hoeven publicaron un algoritmo que corre en verdadero O(nlogn)O(n \log n).

Transformada discreta de Fourier

Sea un polinomio de grado n1n - 1:

A(x)=a0x0+a1x1++an1xn1A(x) = a_0 x^0 + a_1 x^1 + \dots + a_{n-1} x^{n-1}

Sin pérdida de generalidad asumimos que nn — la cantidad de coeficientes — es una potencia de 22. Si nn no es una potencia de 22, simplemente agregamos los términos faltantes aixia_i x^i y ponemos los coeficientes aia_i en 00.

La teoría de los números complejos nos dice que la ecuación xn=1x^n = 1 tiene nn soluciones complejas (llamadas las raíces nn-ésimas de la unidad), y las soluciones son de la forma wn,k=e2kπinw_{n, k} = e^{\frac{2 k \pi i}{n}} con k=0n1k = 0 \dots n-1. Además, estos números complejos tienen algunas propiedades muy interesantes: por ejemplo, la raíz nn-ésima principal wn=wn,1=e2πinw_n = w_{n, 1} = e^{\frac{2 \pi i}{n}} se puede usar para describir todas las demás raíces nn-ésimas: wn,k=(wn)kw_{n, k} = (w_n)^k.

La transformada discreta de Fourier (DFT) del polinomio A(x)A(x) (o, equivalentemente, del vector de coeficientes (a0,a1,,an1)(a_0, a_1, \dots, a_{n-1})) se define como los valores del polinomio en los puntos x=wn,kx = w_{n, k}, es decir, es el vector:

DFT(a0,a1,,an1)=(y0,y1,,yn1)=(A(wn,0),A(wn,1),,A(wn,n1))=(A(wn0),A(wn1),,A(wnn1))DFT(a0,a1,,an1)amp;=(y0,y1,,yn1)amp;=(A(wn,0),A(wn,1),,A(wn,n1))amp;=(A(wn0),A(wn1),,A(wnn1))\begin{align} \text{DFT}(a_0, a_1, \dots, a_{n-1}) &= (y_0, y_1, \dots, y_{n-1}) \ &= (A(w_{n, 0}), A(w_{n, 1}), \dots, A(w_{n, n-1})) \ &= (A(w_n^0), A(w_n^1), \dots, A(w_n^{n-1})) \end{align}

De forma similar se define la transformada discreta de Fourier inversa: la DFT inversa de los valores del polinomio (y0,y1,,yn1)(y_0, y_1, \dots, y_{n-1}) son los coeficientes del polinomio (a0,a1,,an1)(a_0, a_1, \dots, a_{n-1}).

InverseDFT(y0,y1,,yn1)=(a0,a1,,an1)\text{InverseDFT}(y_0, y_1, \dots, y_{n-1}) = (a_0, a_1, \dots, a_{n-1})

Así, si una DFT directa calcula los valores del polinomio en los puntos de las raíces nn-ésimas, la DFT inversa puede restaurar los coeficientes del polinomio usando esos valores.

Aplicación de la DFT: multiplicación rápida de polinomios

Sean dos polinomios AA y BB. Calculamos la DFT de cada uno: DFT(A)\text{DFT}(A) y DFT(B)\text{DFT}(B).

¿Qué ocurre si multiplicamos estos polinomios? Obviamente, en cada punto los valores simplemente se multiplican, es decir

(AB)(x)=A(x)B(x).(A \cdot B)(x) = A(x) \cdot B(x).

Esto significa que si multiplicamos los vectores DFT(A)\text{DFT}(A) y DFT(B)\text{DFT}(B) — multiplicando cada elemento de un vector por el elemento correspondiente del otro — entonces obtenemos nada menos que la DFT del polinomio DFT(AB)\text{DFT}(A \cdot B):

DFT(AB)=DFT(A)DFT(B)\text{DFT}(A \cdot B) = \text{DFT}(A) \cdot \text{DFT}(B)

Finalmente, aplicando la DFT inversa, obtenemos:

AB=InverseDFT(DFT(A)DFT(B))A \cdot B = \text{InverseDFT}(\text{DFT}(A) \cdot \text{DFT}(B))

En el lado derecho, el producto de las dos DFT se entiende como el producto componente a componente de los elementos de los vectores. Esto se puede calcular en tiempo O(n)O(n). Si podemos calcular la DFT y la DFT inversa en O(nlogn)O(n \log n), entonces podemos calcular el producto de los dos polinomios (y, en consecuencia, también de dos números largos) con la misma complejidad temporal.

Hay que notar que los dos polinomios deben tener el mismo grado. De lo contrario, los dos vectores resultado de la DFT tienen distinta longitud. Podemos lograrlo agregando coeficientes con valor 00.

Y además, como el resultado del producto de dos polinomios es un polinomio de grado 2(n1)2 (n - 1), hay que duplicar los grados de cada polinomio (otra vez rellenando con 00s). A partir de un vector con nn valores no se puede reconstruir el polinomio deseado con 2n12n - 1 coeficientes.

Transformada rápida de Fourier

La transformada rápida de Fourier es un método que permite calcular la DFT en tiempo O(nlogn)O(n \log n). La idea básica de la FFT es aplicar divide y vencerás. Dividimos el vector de coeficientes del polinomio en dos vectores, calculamos recursivamente la DFT de cada uno y combinamos los resultados para calcular la DFT del polinomio completo.

Así, sea un polinomio A(x)A(x) de grado n1n - 1, donde nn es una potencia de 22 y n>1n > 1:

A(x)=a0x0+a1x1++an1xn1A(x) = a_0 x^0 + a_1 x^1 + \dots + a_{n-1} x^{n-1}

Lo dividimos en dos polinomios más pequeños, uno que contiene solo los coeficientes de las posiciones pares y otro que contiene los coeficientes de las posiciones impares:

A0(x)=a0x0+a2x1++an2xn21A1(x)=a1x0+a3x1++an1xn21A0(x)amp;=a0x0+a2x1++an2xn21A1(x)amp;=a1x0+a3x1++an1xn21\begin{align} A_0(x) &= a_0 x^0 + a_2 x^1 + \dots + a_{n-2} x^{\frac{n}{2}-1} \ A_1(x) &= a_1 x^0 + a_3 x^1 + \dots + a_{n-1} x^{\frac{n}{2}-1} \end{align}

Es fácil ver que

A(x)=A0(x2)+xA1(x2).A(x) = A_0(x^2) + x A_1(x^2).

Los polinomios A0A_0 y A1A_1 tienen solo la mitad de coeficientes que el polinomio AA. Si podemos calcular DFT(A)\text{DFT}(A) en tiempo lineal usando DFT(A0)\text{DFT}(A_0) y DFT(A1)\text{DFT}(A_1), entonces obtenemos la recurrencia TDFT(n)=2TDFT(n2)+O(n)T_{\text{DFT}}(n) = 2 T_{\text{DFT}}\left(\frac{n}{2}\right) + O(n) para la complejidad temporal, que resulta en TDFT(n)=O(nlogn)T_{\text{DFT}}(n) = O(n \log n) por el teorema maestro.

Veamos cómo se puede lograr eso.

Supongamos que ya calculamos los vectores (yk0)k=0n/21=DFT(A0)\left(y_k^0\right){k=0}^{n/2-1} = \text{DFT}(A_0) y (yk1)k=0n/21=DFT(A1)\left(y_k^1\right){k=0}^{n/2-1} = \text{DFT}(A_1). Busquemos una expresión para (yk)k=0n1=DFT(A)\left(y_k\right)_{k=0}^{n-1} = \text{DFT}(A).

Para los primeros n2\frac{n}{2} valores podemos usar simplemente la ecuación ya mencionada A(x)=A0(x2)+xA1(x2)A(x) = A_0(x^2) + x A_1(x^2):

yk=yk0+wnkyk1,k=0n21.y_k = y_k^0 + w_n^k y_k^1, \quad k = 0 \dots \frac{n}{2} - 1.

Sin embargo, para los segundos n2\frac{n}{2} valores necesitamos encontrar una expresión un poco distinta:

yk+n/2=A(wnk+n/2)=A0(wn2k+n)+wnk+n/2A1(wn2k+n)=A0(wn2kwnn)+wnkwnn/2A1(wn2kwnn)=A0(wn2k)wnkA1(wn2k)=yk0wnkyk1yk+n/2amp;=A(wnk+n/2)amp;=A0(wn2k+n)+wnk+n/2A1(wn2k+n)amp;=A0(wn2kwnn)+wnkwnn/2A1(wn2kwnn)amp;=A0(wn2k)wnkA1(wn2k)amp;=yk0wnkyk1\begin{align} y_{k+n/2} &= A\left(w_n^{k+n/2}\right) \ &= A_0\left(w_n^{2k+n}\right) + w_n^{k + n/2} A_1\left(w_n^{2k+n}\right) \ &= A_0\left(w_n^{2k} w_n^n\right) + w_n^k w_n^{n/2} A_1\left(w_n^{2k} w_n^n\right) \ &= A_0\left(w_n^{2k}\right) - w_n^k A_1\left(w_n^{2k}\right) \ &= y_k^0 - w_n^k y_k^1 \end{align}

Aquí usamos de nuevo A(x)=A0(x2)+xA1(x2)A(x) = A_0(x^2) + x A_1(x^2) y las dos identidades wnn=1w_n^n = 1 y wnn/2=1w_n^{n/2} = -1.

Por lo tanto obtenemos las fórmulas deseadas para calcular el vector completo (yk)(y_k):

yk=yk0+wnkyk1,k=0n21,yk+n/2=yk0wnkyk1,k=0n21.ykamp;=yk0+wnkyk1,amp;k=0n21,yk+n/2amp;=yk0wnkyk1,amp;k=0n21.\begin{align} y_k &= y_k^0 + w_n^k y_k^1, &\quad k = 0 \dots \frac{n}{2} - 1, \ y_{k+n/2} &= y_k^0 - w_n^k y_k^1, &\quad k = 0 \dots \frac{n}{2} - 1. \end{align}

(Este patrón a+ba + b y aba - b a veces se llama mariposa o butterfly.)

Así aprendimos a calcular la DFT en tiempo O(nlogn)O(n \log n).

FFT inversa

Sea el vector (y0,y1,yn1)(y_0, y_1, \dots y_{n-1}) — los valores del polinomio AA de grado n1n - 1 en los puntos x=wnkx = w_n^k — dado. Queremos restaurar los coeficientes (a0,a1,,an1)(a_0, a_1, \dots, a_{n-1}) del polinomio. Este problema conocido se llama interpolación, y hay algoritmos generales para resolverlo. Pero en este caso especial (ya que conocemos los valores del polinomio en las raíces de la unidad) podemos obtener un algoritmo mucho más simple (que es prácticamente el mismo que la FFT directa).

Podemos escribir la DFT, según su definición, en forma matricial:

(wn0wn0wn0wn0wn0wn0wn1wn2wn3wnn1wn0wn2wn4wn6wn2(n1)wn0wn3wn6wn9wn3(n1)wn0wnn1wn2(n1)wn3(n1)wn(n1)(n1))(a0a1a2a3an1)=(y0y1y2y3yn1) (wn0amp;wn0amp;wn0amp;wn0amp;amp;wn0wn0amp;wn1amp;wn2amp;wn3amp;amp;wnn1wn0amp;wn2amp;wn4amp;wn6amp;amp;wn2(n1)wn0amp;wn3amp;wn6amp;wn9amp;amp;wn3(n1)amp;amp;amp;amp;amp;wn0amp;wnn1amp;wn2(n1)amp;wn3(n1)amp;amp;wn(n1)(n1))\begin{pmatrix} w_n^0 & w_n^0 & w_n^0 & w_n^0 & \cdots & w_n^0 \ w_n^0 & w_n^1 & w_n^2 & w_n^3 & \cdots & w_n^{n-1} \ w_n^0 & w_n^2 & w_n^4 & w_n^6 & \cdots & w_n^{2(n-1)} \ w_n^0 & w_n^3 & w_n^6 & w_n^9 & \cdots & w_n^{3(n-1)} \ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \ w_n^0 & w_n^{n-1} & w_n^{2(n-1)} & w_n^{3(n-1)} & \cdots & w_n^{(n-1)(n-1)} \end{pmatrix} (a0a1a2a3an1)\begin{pmatrix} a_0 \ a_1 \ a_2 \ a_3 \ \vdots \ a_{n-1} \end{pmatrix} = (y0y1y2y3yn1)\begin{pmatrix} y_0 \ y_1 \ y_2 \ y_3 \ \vdots \ y_{n-1} \end{pmatrix}

Esta matriz se llama matriz de Vandermonde.

Así, podemos calcular el vector (a0,a1,,an1)(a_0, a_1, \dots, a_{n-1}) multiplicando el vector (y0,y1,yn1)(y_0, y_1, \dots y_{n-1}) por la izquierda por la inversa de la matriz:

(a0a1a2a3an1)=(wn0wn0wn0wn0wn0wn0wn1wn2wn3wnn1wn0wn2wn4wn6wn2(n1)wn0wn3wn6wn9wn3(n1)wn0wnn1wn2(n1)wn3(n1)wn(n1)(n1))1(y0y1y2y3yn1) (a0a1a2a3an1)\begin{pmatrix} a_0 \ a_1 \ a_2 \ a_3 \ \vdots \ a_{n-1} \end{pmatrix} = (wn0amp;wn0amp;wn0amp;wn0amp;amp;wn0wn0amp;wn1amp;wn2amp;wn3amp;amp;wnn1wn0amp;wn2amp;wn4amp;wn6amp;amp;wn2(n1)wn0amp;wn3amp;wn6amp;wn9amp;amp;wn3(n1)amp;amp;amp;amp;amp;wn0amp;wnn1amp;wn2(n1)amp;wn3(n1)amp;amp;wn(n1)(n1))\begin{pmatrix} w_n^0 & w_n^0 & w_n^0 & w_n^0 & \cdots & w_n^0 \ w_n^0 & w_n^1 & w_n^2 & w_n^3 & \cdots & w_n^{n-1} \ w_n^0 & w_n^2 & w_n^4 & w_n^6 & \cdots & w_n^{2(n-1)} \ w_n^0 & w_n^3 & w_n^6 & w_n^9 & \cdots & w_n^{3(n-1)} \ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \ w_n^0 & w_n^{n-1} & w_n^{2(n-1)} & w_n^{3(n-1)} & \cdots & w_n^{(n-1)(n-1)} \end{pmatrix}^{-1} (y0y1y2y3yn1)\begin{pmatrix} y_0 \ y_1 \ y_2 \ y_3 \ \vdots \ y_{n-1} \end{pmatrix}

Una comprobación rápida verifica que la inversa de la matriz tiene la siguiente forma:

1n(wn0wn0wn0wn0wn0wn0wn1wn2wn3wn(n1)wn0wn2wn4wn6wn2(n1)wn0wn3wn6wn9wn3(n1)wn0wn(n1)wn2(n1)wn3(n1)wn(n1)(n1)) \frac{1}{n} (wn0amp;wn0amp;wn0amp;wn0amp;amp;wn0wn0amp;wn1amp;wn2amp;wn3amp;amp;wn(n1)wn0amp;wn2amp;wn4amp;wn6amp;amp;wn2(n1)wn0amp;wn3amp;wn6amp;wn9amp;amp;wn3(n1)amp;amp;amp;amp;amp;wn0amp;wn(n1)amp;wn2(n1)amp;wn3(n1)amp;amp;wn(n1)(n1))\begin{pmatrix} w_n^0 & w_n^0 & w_n^0 & w_n^0 & \cdots & w_n^0 \ w_n^0 & w_n^{-1} & w_n^{-2} & w_n^{-3} & \cdots & w_n^{-(n-1)} \ w_n^0 & w_n^{-2} & w_n^{-4} & w_n^{-6} & \cdots & w_n^{-2(n-1)} \ w_n^0 & w_n^{-3} & w_n^{-6} & w_n^{-9} & \cdots & w_n^{-3(n-1)} \ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \ w_n^0 & w_n^{-(n-1)} & w_n^{-2(n-1)} & w_n^{-3(n-1)} & \cdots & w_n^{-(n-1)(n-1)} \end{pmatrix}

Así obtenemos la fórmula:

ak=1nj=0n1yjwnkja_k = \frac{1}{n} \sum_{j=0}^{n-1} y_j w_n^{-k j}

Comparando esto con la fórmula para yky_k

yk=j=0n1ajwnkj,y_k = \sum_{j=0}^{n-1} a_j w_n^{k j},

notamos que estos problemas son casi los mismos, así que los coeficientes aka_k se pueden encontrar con el mismo algoritmo de divide y vencerás, al igual que la FFT directa, solo que en lugar de wnkw_n^k hay que usar wnkw_n^{-k}, y al final hay que dividir los coeficientes resultantes por nn.

Así, el cálculo de la DFT inversa es casi el mismo que el de la DFT directa, y también se puede realizar en tiempo O(nlogn)O(n \log n).

Implementación

Aquí presentamos una implementación recursiva simple de la FFT y de la FFT inversa, ambas en una sola función, porque la diferencia entre la FFT directa y la inversa es mínima. Para almacenar los números complejos usamos el tipo complex de la STL de C++.

using cd = complex<double>; 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; } }

A la función se le pasa un vector de coeficientes, y la función calculará la DFT o la DFT inversa y guardará el resultado otra vez en este vector. El argumento invert\text{invert} indica si hay que calcular la DFT directa o la inversa. Dentro de la función primero comprobamos si la longitud del vector es igual a uno; si es el caso, no hay que hacer nada. En caso contrario, dividimos el vector aa en dos vectores a0a0 y a1a1 y calculamos la DFT de ambos de forma recursiva. Luego inicializamos el valor wnwn y una variable ww, que contendrá la potencia actual de wnwn. Después se calculan los valores de la DFT resultante usando las fórmulas de arriba.

Si el flag invert\text{invert} está activado, reemplazamos wnwn por wn1wn^{-1}, y cada uno de los valores del resultado se divide por 22 (como esto se hace en cada nivel de la recursión, al final los valores quedarán divididos por nn).

Usando esta función podemos crear una función para multiplicar dos polinomios:

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 < a.size() + b.size()) n <<= 1; fa.resize(n); fb.resize(n); fft(fa, false); fft(fb, false); for (int i = 0; i < n; i++) fa[i] *= fb[i]; fft(fa, true); vector<int> result(n); for (int i = 0; i < n; i++) result[i] = round(fa[i].real()); return result; }

Esta función trabaja con polinomios de coeficientes enteros, aunque también se puede adaptar para otros tipos. Como hay cierto error al trabajar con números complejos, hay que redondear los coeficientes resultantes al final.

Finalmente, la función para multiplicar dos números largos prácticamente no se diferencia de la función para multiplicar polinomios. Lo único que hay que hacer después es normalizar el número:

int carry = 0; for (int i = 0; i < n; i++) result[i] += carry; carry = result[i] / 10; result[i] %= 10; }

Como la longitud del producto de dos números nunca supera la longitud total de ambos números, el tamaño del vector alcanza para realizar todas las operaciones de acarreo.

Implementación mejorada: cálculo in-place

Para aumentar la eficiencia pasaremos de la implementación recursiva a una iterativa. En la implementación recursiva de arriba separamos explícitamente el vector aa en dos vectores: los elementos en posiciones pares se asignaban a un vector temporal, y los de posiciones impares a otro. Sin embargo, si reordenamos los elementos de cierta forma, no hace falta crear esos vectores temporales (es decir, todos los cálculos se pueden hacer “in-place”, justo en el propio vector AA).

Nótese que en el primer nivel de recursión, los elementos cuyo bit más bajo de la posición era cero se asignaron al vector a0a_0, y los que tenían un uno como bit más bajo de la posición se asignaron a a1a_1. En el segundo nivel de recursión ocurre lo mismo, pero con el segundo bit más bajo, etc. Por lo tanto, si invertimos los bits de la posición de cada coeficiente y los ordenamos por esos valores invertidos, obtenemos el orden deseado (se llama permutación de inversión de bits, bit-reversal permutation).

Por ejemplo, el orden deseado para n=8n = 8 tiene la forma:

a={[(a0,a4),(a2,a6)],[(a1,a5),(a3,a7)]}a = \bigg{ \Big[ (a_0, a_4), (a_2, a_6) \Big], \Big[ (a_1, a_5), (a_3, a_7) \Big] \bigg}

En efecto, en el primer nivel de recursión (rodeado por llaves), el vector se divide en dos partes [a0,a2,a4,a6][a_0, a_2, a_4, a_6] y [a1,a3,a5,a7][a_1, a_3, a_5, a_7]. Como se ve, en la permutación de inversión de bits esto corresponde simplemente a dividir el vector en dos mitades: los primeros n2\frac{n}{2} elementos y los últimos n2\frac{n}{2} elementos. Luego hay una llamada recursiva para cada mitad. Sean las DFT resultantes de cada una devueltas en el lugar de los propios elementos (es decir, la primera mitad y la segunda mitad del vector aa, respectivamente).

a={[y00,y10,y20,y30],[y01,y11,y21,y31]}a = \bigg{ \Big[y_0^0, y_1^0, y_2^0, y_3^0\Big], \Big[y_0^1, y_1^1, y_2^1, y_3^1 \Big] \bigg}

Ahora queremos combinar las dos DFT en una para el vector completo. El orden de los elementos es ideal, y también podemos realizar la unión directamente en este vector. Podemos tomar los elementos y00y_0^0 e y01y_0^1 y aplicar la transformada mariposa. El lugar de los dos valores resultantes es el mismo que el de los dos valores iniciales, así que obtenemos:

a={[y00+wn0y01,y10,y20,y30],[y00wn0y01,y11,y21,y31]}a = \bigg{ \Big[y_0^0 + w_n^0 y_0^1, y_1^0, y_2^0, y_3^0\Big], \Big[y_0^0 - w_n^0 y_0^1, y_1^1, y_2^1, y_3^1\Big] \bigg}

De forma similar podemos calcular la transformada mariposa de y10y_1^0 e y11y_1^1 y poner los resultados en su lugar, y así sucesivamente. Como resultado obtenemos:

a={[y00+wn0y01,y10+wn1y11,y20+wn2y21,y30+wn3y31],[y00wn0y01,y10wn1y11,y20wn2y21,y30wn3y31]}a = \bigg{ \Big[y_0^0 + w_n^0 y_0^1, y_1^0 + w_n^1 y_1^1, y_2^0 + w_n^2 y_2^1, y_3^0 + w_n^3 y_3^1\Big], \Big[y_0^0 - w_n^0 y_0^1, y_1^0 - w_n^1 y_1^1, y_2^0 - w_n^2 y_2^1, y_3^0 - w_n^3 y_3^1\Big] \bigg}

Así calculamos la DFT requerida a partir del vector aa.

Aquí describimos el proceso de calcular la DFT solo en el primer nivel de recursión, pero lo mismo funciona obviamente también para todos los demás niveles. Así, después de aplicar la permutación de inversión de bits, podemos calcular la DFT in-place, sin memoria adicional.

Esto además nos permite deshacernos de la recursión. Simplemente empezamos en el nivel más bajo, es decir, dividimos el vector en pares y les aplicamos la transformada mariposa. Esto deja el vector aa con el trabajo del último nivel aplicado. En el paso siguiente dividimos el vector en vectores de tamaño 44, y otra vez aplicamos la transformada mariposa, lo que nos da la DFT de cada bloque de tamaño 44. Y así sucesivamente. Finalmente, en el último paso obtenemos el resultado de las DFT de ambas mitades de aa, y aplicando la transformada mariposa obtenemos la DFT del vector aa completo.

using cd = complex<double>; const double PI = acos(-1); int reverse(int num, int lg_n) { int res = 0; for (int i = 0; i < lg_n; i++) { if (num & (1 << i)) res |= 1 << (lg_n - 1 - i); } return res; } void fft(vector<cd> & a, bool invert) { int n = a.size(); int lg_n = 0; while ((1 << lg_n) < n) lg_n++; for (int i = 0; i < n; i++) { if (i < reverse(i, lg_n)) swap(a[i], a[reverse(i, lg_n)]); } 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; } }

Primero aplicamos la permutación de inversión de bits intercambiando cada elemento con el elemento de la posición invertida. Luego, en los logn1\log n - 1 estados del algoritmo, calculamos la DFT de cada bloque del tamaño correspondiente len\text{len}. Para todos esos bloques tenemos la misma raíz de la unidad wlen\text{wlen}. Iteramos todos los bloques y aplicamos la transformada mariposa en cada uno.

Podemos optimizar aún más la inversión de bits. En la implementación anterior iterábamos todos los bits del índice y creábamos el índice con los bits invertidos. Sin embargo, podemos invertir los bits de otra forma.

Supongamos que jj ya contiene el reverso de ii. Entonces, para pasar a i+1i + 1, hay que incrementar ii, y también hay que incrementar jj, pero en un sistema numérico “invertido”. Sumar uno en el sistema binario convencional equivale a invertir todos los unos de cola en ceros e invertir el cero justo antes de ellos en un uno. De forma equivalente, en el sistema numérico “invertido” invertimos todos los unos iniciales y también el siguiente cero.

Así obtenemos la siguiente implementación:

using cd = complex<double>; const double PI = acos(-1); void fft(vector<cd> & a, bool invert) { int n = a.size(); 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]); } 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; } }

Además, podemos precomputar de antemano la permutación de inversión de bits. Esto es especialmente útil cuando el tamaño nn es el mismo para todas las llamadas. Pero incluso cuando solo tenemos tres llamadas (que son las necesarias para multiplicar dos polinomios), el efecto se nota. También podemos precomputar todas las raíces de la unidad y sus potencias.

Transformada teórica de números

Ahora cambiamos un poco el objetivo. Seguimos queriendo multiplicar dos polinomios en tiempo O(nlogn)O(n \log n), pero esta vez queremos calcular los coeficientes módulo algún número primo pp. Por supuesto, para esta tarea podemos usar la DFT normal y aplicar el operador módulo al resultado. Sin embargo, hacer eso puede llevar a errores de redondeo, sobre todo al tratar con números grandes. La transformada teórica de números (NTT) tiene la ventaja de que solo trabaja con enteros y, por lo tanto, se garantiza que los resultados son correctos.

La transformada discreta de Fourier se basa en números complejos y en las raíces nn-ésimas de la unidad. Para calcularla de forma eficiente usamos de manera extensa propiedades de las raíces (por ejemplo, que hay una raíz que genera todas las demás por exponenciación).

Pero las mismas propiedades valen para las raíces nn-ésimas de la unidad en aritmética modular. Una raíz nn-ésima de la unidad en un cuerpo primo es un número wnw_n que cumple:

(wn)n=1(modp),(wn)k1(modp),1k<n.(wn)namp;=1(modp),(wn)kamp;1(modp),1klt;n.\begin{align} (w_n)^n &amp;= 1 \pmod{p}, \ (w_n)^k &amp;\ne 1 \pmod{p}, \quad 1 \le k &lt; n. \end{align}

Las otras n1n-1 raíces se pueden obtener como potencias de la raíz wnw_n.

Para aplicarlo en el algoritmo de la transformada rápida de Fourier, necesitamos que exista una raíz para algún nn que sea potencia de 22, y también para todas las potencias menores. Podemos notar la siguiente propiedad interesante:

(wn2)m=wnn=1(modp),with m=n2(wn2)k=wn2k1(modp),1k<m.(wn2)m=wnnamp;=1(modp),with m=n2(wn2)k=wn2kamp;1(modp),1klt;m.\begin{align} (w_n^2)^m = w_n^n &amp;= 1 \pmod{p}, \quad \text{with } m = \frac{n}{2}\ (w_n^2)^k = w_n^{2k} &amp;\ne 1 \pmod{p}, \quad 1 \le k &lt; m. \end{align}

Así, si wnw_n es una raíz nn-ésima de la unidad, entonces wn2w_n^2 es una raíz n2\frac{n}{2}-ésima de la unidad. Y en consecuencia, para todas las potencias de dos menores existen raíces del grado requerido, y se pueden calcular usando wnw_n.

Para calcular la DFT inversa necesitamos el inverso wn1w_n^{-1} de wnw_n. Pero para un módulo primo el inverso siempre existe.

Así, todas las propiedades que necesitamos de las raíces complejas también están disponibles en aritmética modular, siempre que tengamos un módulo pp lo bastante grande para el cual exista una raíz nn-ésima de la unidad.

Por ejemplo, podemos tomar los siguientes valores: módulo p=7340033p = 7340033, w220=5w_{2^{20}} = 5. Si este módulo no alcanza, hay que encontrar otro par. Podemos usar el hecho de que para módulos de la forma p=c2k+1p = c 2^k + 1 (y pp primo) siempre existe la raíz 2k2^k-ésima de la unidad. Se puede demostrar que gcg^c es una raíz 2k2^k-ésima de la unidad de ese tipo, donde gg es una raíz primitiva de pp.

const int mod = 7340033; const int root = 5; const int root_1 = 4404020; const int root_pw = 1 << 20; void fft(vector<int> & a, bool invert) { int n = a.size(); 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]); } for (int len = 2; len <= n; len <<= 1) { int wlen = invert ? root_1 : root; for (int i = len; i < root_pw; i <<= 1) wlen = (int)(1LL * wlen * wlen % mod); for (int i = 0; i < n; i += len) { int w = 1; for (int j = 0; j < len / 2; j++) { int u = a[i+j], v = (int)(1LL * a[i+j+len/2] * w % mod); a[i+j] = u + v < mod ? u + v : u + v - mod; a[i+j+len/2] = u - v >= 0 ? u - v : u - v + mod; w = (int)(1LL * w * wlen % mod); } } } if (invert) { int n_1 = inverse(n, mod); for (int & x : a) x = (int)(1LL * x * n_1 % mod); } }

Aquí la función inverse calcula el inverso modular (véase Inverso multiplicativo modular). Las constantes mod, root, root_pw determinan el módulo y la raíz, y root_1 es el inverso de root módulo mod.

En la práctica esta implementación es más lenta que la que usa números complejos (debido a la enorme cantidad de operaciones de módulo), pero tiene algunas ventajas como menor uso de memoria y ausencia de errores de redondeo.

Multiplicación con módulo arbitrario

Aquí queremos lograr el mismo objetivo que en la sección anterior. Multiplicar dos polinomios A(x)A(x) y B(x)B(x), y calcular los coeficientes módulo algún número MM. La transformada teórica de números solo funciona para ciertos números primos. ¿Qué pasa cuando el módulo no tiene la forma deseada?

Una opción sería realizar varias transformadas teóricas de números con distintos números primos de la forma c2k+1c 2^k + 1, y luego aplicar el Teorema Chino del Resto para calcular los coeficientes finales.

Otra opción es repartir los polinomios A(x)A(x) y B(x)B(x) en dos polinomios más pequeños cada uno

A(x)=A1(x)+A2(x)CB(x)=B1(x)+B2(x)CA(x)amp;=A1(x)+A2(x)CB(x)amp;=B1(x)+B2(x)C\begin{align} A(x) &amp;= A_1(x) + A_2(x) \cdot C \ B(x) &amp;= B_1(x) + B_2(x) \cdot C \end{align}

con CMC \approx \sqrt{M}.

Entonces el producto de A(x)A(x) y B(x)B(x) se puede representar como:

A(x)B(x)=A1(x)B1(x)+(A1(x)B2(x)+A2(x)B1(x))C+(A2(x)B2(x))C2A(x) \cdot B(x) = A_1(x) \cdot B_1(x) + \left(A_1(x) \cdot B_2(x) + A_2(x) \cdot B_1(x)\right)\cdot C + \left(A_2(x) \cdot B_2(x)\right)\cdot C^2

Los polinomios A1(x)A_1(x), A2(x)A_2(x), B1(x)B_1(x) y B2(x)B_2(x) contienen solo coeficientes menores que M\sqrt{M}; por lo tanto, los coeficientes de todos los productos que aparecen son menores que MnM \cdot n, lo cual suele ser lo bastante pequeño para manejarlo con tipos típicos de punto flotante.

Este enfoque, por lo tanto, requiere calcular los productos de polinomios con coeficientes más pequeños (usando la FFT normal y la FFT inversa), y luego el producto original se puede restaurar usando suma y multiplicación modular en tiempo O(n)O(n).

Aplicaciones

La DFT se puede usar en una enorme variedad de otros problemas que, a primera vista, no tienen nada que ver con multiplicar polinomios.

Todas las sumas posibles

Se nos dan dos arreglos a[]a[] y b[]b[]. Hay que encontrar todas las sumas posibles a[i]+b[j]a[i] + b[j] y, para cada suma, contar cuántas veces aparece.

Por ejemplo, para a=[1, 2, 3]a = [1,~ 2,~ 3] y b=[2, 4]b = [2,~ 4] obtenemos: la suma 33 se puede obtener de 11 forma, la suma 44 también de 11 forma, 55 de 22, 66 de 11, 77 de 11.

Construimos para los arreglos aa y bb dos polinomios AA y BB. Los números del arreglo actuarán como los exponentes en el polinomio (a[i]xa[i]a[i] \Rightarrow x^{a[i]}); y los coeficientes de ese término serán cuántas veces aparece el número en el arreglo.

Luego, al multiplicar estos dos polinomios en tiempo O(nlogn)O(n \log n), obtenemos un polinomio CC, donde los exponentes nos dirán qué sumas se pueden obtener, y los coeficientes nos dirán cuántas veces. Para demostrarlo en el ejemplo:

(1x1+1x2+1x3)(1x2+1x4)=1x3+1x4+2x5+1x6+1x7(1 x^1 + 1 x^2 + 1 x^3) (1 x^2 + 1 x^4) = 1 x^3 + 1 x^4 + 2 x^5 + 1 x^6 + 1 x^7

Todos los productos escalares posibles

Se nos dan dos arreglos a[]a[] y b[]b[] de longitud nn. Hay que calcular los productos de aa con cada desplazamiento cíclico de bb.

Generamos dos arreglos nuevos de tamaño 2n2n: Invertimos aa y le agregamos nn ceros. Y simplemente concatenamos bb consigo mismo. Cuando multiplicamos estos dos arreglos como polinomios, y miramos los coeficientes c[n1], c[n], , c[2n2]c[n-1],~ c[n],~ \dots,~ c[2n-2] del producto cc, obtenemos:

c[k]=i+j=ka[i]b[j]c[k] = \sum_{i+j=k} a[i] b[j]

Y como todos los elementos a[i]=0a[i] = 0 para ini \ge n:

c[k]=i=0n1a[i]b[ki]c[k] = \sum_{i=0}^{n-1} a[i] b[k-i]

Es fácil ver que esta suma es justamente el producto escalar del vector aa con el (k(n1))(k - (n - 1))-ésimo desplazamiento cíclico a la izquierda de bb. Así, estos coeficientes son la respuesta al problema, y aún así pudimos obtenerla en tiempo O(nlogn)O(n \log n). Nótese aquí que c[2n1]c[2n-1] también nos da el nn-ésimo desplazamiento cíclico, pero ese es el mismo que el 00-ésimo desplazamiento cíclico, así que no hace falta considerarlo por separado en la respuesta.

Dos franjas

Se nos dan dos franjas booleanas (arreglos cíclicos de valores 00 y 11) aa y bb. Queremos encontrar todas las formas de pegar la primera franja a la segunda de modo que en ninguna posición tengamos un 11 de la primera franja al lado de un 11 de la segunda.

El problema en realidad no se diferencia mucho del anterior. Pegar dos franjas significa simplemente que aplicamos un desplazamiento cíclico al segundo arreglo, y podemos pegar las dos franjas si el producto escalar de los dos arreglos es 00.

Matching de strings

Se nos dan dos strings, un texto TT y un patrón PP, formados por letras minúsculas. Hay que calcular todas las ocurrencias del patrón en el texto.

Creamos un polinomio para cada string (T[i]T[i] y P[I]P[I] son números entre 00 y 2525 correspondientes a las 2626 letras del alfabeto):

A(x)=a0x0+a1x1++an1xn1,n=TA(x) = a_0 x^0 + a_1 x^1 + \dots + a_{n-1} x^{n-1}, \quad n = |T|

con

ai=cos(αi)+isin(αi),αi=2πT[i]26.a_i = \cos(\alpha_i) + i \sin(\alpha_i), \quad \alpha_i = \frac{2 \pi T[i]}{26}.

Y

B(x)=b0x0+b1x1++bm1xm1,m=PB(x) = b_0 x^0 + b_1 x^1 + \dots + b_{m-1} x^{m-1}, \quad m = |P|

con

bi=cos(βi)isin(βi),βi=2πP[mi1]26.b_i = \cos(\beta_i) - i \sin(\beta_i), \quad \beta_i = \frac{2 \pi P[m-i-1]}{26}.

Nótese que con la expresión P[mi1]P[m-i-1] se invierte explícitamente el patrón.

Los coeficientes (m1+i)(m-1+i)-ésimos del producto de los dos polinomios C(x)=A(x)B(x)C(x) = A(x) \cdot B(x) nos dirán si el patrón aparece en el texto en la posición ii.

cm1+i=j=0m1ai+jbm1j=j=0m1(cos(αi+j)+isin(αi+j))(cos(βj)isin(βj))c_{m-1+i} = \sum_{j = 0}^{m-1} a_{i+j} \cdot b_{m-1-j} = \sum_{j=0}^{m-1} \left(\cos(\alpha_{i+j}) + i \sin(\alpha_{i+j})\right) \cdot \left(\cos(\beta_j) - i \sin(\beta_j)\right)

con αi+j=2πT[i+j]26\alpha_{i+j} = \frac{2 \pi T[i+j]}{26} y βj=2πP[j]26\beta_j = \frac{2 \pi P[j]}{26}

Si hay un match, entonces T[i+j]=P[j]T[i+j] = P[j], y por lo tanto αi+j=βj\alpha_{i+j} = \beta_j. Esto da (usando la identidad trigonométrica de Pitágoras):

cm1+i=j=0m1(cos(αi+j)+isin(αi+j))(cos(αi+j)isin(αi+j))=j=0m1cos(αi+j)2+sin(αi+j)2=j=0m11=mcm1+iamp;=j=0m1(cos(αi+j)+isin(αi+j))(cos(αi+j)isin(αi+j))amp;=j=0m1cos(αi+j)2+sin(αi+j)2=j=0m11=m\begin{align} c_{m-1+i} &amp;= \sum_{j = 0}^{m-1} \left(\cos(\alpha_{i+j}) + i \sin(\alpha_{i+j})\right) \cdot \left(\cos(\alpha_{i+j}) - i \sin(\alpha_{i+j})\right) \ &amp;= \sum_{j = 0}^{m-1} \cos(\alpha_{i+j})^2 + \sin(\alpha_{i+j})^2 = \sum_{j = 0}^{m-1} 1 = m \end{align}

Si no hay un match, entonces al menos un carácter es distinto, lo que hace que uno de los productos ai+1bm1ja_{i+1} \cdot b_{m-1-j} no sea igual a 11, y eso lleva a que el coeficiente cm1+imc_{m-1+i} \ne m.

Matching de strings con comodines

Esta es una extensión del problema anterior. Esta vez permitimos que el patrón contenga el carácter comodín **, que puede coincidir con cualquier letra posible. Por ejemplo, el patrón aca*c aparece en el texto abccaaccabccaacc en exactamente tres posiciones, en el índice 00, el índice 44 y el índice 55.

Creamos exactamente los mismos polinomios, excepto que ponemos bi=0b_i = 0 si P[mi1]=P[m-i-1] = *. Si xx es la cantidad de comodines en PP, entonces habrá un match de PP en TT en el índice ii si cm1+i=mxc_{m-1+i} = m - x.

Problemas de práctica