Polinomios: FFT, NTT y Operaciones Avanzadas

FFT

La Transformada Rápida de Fourier (FFT) se basa en números complejos, aunque no es esencial comprenderlos a fondo para su implementación práctica.

Ahora ya lo sabemos, ¿verdad?.

Reflexión

Consideremos cómo multiplicar dos polinomios \(f,g\). Un enfoque directo es con complejidad \(\mathcal O(n\times m)\), pero no parece optimizable de manera evidente.

Podemos pensar en trabajar desde el resultado: ya que el producto es un polinomio de grado \(N=n+m\), ¿por qué no interpolarlo directamente? Esto parece correcto, pero resulta en \(\mathcal O(N^2)\), ¡incluso más lento!

Propiedad Clave

Según la interpolación de Lagrange, podemos elegir cualquier \(N\) valores para evaluar. Seleccionar \(1\sim N\) no ayuda. Aquí se requiere imaginación: usar las raíces de la unidad de orden \(N\), específicamente sus potencias.

Método

Evaluación Polinomial

Esencialmente, es multiplicar un vector por una matriz:

[\begin{bmatrix} x_{0} \ x_{1} \ x_{2} \ \vdots \ x_{N-1} \end{bmatrix} \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \ 1 & \alpha & \alpha^{2} & \cdots & \alpha^{N-1} \ 1 & \alpha^{2} & \alpha^{4} & \cdots & \alpha^{2(N-1)} \ \vdots & \vdots & \vdots & \ddots & \vdots \ 1 & \alpha^{N-1} & \alpha^{2(N-1)} & \cdots & \alpha^{(N-1)(N-1)} \end{bmatrix} = \begin{bmatrix} X_{0} \ X_{1} \ X_{2} \ \vdots \ X_{N-1} \end{bmatrix} ]Sea \(\alpha\) una raíz de la unidad de orden \(N\), entonces la matriz tiene solo \(N\) elementos distintos, pero aún es costoso calcular.

Aprovechemos una propiedad: si \(N\) es par, \(\alpha^k=-\alpha^{k+\frac{N}{2}}\). Esto permite calcular \(f(\alpha^k),f(\alpha^{k+\frac{N}{2}})\) simultáneamente, reduciendo el trabajo a \(\frac{N}{2}\) evaluaciones. Sin embargo, si evaluamos \(f(\alpha^k)\) directamente, no hay mejora.

Separemos los términos pares e impares de \(f\):

[f_0(x)=a_0+a_2x^2...+a_{2d}x^{2d} ][f_1(x)=a_1x+a_3x^3...+a_{2k+1}x^{2k+1}=x(a_1+a_3x^2...+a^{2d+1}x^{2d}) ]Al sustituir \(\alpha^k \):

[f_0(\alpha^k)=a_0+a_2\alpha^{2k}+...a_{2d}\alpha^{2kd}=f_0(\alpha^{2k}) ]Análogamente,

[f_1(\alpha^k)=\alpha^{k}f_1(\alpha^{2k}) ]Nótese que \(\alpha_{N}^{2k}\) es equivalente a \(\alpha_{\frac{N}{2}}^k\) bajo la raíz de la unidad de orden \(\frac{N}{2}\), convirtiéndose en un subproblema.

[f(\alpha^k)=f_0(\alpha^{2k})+\alpha^kf_1(\alpha^{2k})=f'(\alpha_{\frac{N}{2}}^{k})+\alpha^kf''(\alpha_{\frac{N}{2}}^{k}) ]Las funciones \(f'(x)\) y \(f''(x)\) tienen la misma forma que \(f(x)\), pero con grado reducido a la mitad, permitiendo recursión.

Además,

