P3803 【模板】多项式乘法(FFT)

这篇博客介绍了如何利用快速傅里叶变换(FFT)算法高效地计算两个多项式的卷积。首先,介绍了复数和单位根的概念,然后详细阐述了FFT的工作原理,包括单位根的性质和递归实现。接着,提到了快速傅里叶逆变换(IFFT)在将点值表示法转换为系数表示法中的作用。最后,通过迭代实现的方式展示了如何在实际编程中应用FFT算法。
摘要由CSDN通过智能技术生成


更 好 的 阅 读 体 验 \large更好的阅读体验

原题链接

P3811
AC记录:Accepted

题目大意

给定一个 n n n 次多项式 F ( x ) F(x) F(x),和一个 m m m 次多项式 G ( x ) G(x) G(x)
请求出 F ( x ) F(x) F(x) G ( x ) G(x) G(x) 的卷积。

输入格式

第一行两个整数 n , m n,m n,m
接下来一行 n + 1 n+1 n+1 个数字,从低到高表示 F ( x ) F(x) F(x) 的系数。
接下来一行 m + 1 m+1 m+1 个数字,从低到高表示 G ( x ) G(x) G(x) 的系数。

输出格式

一行 n + m + 1 n+m+1 n+m+1 个数字,从低到高表示 F ( x ) ⋅ G ( x ) F(x) \cdot G(x) F(x)G(x) 的系数。

S a m p l e \mathbf{Sample} Sample I n p u t \mathbf{Input} Input

1 2
1 2
1 2 1

S a m p l e \mathbf{Sample} Sample O u t p u t \mathbf{Output} Output

1 4 5 2

H i n t & E x p l a i n \mathbf{Hint\&Explain} Hint&Explain
样例如图所示。
其中粉色虚线为 F ( x ) F(x) F(x),绿色虚线为 G ( x ) G(x) G(x),蓝色实线为 F ( x ) ⋅ G ( x ) F(x)\cdot G(x) F(x)G(x)
HG6BvD.png

数据范围

保证输入中的系数大于等于 0 0 0 且小于等于 9 9 9
对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 10 6 1 \le n, m \leq {10}^6 1n,m106

解题思路

温馨提示 此题解的字数有亿点点多,思维难度也会偏难,请读者 做好准备。 { \def\fcol{#FF9100} \def\scol{#FFF4E5} \def\sidelength{50pt} \def\titlehigh{30pt} \def\titlewordhigh{37pt} \color{\fcol}{\rule[0pt]{2pt}{\sidelength}} \color{\scol}{\rule[\titlehigh]{200pt}{20pt}} \kern{-200pt} \color{#FFFFFF}{\rule[0pt]{200pt}{\titlehigh}} \color{\fcol}{\raisebox{\titlewordhigh}{\kern{-195pt}{\footnotesize\bf 温馨提示}}} \color{black} % 在raisebox{ /here/ }{}修改文字高度,底线为7pt,依此减少10pt {\raisebox{17pt}{\kern{-195pt}{\footnotesize\bf 此题解的字数有亿点点多,思维难度也会偏难,请读者}}} {\raisebox{7pt}{\kern{-195pt}{\footnotesize\bf 做好准备。}}} } 亿


题目都告诉你了,是用FFT啊。

在介绍FFT之前,你需要知道一些前置的知识。


前置知识

1. \texttt{1.} 1.复数
复数由两部分组成:实部和虚部。设 a , b a,b a,b 为实数, i = − 1 i=\sqrt{-1} i=1 ,则形如 a + b i a+bi a+bi 的数叫做复数。

2. \texttt{2.} 2.复数的运算法则
设有两个复数为 a + b i a+bi a+bi c + d i c+di c+di

加法:
( a + b i ) + ( c + d i ) = a + b i + c + d i = ( a + c ) + ( b + d ) i \begin{matrix} (a+bi)+(c+di) \\[5pt] =a+bi+c+di \\[5pt] =(a+c)+(b+d)i \end{matrix} (a+bi)+(c+di)=a+bi+c+di=(a+c)+(b+d)i
其实就是把实部和虚部分别相加就可以了。

乘法:
( a + b i ) ( c + d i ) = a c + a d i + b c i + b d i 2 = a c + ( a d + b c ) i − b d s = ( a c − b d ) + ( a d + b c ) i \begin{matrix} (a+bi)(c+di) \\[5pt] =ac+adi+bci+bdi^2 \\[5pt] =ac+(ad+bc)i-bds \\[5pt] =(ac-bd)+(ad+bc)i \end{matrix} (a+bi)(c+di)=ac+adi+bci+bdi2=ac+(ad+bc)ibds=(acbd)+(ad+bc)i

几何定义:复数相乘,模长相乘,幅角相加。

Attention   :   \color{red}\texttt{Attention : } Attention : 以下内容默认 n n n 2 2 2 的正整数次幂。

3. \texttt{3.} 3.单位根
在复平面( x x x 轴代表实部, y y y 轴代表虚部)上画一个单位圆(半径为 1 1 1 的圆),以圆点为起点,圆的 n n n 等分点为终点,做 n n n 个向量,设幅角为正且最小的向量对应的复数为 ω n \omega_n ωn,称为 n n n 次单位根。下图所示的是 3 3 3 次单位根。
HGXDtP.png
根据复数乘法的运算法则,不难推出剩下的 n − 1 n-1 n1 个单位根为 ω n 2 , ω n 3 , ⋯   , ω n n \omega^2_n,\omega^3_n,\cdots,\omega^n_n ωn2,ωn3,,ωnn
注意: ω n 0 = ω n n \omega^0_n=\omega^n_n ωn0=ωnn

幅角:假设以逆时针为正方向,从 x x x轴正半轴到已知向量的转角的有向角叫做幅角

4. \texttt{4.} 4.单位根的性质

  • 由欧拉公式可得, ω n k = cos ⁡ k 2 π n + i sin ⁡ k 2 π n \omega^k_n=\cos k\frac{2\pi}{n}+i\sin k\frac{2\pi}{n} ωnk=coskn2π+isinkn2π
  • ∀ a , ω a n a k = ω n k \forall a,\omega^{ak}_{an}=\omega^k_n a,ωanak=ωnk
    证明:
    ω a n a k = cos ⁡ a k ⋅ 2 π a n + i sin ⁡ a k 2 π a n = cos ⁡ k ⋅ 2 π n + i sin ⁡ k 2 π n = ω n \begin{matrix} \omega^{ak}_{an}=\cos ak\cdot\frac{2\pi}{an}+i\sin ak\frac{2\pi}{an} \\[5pt] =\cos k\cdot\frac{2\pi}{n}+i\sin k\frac{2\pi}{n} \\[5pt] =\omega_n \end{matrix} ωanak=cosakan2π+isinakan2π=coskn2π+isinkn2π=ωn
  • ω n k + n 2 = − ω n k \omega^{k+\frac{n}{2}}_{n}=-\omega^k_n ωnk+2n=ωnk
    证明:
    ω n k + n 2 = ω n k ⋅ ω n n 2 = ω n k ⋅ ( cos ⁡ n 2 ⋅ 2 π n + i sin ⁡ n 2 ⋅ 2 π n ) = ω n k ⋅ ( cos ⁡ π + i sin ⁡ π ) = ω n k ⋅ ( − 1 ) = − ω n k \begin{matrix} \omega^{k+\frac{n}{2}}_{n}=\omega^k_n\cdot\omega^{\frac{n}{2}}_n \\[5pt] =\omega^k_n\cdot\left(\cos\frac{n}{2}\cdot\frac{2\pi}{n}+i\sin\frac{n}{2}\cdot\frac{2\pi}{n}\right) \\[5pt] =\omega^k_n\cdot\left(\cos\pi+i\sin\pi\right) \\[5pt] =\omega^k_n\cdot(-1) \\[5pt] =-\omega^k_n \end{matrix} ωnk+2n=ωnkωn2n=ωnk(cos2nn2π+isin2nn2π)=ωnk(cosπ+isinπ)=ωnk(1)=ωnk

接下来,就是重头戏FFT了。


快速傅里叶变换(FFT)

设有一个多项式 A ( x ) A(x) A(x),他的系数为 ( a 0 , a 1 , a 2 , ⋯   , a n − 1 ) (a_0,a_1,a_2,\cdots,a_{n-1}) (a0,a1,a2,,an1)
那么
A ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 + ⋯ + a n − 1 x n − 1 A(x)=a_0+a_1x+a_2x^2+a_3x^3+\cdots+a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2+a3x3++an1xn1
按照 x x x 的次数的奇偶性分类,有
A ( x ) = ( a 0 + a 2 x 2 + ⋯ + a n − 2 x n − 2 ) + ( a 1 x + a 3 x 3 + ⋯ + a n − 1 x n − 1 ) A(x)=(a_0+a_2{x^2}+\cdots+a_{n-2}x^{n-2})+(a_1x+a_3{x^3}+\cdots+a_{n-1}x^{n-1}) A(x)=(a0+a2x2++an2xn2)+(a1x+a3x3++an1xn1)

A 1 ( x ) = a 0 + a 2 x + ⋯ + a n − 2 x n 2 − 1 A 2 ( x ) = a 1 + a 3 x + ⋯ + a n − 1 x n 2 − 1 A_1(x)=a_0+a_2x+\cdots+a_{n-2}x^{\frac n2-1} \\[5pt] A_2(x)=a_1+a_3x+\cdots+a_{n-1}x^{\frac n2-1} A1(x)=a0+a2x++an2x2n1A2(x)=a1+a3x++an1x2n1
不难得到
A ( x ) = A 1 ( x 2 ) + x A 2 ( x 2 ) A(x)=A_1(x^2)+xA_2(x^2) A(x)=A1(x2)+xA2(x2)
接下来就是把单位根代入了。
但是,不要着急,先把前半部分代进去。
ω n k ( k < n 2 ) \omega^k_n(k<\frac n2) ωnk(k<2n) 代入原式,有
A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega^k_n)=A_1(\omega^{2k}_n)+\omega^k_nA_2(\omega^{2k}_n) A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)
再把后半部分代进去。
ω n k + n 2 ( k < n 2 ) \omega^{k+\frac n2}_n(k<\frac n2) ωnk+2n(k<2n) 代入原式,有
A ( ω n k + n 2 ) = A 1 ( ω n 2 ( k + n 2 ) ) + ω n k + n 2 A 2 ( ω n 2 ( k + n 2 ) ) = A 1 ( ω n 2 k + n ) − ω n k A 2 ( ω n 2 k + n ) = A 1 ( ω n 2 k ⋅ ω n n ) − ω n k A 2 ( ω n 2 k ⋅ ω n n ) = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) A(\omega^{k+\frac n2}_n)=A_1(\omega^{2\left(k+\frac n2\right)}_n)+\omega^{k+\frac n2}_nA_2(\omega^{2(k+\frac n2)}_n) \\[5pt] =A_1(\omega^{2k+n}_n)-\omega^k_nA_2(\omega^{2k+n}_n) \\[5pt] =A_1(\omega^{2k}_n\cdot\omega^n_n)-\omega^k_nA_2(\omega^{2k}_n\cdot\omega^n_n) \\[5pt] =A_1(\omega^{2k}_n)-\omega^k_nA_2(\omega^{2k}_n) A(ωnk+2n)=A1(ωn2(k+2n))+ωnk+2nA2(ωn2(k+2n))=A1(ωn2k+n)ωnkA2(ωn2k+n)=A1(ωn2kωnn)ωnkA2(ωn2kωnn)=A1(ωn2k)ωnkA2(ωn2k)
发现没有,这两个式子只有一个常数项不同!
由于 k k k 在取遍 [ 0 , n 2 − 1 ] \left[0,\frac n2-1\right] [0,2n1] 的时候, k + n 2 k+\frac n2 k+2n 取遍了 [ n 2 , n − 1 ] \left[\frac n2,n-1\right] [2n,n1]。所以,我们在求第一部分的时候,也可以同时求出第二部分的值。
直接把问题缩小了一半!
直接递归求解就好了。
时间复杂度: Θ ( n log ⁡ 2 n ) \Theta(n\log_2n) Θ(nlog2n)