[f(\alpha^{k+\frac{N}{2}})=f_0(\alpha^{2k})+\alpha^{k+\frac{N}{2}}f_1(\alpha^{2k})=f'(\alpha_{\frac{N}{2}}^{k})-\alpha^kf''(\alpha_{\frac{N}{2}}^{k}) ]Así, ambos valores se calculan juntos.

Recuperación del Polinomio

Actualmente solo evaluamos en raíces de la unidad; interpolar para obtener coeficientes parece \(\mathcal O(N^2)\). Dado que la evaluación multiplica por una matriz, la recuperación usa la matriz inversa.

[\begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \ 1 & \alpha & \alpha^{2} & \cdots & \alpha^{N-1} \ 1 & \alpha^{2} & \alpha^{4} & \cdots & \alpha^{2(N-1)} \ \vdots & \vdots & \vdots & \ddots & \vdots \ 1 & \alpha^{N-1} & \alpha^{2(N-1)} & \cdots & \alpha^{(N-1)(N-1)} \end{bmatrix} ]Su inversa es especial:

[\frac{1}{N} \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \ 1 & \alpha^{N-1} & \alpha^{2(N-1)} & \cdots & \alpha^{(N-1)(N-1)} \ 1 & \alpha^{(N-2)} & \alpha^{2(N-2)} & \cdots & \alpha^{(N-2)(N-1)} \ \vdots & \vdots & \vdots & \ddots & \vdots \ 1 & \alpha^{} & \alpha^{2} & \cdots & \alpha^{(N-1)} \end{bmatrix} ]Cada \(\alpha^k\) se transforma en su conjugado \(\alpha^{N-k}\). Entonces, se aplica la evaluación inversa.

Complejidad: \(\mathcal O(n\times \log n)\).

Implementación

Debido al uso de números de punto flotante, la versión recursiva es lenta. Analizando la esencia, se puede derivar una versión iterativa.

Primero, la FFT recursiva separa términos pares e impares en cada nivel. Al llegar al caso base, es asignar valores según la inversión binaria. Esto se puede implementar con un bucle.

Luego, la fusión usa la "optimización de mariposa", calculando in-situ \(\alpha^k\) y \(\alpha^{k+\frac{N}{2}}\) para mejor comprensión.

Código de ejemplo:

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e6+5;
const double dos_pi=2.0*acos(-1);
int n,m;
struct Complex{
	double real, imag;
	Complex operator + (const Complex &otro) const {
		return {real + otro.real, imag + otro.imag};
	}
	Complex operator - (const Complex &otro) const {
		return {real - otro.real, imag - otro.imag};
	}
	Complex operator * (const Complex &otro) const {
		return {real * otro.real - imag * otro.imag, real * otro.imag + imag * otro.real};
	}
} roots[maxn], polyA[maxn], polyB[maxn];

void fourierTransform(Complex arr[], int len, int dir){
	for(int i=0, j=0; i<len; i++){
		if(i < j) swap(arr[i], arr[j]);
		for(int k=len/2; j^=k, j<k; k>>=1);
	}
	roots[0] = {1,0};
	for(int step=1; step<len; step<<=1){
		double angle = 1.0/(step*2);
		Complex base = {cos(dos_pi*angle), dir*sin(dos_pi*angle)};
		for(int j=step*2-2; j>=0; j-=2){
			roots[j] = roots[j>>1];
			roots[j+1] = roots[j] * base;
		}
		for(int i=0; i<len; i+=(step<<1)){
			for(int j=i; j<i+step; j++){
				Complex temp1 = arr[j], temp2 = arr[j+step] * roots[j-i];
				arr[j] = temp1 + temp2, arr[j+step] = temp1 - temp2;
			}
		}
	}
	if(dir == -1){
		double invLen = 1.0/len;
		for(int i=0; i<len; i++){
			arr[i].real *= invLen, arr[i].imag *= invLen;
		}
	}
}
int main(){
	scanf("%d%d", &n, &m);
	for(int i=0; i<=n; i++){int val; scanf("%d", &val); polyA[i].real=val;}
	for(int i=0; i<=m; i++){int val; scanf("%d", &val); polyB[i].real=val;}
	int totalLen = n+m+1;
	if((1<<__lg(totalLen)) != totalLen) totalLen = (1<<(__lg(totalLen)+1));
	fourierTransform(polyA, totalLen, 1);
	fourierTransform(polyB, totalLen, 1);
	for(int i=0; i<totalLen; i++) polyA[i] = polyA[i] * polyB[i];
	fourierTransform(polyA, totalLen, -1);
	for(int i=0; i<=n+m; i++) printf("%d ", int(polyA[i].real+0.5));
	return 0;
}

NTT

Aplicación

Se utiliza para multiplicar polinomios con coeficientes módulo un primo.

Idea

Notamos que la raíz primitiva es análoga a la raíz de la unidad. Definimos la raíz de la unidad de orden \(N\) módulo \(m\) como \(\omega^N\), representada por \(g^{\frac{m-1}{N}}\), donde \(g\) es una raíz primitiva. Las propiedades son similares.

Dado que \(\varphi(m)=m-1\) se divide por \(2\) en cada paso, los módulos NTT suelen tener \(m-1\) con muchos factores de \(2\), como \(998244353=119\times 2^{23}+1\).

Código

El código es similar al FFT, con ajustes para aritmética modular.

int n,a[N],ff[N],f[N],g[N];
ll factorial[N], inverse[N], roots[N];
int fastPow(int base, int exp, int mod){
	int result = 1;
	while(exp > 0){
		if(exp & 1) result = (ll)result * base % mod;
		base = (ll)base * base % mod;
		exp >>= 1;
	}
	return result;
}
int modAdd(int x, int mod){
	return x >= mod ? x - mod : x;
}
void nttTransform(int arr[], int len, int dir){
	for(int i=0, j=0; i<len; i++){
		if(i < j) swap(arr[i], arr[j]);
		for(int k=len>>1; (j^=k) < k; k>>=1);
	}
	roots[0] = 1;
	for(int step=1; step<len; step<<=1){
		int step2 = step<<1;
		int base = fastPow(dir==1 ? 3 : 332748118, (mod-1)/step2, mod);
		for(int i=step2-2; i>=0; i-=2){
			roots[i] = roots[i>>1];
			roots[i+1] = roots[i] * base % mod;
		}
		for(int i=0; i<len; i+=step2){
			for(int j=i; j<i+step; j++){
				int val1 = arr[j], val2 = roots[j-i] * arr[j+step] % mod;
				arr[j] = modAdd(val1 + val2, mod);
				arr[j+step] = modAdd(val1 - val2 + mod, mod);
			}
		}
	}
	if(dir == -1){
		int invLen = fastPow(len, mod-2, mod);
		for(int i=0; i<len; i++){
			arr[i] = (ll)arr[i] * invLen % mod;
		}
	}
}

Multiplicación Polinomial con Módulo Arbitrario

Generalmente, el rango de valores es grande, FFT puede tener problemas de precisión, y el módulo no es NTT. Se requieren técnicas especiales.

NTT con Tres Módulos

Se puede usar NTT nueve veces con tres módulos de orden \(10^9\), calculando por separado y combinando con el Teorema Chino del Resto antes de aplicar el módulo arbitrario. Ventaja: conceptualmente simple. Desventaja: alto costo constante.

FFT con Coeficientes Separados

8 FFT

Dividimos cada coeficiente \(a_i\) de \(f,g\) en \(x\times 2^{15}+y\), y luego multiplicamos. Sea \(f=f_1\times 2^{15}+f_2\), similar para \(g\). Entonces \(f*g=f_2*g_2+(f_1*g_2+f_2*g_1)\times 2^{15}+f_1*g_1\times 4^{15}\). Requiere 8 FFT, menos eficiente que 9 NTT.

5 FFT

Aprovechando los números complejos, podemos unir \(f_1,f_2\) en la parte real e imaginaria de un polinomio: \(f'=f_1+f_2I\). Luego, \(f'*g_1\) y \(f'*g_2\) dan los componentes necesarios. Esto requiere 5 FFT, más rápido aún. Existe una variante con 4 FFT, no cubierta aquí.

FWT

Convolución xor/or/and

Sea \(\otimes\in{\operatorname{or},\operatorname{xor},\operatorname{and}}\), calculamos

[C_i=\sum_{j\otimes k=i}A_jB_k ]Análogo a FFT, convertimos a multiplicación punto a punto: \(FWT_C[i]=FWT_A[i]*FWT_B[i]\), luego transformamos inversamente.

Transformaciones específicas:

Convolución or

for(int step=1; step<len; step<<=1){
		int step2 = step<<1;
		for(int i=0; i<len; i+=step2){
			for(int j=i; j<i+step; j++){
				a[j+step] = dir==1 ? modAdd(a[j+step]+a[j], mod) : modAdd(a[j+step]-a[j]+mod, mod);
			}
		}
	}

Convolución and

for(int step=1; step<len; step<<=1){
		int step2 = step<<1;
		for(int i=0; i<len; i+=step2){
			for(int j=i; j<i+step; j++){
				a[j] = dir==1 ? modAdd(a[j]+a[j+step], mod) : modAdd(a[j]-a[j+step]+mod, mod);
			}
		}
	}

Convolución xor

for(int step=1; step<len; step<<=1){
		int step2 = step<<1;
		for(int i=0; i<len; i+=step2){
			for(int j=i; j<i+step; j++){
				int tempX = a[j], tempY = a[j+step];
				a[j] = modAdd(tempX+tempY, mod);
				a[j+step] = modAdd(tempX-tempY+mod, mod);
			}
		}
	}
	if(dir == -1){
		ll invLen = fastPow(len, mod-2, mod);
		for(int i=0; i<len; i++){
			a[i] = invLen * a[i] % mod;
		}
	}

Convolución de Subconjuntos

[C_S=\sum_{T \subseteq S} A_TB_{S-T} ]Se puede reformular como \(C_S=\sum_{i\cap j=\emptyset,i\cup j=S} A_iB_{j}\), similar a la convolución or pero con la condición de intersección vacía. Al expandir, obtenemos \(C_S=\sum_{|i|+|j|=|S|,i\cup j=S} A_iB_{j}\). Entonces, extendemos la convolución or con una dimensión adicional para el tamaño del conjunto, y combinamos.

for(int i=0; i<tamano; i++){
		cin >> a[ __builtin_popcount(i) ][i];
	}
	for(int i=0; i<tamano; i++){
		cin >> b[ __builtin_popcount(i) ][i];
	}
	for(int i=0; i<=n; i++){
		fwtTransform(a[i], tamano, 1);
		fwtTransform(b[i], tamano, 1);
	}
	for(int j=0; j<=n; j++){
		for(int k=0; k<=j; k++){
			for(int i=0; i<tamano; i++){
				c[j][i] = modAdd(c[j][i] + (long long)a[k][i] * b[j-k][i] % mod, mod);
			}
		}
	}
    for(int i=0; i<=n; i++){
		fwtTransform(c[i], tamano, -1);
	}

Operaciones Polinomiales Avanzadas

La mayoría usa NTT para multiplicación.

Inversión Polinomial

Para un polinomio \(F(x)\), encontrar \(G(x)\) tal que \(F(x)*G(x) \equiv 1 \mod x^n\) módulo un primo NTT \(p\).

Enfoque directo

Se puede hacer por recurrencia, complejidad \(\mathcal O(nm)\). La condición necesaria y suficiente es que el término constante de \(F(x)\) tenga inverso módulo \(p\).

Método de duplicación

Usamos duplicación. Sea \(n=2^k\). Asumamos que tenemos \(F(x)*G_m(x)\equiv 1 \mod x^m\). Queremos \(F(x)*G_{2m}(x)\equiv 1 \mod x^{2m}\).

Derivación:

Primero, \(G_{2m}(x)-G_m(x)\equiv 0 \mod x^{m} \). Al cuadrado:

[(G_{2m}(x)-G_m(x))^2\equiv 0 \mod x^{2m} ][G_{2m}(x)^2-2G_{2m}(x)*G_m(x)+G_m(x)^2\equiv 0 \mod x^{2m} ]Usando \(F(x)*G_{2m}(x)\equiv 1 \mod x^{2m} \), multiplicamos por \(F(x)\):

[G_{2m}^2(x)F(x)-2G_{2m}(x)G_m(x)F(x)+G_m(x)^2F(x)\equiv 0 \mod x^{2m} ][G_{2m}(x)-2G_m(x)+G_m(x)^2F(x)\equiv 0 \mod x^{2m} ][G_{2m}(x)\equiv 2G_m(x)-G_m(x)^2F(x) \mod x^{2m} ]El lado derecho es conocido; se calcula con NTT. Complejidad por duplicación: \(\mathcal O(m\log m)\), total \(\mathcal O(n\log n)\).

Optimización: \(G_{2m}(x)\equiv G_m(x)(2-G_m(x)F(x)) \mod x^{2m} \). Notar que \(G_m(x)^2F(x)\) tiene grado hasta \(4m-3\), requiere longitud \(4m\).

Optimización adicional

El método anterior usa 3 NTT de longitud \(4m\). Se puede optimizar. Dividimos \(G_m(x)^2*F(x)\) en dos pasos: primero \(C(x)=F(x)*G_m(x)\mod x^{2m}\), luego \(D(x)=C(x)*G_m(x)\mod x^{2m}\). Observamos que \(C[0]=1\) y \(C[1..m-1]=0\), así que solo necesitamos calcular \(C[m..2m-1]\). Análogo para \(D(x)\). En total, 5 NTT de longitud \(2m\).

División Polinomial

Dados \(F(x)\) de grado \(n\) y \(G(x)\) de grado \(m\), encontrar \(Q(x), R(x)\) con \(Q\) de grado \(n-m\) y \(R\) de grado \([F_R(x)=Q_R(x)*G_R(x)+x^{n-m+1}*R_R(x) ]Modulando por \(x^{n-m+1}\) eliminamos \(R_R(x)\):

[F_R(x)=Q_R(x)*G_R(x)\mod x^{n-m+1} ][Q_R(x)=F_R(x)*G_R^{-1}(x)\mod x^{n-m+1} ]Se calcula la inversa de \(G_R(x)\), se obtiene \(Q_R(x)\), se invierte para \(Q(x)\), y luego \(R(x)=F(x)-G(x)*Q(x)\). Complejidad \(\mathcal O(n\log n)\).

Recurrencia Lineal Homogénea de Coeficientes Constantes

Calcular el \(n\)-ésimo término de una secuencia \(f_i\) que satisface:

[f_n=\sum_{i=1}^k a_if_{k-i} ]### Exponenciación matricial

Directamente con matrices, complejidad \(\mathcal O(k^3\log n)\). Optimización: notar que \(f_j\) se puede expresar como combinación lineal de \(f_1..f_k\). El proceso de expansión se relaciona con reducir potencias de polinomios. Específicamente, \(x^n \mod x^k-\sum_{i=1}^k a_ix^{k-i}\). Se puede usar exponenciación rápida con división polinomial. Si el módulo es NTT, complejidad \(\mathcal O(k\log k\log n)\); si no, con división larga y multiplicación directa, \(\mathcal O(k^2\log n)\).

Raíz Cuadrada Polinomial

Encontrar \(G(x)^2\equiv F(x)\mod x^n\).

Enfoque directo

Similar a la inversión, se puede hacer por recurrencia si \(F[0]\) tiene raíz cuadrada módulo \(p\).

Método de duplicación

Análogo a la inversión. Asumamos \(G_m^2(x)\equiv F(x)\mod x^m\). Queremos \(G_{2m}(x)^2\equiv F(x)\mod x^{2m}\). Como \(G_{2m}(x)-G_m(x)\equiv 0 \mod x^m \), al cuadrado y manipulando:

[G_{2m}(x)=\frac{F(x)+G_m(x)^2}{2G_m(x)}\mod x^{2m} ]Requiere inversión, complejidad \(\mathcal O(n\log n)\).

Evaluación Multi-Punto Polinomial

Para un polinomio \(F(x)\) de grado \(n\), calcular \(F(a_1),F(a_2),\dots,F(a_m)\).

Divide y vencerás

Observar que \(F(a_i)\equiv F(x)\mod x-a_i\). Usamos división recursiva. Para un intervalo \([l,r]\), tenemos \(F'(x)\), calculamos \(F'(x)\mod \prod_{i=l}^r (x-a_i)\) y recursamos. El producto se precalcula con NTT dividido, complejidad \(\mathcal O(n\log^2 n)\). Total \(\mathcal O(n\log^2 n)\).

Logaritmo Polinomial

Repaso de límites, derivadas e integrales...

Dados \(F(x)\), encontrar \(G(x)\equiv \ln F(x)\mod x^n\). Requiere \(F[0]=1\).

Sea \(f(x)=\ln x\), entonces \(G(x)=f(F(x))\). Derivando: \(G'(x)\equiv \frac{F'(x)}{F(x)}\mod x^n\). Calculamos \(G'(x)\) y luego integramos.

Derivada polinomial: \(F'(x)=\sum_{i=0}^{n-1}(i+1)F[i+1]x^i\). Integral: \(\int F(x)=\sum_{i=0}^{n}\frac{F'[i]x^{i+1}}{i+1}\). Entonces \(G(x)=\int \frac{F'(x)}{F(x)}\mod x^n\).

Exponencial Polinomial

Dados \(A(x)\), encontrar \(B(x)\equiv e^{A(x)}\mod x^n\). Requiere \(A[0]=0\).

Toma logaritmo: \(\ln B(x)\equiv A(x)\mod x^n\). Usamos iteración de Newton.

No necesitamos saber la demostración, solo aplicar la fórmula.

Sea \(G(x)=\ln x - A(x)\), donde \(x\) es un polinomio y \(A(x)\) es constante. Aplicamos la fórmula:

[B_{2m}(x)\equiv B_m(x)-\frac{G(B_m(x))}{G'(B_m(x))}\mod x^{2m} ][B_{2m}(x)\equiv B_m(x)-\frac{\ln B_m(x)-A(x)}{1/B_m(x)}\mod x^{2m} ][B_{2m}(x)\equiv B_m(x)-B_m(x)(\ln B_m(x)-A(x))\mod x^{2m} ][B_{2m}(x)\equiv B_m(x)(1-\ln B_m(x)+A(x))\mod x^{2m} ]

Potencia Rápida Polinomial

Dados \(A(x),k\), encontrar \(B(x)\equiv A(x)^k\mod x^n\). \(k\) es un número grande.

Exponenciación binaria directa

Complejidad \(O(n\log n\log k)\), no óptimo si \(k\) es muy grande.

Usando ln y exp

Notar que \(\ln B(x)\equiv k\ln A(x)\mod x^n\), entonces \(B(x)=e^{k\ln A(x)}\). Requiere \(A[0]=1\). Si no es así, ajustamos: sea \(A(x)=px^qC(x)\) con \(C[0]=1\). Entonces \(B(x)\equiv p^kx^{qk}e^{k\ln C(x)}\mod x^n\). Nota: \(p^k\) módulo \(\varphi(p)\), y si \(qk\ge n\), \(B(x)=0\).

Interpolación Rápida

Divide y vencerás

Interpolación de Lagrange rápida. Complejiadd \(\mathcal O(n\log^2 n)\).

La fórmula es:

[F(x)=\sum_{i=1}^{n}y_i\prod_{j\neq i}\frac{x-x_j}{x_i-x_j} ]Reescribimos como:

[F(x)=\sum_{i=1}^{n}\frac{y_i}{\prod_{k\neq i}(x_i-x_k)}\prod_{j\neq i}(x-x_j) ]Sea \(G_i(x)=\prod_{j\neq i}(x-x_j)\), entonces \(\prod_{k\neq i}(x_i-x_k)=G_i(x_i)\). Definimos \(G(x)=\sum_{i=1}^{n}G_i(x)\), que se calcula con NTT dividido. Evaluamos \(G(x_1),\dots,G(x_n)\) con evaluación multi-punto. Finalmente, \(F(x)=\sum_{i=1}^{n}\frac{y_i}{G(x_i)}G_i(x)\), calculable con NTT dividido.

Optimización

El método anterior tiene alta constante. Observando por derivadas: si \(W(x)=\prod_{i=1}^{n}(x-x_i)\), entonces \(W'(x)=\sum_{i=1}^{n}\prod_{j\neq i}(x-x_j)=G(x)\). Esto reduce la constante en aproximadamente un cuarto.

Código

Se incluye código para inversión, división y raíz cuadrada. Se usa vector para operaciones polinomiales, con resize para manejo dinámico.

[Insertar código aquí]

Etiquetas: fft NTT Polinomios Multiplicación de Polinomios Transformada de Fourier

Publicado el 6-10 16:18