快速傅里叶逆变换(IFFT)

不要以为FFT就结束了。
刚才FFT是基于 点值表示法 的。
但题目里面说的是要 系数表示法 。所以要把点值表示法转换成系数表示法,这个过程就叫 傅里叶逆变换
( y 0 , y 1 , ⋯   , y n − 1 ) (y_0,y_1,\cdots,y_{n-1}) (y0,y1,,yn1) ( a 0 , a 1 , ⋯   , a n − 1 ) (a_0,a_1,\cdots,a_{n-1}) (a0,a1,,an1) 的傅里叶变换(就是点值表示法)。
又设 ( c 0 , c 1 , ⋯   , c n − 1 ) (c_0,c_1,\cdots,c_{n-1}) (c0,c1,,cn1) ( y 0 , y 1 , ⋯   , y n − 1 ) (y_0,y_1,\cdots,y_{n-1}) (y0,y1,,yn1) ( ω n 0 , ω n − 1 , ⋯   , ω n − ( n − 1 ) ) (\omega^0_n,\omega^{-1}_n,\cdots,\omega^{-(n-1)}_n) (ωn0,ωn1,,ωn(n1)) 的点值表示,即
c k = ∑ i = 0 n − 1 y i ( ω n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n i ) j ) ( ω n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( ω n j ) i ( ω n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( ω n j − k ) i = ∑ i = 0 n − 1 a j ( ∑ j = 0 n − 1 ( ω n j − k ) i ) c_k=\sum_{i=0}^{n-1}y_i(\omega^{-k}_n)^i \\[5pt] =\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega^i_n)^j)(\omega^{-k}_n)^i \\[5pt] =\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega^j_n)^i(\omega^{-k}_n)^i \\[5pt] =\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega^{j-k}_n)^i \\[5pt] =\sum_{i=0}^{n-1}a_j(\sum_{j=0}^{n-1}(\omega^{j-k}_n)^i) ck=i=0n1yi(ωnk)i=i=0n1(j=0n1aj(ωni)j)(ωnk)i=i=0n1j=0n1aj(ωnj)i(ωnk)i=i=0n1j=0n1aj(ωnjk)i=i=0n1aj(j=0n1(ωnjk)i)
先到这里,让我们换一个地方。
S ( x ) = ∑ i = 0 n − 1 x i S(x)=\sum\limits_{i=0}^{n-1}x^i S(x)=i=0n1xi
k ≠ 0 k\ne 0 k=0 时,把 ω n k \omega^k_n ωnk 代入得
S ( ω n k ) = 1 + ω n k + ( ω n k ) 2 + ⋯ + ( ω n k ) n − 1 ( 1 ) S(\omega^k_n)=1+\omega^k_n+(\omega^k_n)^2+\cdots+(\omega^k_n)^{n-1} \kern{30pt}(1) S(ωnk)=1+ωnk+(ωnk)2++(ωnk)n1(1)
( 1 ) × ω n k (1)\times\omega^k_n (1)×ωnk,得
ω n k S ( ω n k ) = ω n k + ( ω n k ) 2 + ⋯ + ( ω n k ) n ( 2 ) \omega^k_nS(\omega^k_n)=\omega^k_n+(\omega^k_n)^2+\cdots+(\omega^k_n)^n \kern{45pt}(2) ωnkS(ωnk)=ωnk+(ωnk)2++(ωnk)n(2)
( 2 ) − ( 1 ) (2)-(1) (2)(1),得
ω n k S ( ω n k ) − S ( ω n k ) = ( ω n k ) n − 1 ( ω n k − 1 ) S ( ω n k ) = ( ω n n ) k − 1 S ( ω n k ) = ( ω n n ) k − 1 ω n k − 1 S ( ω n k ) = 1 − 1 ω n k − 1 S ( ω n k ) = 0 \omega^k_nS(\omega^k_n)-S(\omega^k_n)=(\omega^k_n)^n-1 \\[5pt] (\omega^k_n-1)S(\omega^k_n)=(\omega^n_n)^k-1 \\[5pt] S(\omega^k_n)=\frac{(\omega^n_n)^k-1}{\omega^k_n-1} \\[5pt] S(\omega^k_n)=\frac{1-1}{\omega^k_n-1} \\[5pt] S(\omega^k_n)=0 ωnkS(ωnk)S(ωnk)=(ωnk)n1(ωnk1)S(ωnk)=(ωnn)k1S(ωnk)=ωnk1(ωnn)k1S(ωnk)=ωnk111S(ωnk)=0
所以,当 k ≠ 0 k\ne 0 k=0 时, S ( ω n k ) = 0 S(\omega^k_n)=0 S(ωnk)=0
那当 k = 0 k=0 k=0 时呢?
很显然,当 k = 0 k=0 k=0 时, S ( ω n k ) = n S(\omega^k_n)=n S(ωnk)=n。因为此时 ω n k = 1 \omega^k_n=1 ωnk=1,而又因为 1 n = 1 , n ∈ Z 1^n=1,n\in\Z 1n=1,nZ,而这样的项共有 n n n 项,所以整个式子的值为 n × 1 = n n\times 1=n n×1=n

回到原式,继续考虑
c k = ∑ i = 0 n − 1 a j ( ∑ j = 0 n − 1 ( ω n j − k ) i ) c_k=\sum_{i=0}^{n-1}a_j(\sum_{j=0}^{n-1}(\omega^{j-k}_n)^i) ck=i=0n1aj(j=0n1(ωnjk)i)
由上面的式子可以得出,当 j ≠ k j\ne k j=k 时, ∑ j = 0 n − 1 ( ω n j − k ) i = 0 \sum\limits_{j=0}^{n-1}(\omega^{j-k}_n)^i=0 j=0n1(ωnjk)i=0。当 j = k j=k j=k 时, ∑ j = 0 n − 1 ( ω n j − k ) i = n \sum\limits_{j=0}^{n-1}(\omega^{j-k}_n)^i=n j=0n1(ωnjk)i=n。因此,
c k = n a k a k = 1 n c k c_k=na_k \\[5pt] a_k=\frac1nc_k ck=nakak=n1ck
由于 ( c 0 , c 1 , ⋯   , c n − 1 ) (c_0,c_1,\cdots,c_{n-1}) (c0,c1,,cn1) ( y 0 , y 1 , ⋯   , y n − 1 ) (y_0,y_1,\cdots,y_{n-1}) (y0,y1,,yn1) ( ω n 0 , ω n − 1 , ⋯   , ω n − ( n − 1 ) ) (\omega^0_n,\omega^{-1}_n,\cdots,\omega^{-(n-1)}_n) (ωn0,ωn1,,ωn(n1)) 的点值表示,所以相当于又做了一次FFT。


迭代实现

尽管你用了上面这么多字之精华写出来了一串代码,但是,你交到洛谷上面去,你还是会 WA   77pts \color{E74C3C}\texttt{WA }\color{FADB14}\texttt{77pts} WA 77pts。这时候就需要迭代来实现了。
盗用一下某位大佬的图

发现了吗?我们实际上依此操作的元素,实际上就是原来序列的下标的二进制反转得到的!
所以现在的难题就是:如何得到操作后的序列呢?
我们像,对于某个数 x x x 来说,他的原序列,是由 x 2 \frac x2 2x 左移一位再加上最后一位的特判得到的。那么在操作后的序列中也一样,他是由 x 2 \frac x2 2x 右移一位再加上最后一位(也就是第一位)的特判得到的。因此,我们就可以在 Θ ( n ) \Theta(n) Θ(n) 的时间内求出 [ 1 , n ] [1,n] [1,n] 操作后的序列所对应的数了。这里给出转移公式:
设数 x x x k k k 位二进制下操作后的序列所对应的数为 r x r_x rx,有
r x = ⌊ r x 2 2 ⌋ + [ x   m o d   2 = 1 ] ⋅ 2 k − 1 r_x=\left\lfloor\frac{r_{\frac x2}}{2}\right\rfloor+[x\bmod 2=1]\cdot 2^{k-1} rx=2r2x+[xmod2=1]2k1
其中 [ x ] [x] [x] 代表当 x x x 成立时 [ x ] = 1 [x]=1 [x]=1,不成立时 [ x ] = 0 [x]=0 [x]=0

上代码

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

struct Complex{
    Complex(double a=0.00,double b=0.00):real(a),imag(b){}
    double real,imag;
};

const double    pi=acos(-1.00);
Complex         a[4000010],b[4000010];
int             resort[4000010];
int             n,m,lim,dig;

Complex operator + (Complex a,Complex b)
{
    return Complex(a.real+b.real,a.imag+b.imag);
}

Complex operator - (Complex a,Complex b) {
    return Complex(a.real-b.real,a.imag-b.imag);
}

Complex operator * (Complex a,Complex b)
{
    double real,imag;
    real=a.real*b.real-a.imag*b.imag;
    imag=a.real*b.imag+a.imag*b.real;
    return Complex(real,imag);
}

void FFT(Complex *c,int state)
{
    for(int i=0; i<lim; i++)
        if(i<resort[i])
            swap(c[i],c[resort[i]]);
    for(int i=1; i<lim; i<<=1)
    {
        Complex W1n(cos(pi/i),state*sin(pi/i));
        for(int Size=i<<1,j=0; j<lim; j+=Size)
        {
            Complex W(1.00,0.00);
            for(int k=0; k<i; k++,W=W*W1n)
            {
                Complex x=c[j+k],y=W*c[j+i+k];
                c[j+k]=x+y;
                c[j+i+k]=x-y;
            }
        }
    }
    return;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    /* Code */
    cin>>n>>m;
    for(int i=0; i<=n; i++)
        cin>>a[i].real;
    for(int i=0; i<=m; i++)
        cin>>b[i].real;
    lim=1,dig=0;
    while(lim<=n+m)
        lim<<=1,dig++;
    for(int i=0; i<lim; i++)
        resort[i]=(resort[i>>1]>>1)|((i&1)<<(dig-1));
    FFT(a,1);
    FFT(b,1);
    for(int i=0; i<lim; i++)
        a[i]=a[i]*b[i];
    FFT(a,-1);
    for(int i=0; i<=n+m; i++)
        cout<<(int)(a[i].real/lim+0.5)<<" ";
    return 0;
}

完美切题 ∼ \sim

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值