用途
快速傅里叶变换(Fast Fourier Transformation,简称FFT) 一般用来加速多项式乘法。求两个 n n n次多项式相乘,朴素算法需要 O ( n 2 ) O(n^2) O(n2),但FFT只需要 O ( n log n ) O(n\log n) O(nlogn)就能解决。
多项式
系数表示法
一个
n
n
n次
n
+
1
n+1
n+1项多项式可表示为
f
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
⋯
+
a
n
x
n
f(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^n
f(x)=a0+a1x+a2x2+⋯+anxn
这就是多项式的系数表示法。
点值表示法
将 n + 1 n+1 n+1个不同的 x x x值代入 f ( x ) f(x) f(x),可以得到 n + 1 n+1 n+1个对应的 f ( x ) f(x) f(x)值。这 n n n对 ( x k , f ( x k ) ) (x_k,f(x_k)) (xk,f(xk))可以唯一确定一个 n n n次多项式。
为什么呢?可以将 n + 1 n+1 n+1个式子联立起来得到一个 n + 1 n+1 n+1元一次方程组,这个方程组有唯一解。
用 n + 1 n+1 n+1个点表示一个 n n n次函数,称为点值表示法。
系数转为点值
对于一个 n n n次多项式,在已知系数的情况下,将其转为点值表示法,称为DFT(离散傅里叶变换)。
使用秦九韶算法,即可 O ( n ) O(n) O(n)转换一个 x x x值,那么求 n n n个点的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
点值转为系数
对于一个由点值表示法表示的 n n n次多项式,将其转化为系数表示法,称为IDFT(离散傅里叶逆变换)。
用高斯消元需要 O ( n 3 ) O(n^3) O(n3),用拉格朗日插值法可以达到 O ( n 2 ) O(n^2) O(n2)。
多项式乘法
对于用系数表示的 n n n次多项式 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x),求两式相乘后的多项式,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
用点值表示的呢?
对于 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x),我们各取 2 n + 1 2n+1 2n+1个点。
( x 0 , f ( x 0 ) ) , ( x 1 , f ( x 1 ) ) , … , ( x 2 n , f ( x 2 n ) ) ( x 0 , g ( x 0 ) ) , ( x 1 , g ( x 1 ) ) , … , ( x 2 n , g ( x 2 n ) ) (x_0,f(x_0)),(x_1,f(x_1)),\dots ,(x_{2n},f(x_{2n}))\\ \qquad \\(x_0,g(x_0)),(x_1,g(x_1)),\dots ,(x_{2n},g(x_{2n})) (x0,f(x0)),(x1,f(x1)),…,(x2n,f(x2n))(x0,g(x0)),(x1,g(x1)),…,(x2n,g(x2n))
上下相乘得到
( x 0 , f ( x 0 ) ⋅ g ( x 0 ) ) , ( x 1 , f ( x 1 ) ⋅ g ( x 1 ) ) , … , ( x 2 n , f ( x 2 n ) ⋅ g ( x 2 n ) ) (x_0,f(x_0)\cdot g(x_0)),(x_1,f(x_1)\cdot g(x_1)),\dots ,(x_{2n},f(x_{2n})\cdot g(x_{2n})) (x0,f(x0)⋅g(x0)),(x1,f(x1)⋅g(x1)),…,(x2n,f(x2n)⋅g(x2n))
显然这 2 n + 1 2n+1 2n+1个点唯一地确定了一个 2 n 2n 2n次函数,这 2 n + 1 2n+1 2n+1个点就是 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x)的乘积的点值表示法的形式,所以用点值表示法只需要 O ( n ) O(n) O(n)。
那如果将系数表示法转换为点值表示法相乘,再转回系数表示法,会不会更快呢?
用朴素算法是不会的,因为DFT 和IDFT 的时间复杂度都为 O ( n 2 ) O(n^2) O(n2),所以最终的时间复杂度为 O ( n 2 ) O(n^2) O(n2),和直接相乘一样。
有没有其他办法呢?那就要用到FFT 了。
快速傅里叶变换
复数
复数可以写成 a + b i a+bi a+bi的形式。在复平面上为点 ( a , b ) (a,b) (a,b)。
x n = 1 x^n=1 xn=1有 n n n个根,令 ω n \omega_n ωn表示 x n = 1 x^n=1 xn=1的单位复数根,则 ω n = e 2 π i / n \omega_n=e^{2\pi i/n} ωn=e2πi/n,则方程其他的根就是 ω n \omega_n ωn的若干次幂。
由欧拉公式可得
e
i
θ
=
cos
θ
+
i
sin
θ
e^{i\theta}=\cos \theta+i\sin \theta
eiθ=cosθ+isinθ
所以我们可以通过三角函数来求 e 2 π i / n e^{2\pi i/n} e2πi/n的实部和虚部。也由此可得, x n = 1 x^n=1 xn=1的根都均匀分布在复平面上以原点为圆心的单位圆的圆周上。
单位复数根有以下性质:
1. 消去引理
对于任意整数 n ≥ 0 , k ≥ 0 , d > 0 n\geq 0,k\geq 0,d>0 n≥0,k≥0,d>0,都有 ω d n d k = ω n k \omega_{dn}^{dk}=\omega_n^k ωdndk=ωnk
证明: ω d n d k = ( e 2 π i / d n ) d k = ( e 2 π i / n ) k = ω n k \omega_{dn}^{dk}=(e^{2\pi i/dn})^{dk}=(e^{2\pi i/n})^k=\omega_n^k ωdndk=(e2πi/dn)dk=(e2πi/n)k=ωnk
2. 折半引理
对于偶数 n ≥ 0 n\geq 0 n≥0和整数 k ≥ 0 k\geq 0 k≥0, ( ω n k + n / 2 ) 2 = ω n / 2 k (\omega_{n}^{k+n/2})^2=\omega_{n/2}^{k} (ωnk+n/2)2=ωn/2k
证明: ( ω n k + n / 2 ) 2 = ω n 2 k + n = ω n 2 k = ω n / 2 k (\omega_{n}^{k+n/2})^2=\omega_{n}^{2k+n}=\omega_{n}^{2k}=\omega_{n/2}^{k} (ωnk+n/2)2=ωn2k+n=ωn2k=ωn/2k
3. 求和引理
对于任意整数 n ≥ 1 n\geq 1 n≥1和不能被 n n n整除的整数 k k k,有 ∑ j = 0 n − 1 ( ω n k ) j = 0 \sum\limits_{j=0}^{n-1}(\omega_n^k)^j=0 j=0∑n−1(ωnk)j=0
证明: ∑ j = 0 n − 1 ( ω n k ) j = 1 − ( ω n k ) n 1 − ω n k = 0 1 − ω n k = 0 \sum\limits_{j=0}^{n-1}(\omega_n^k)^j=\dfrac{1-(\omega_n^k)^n}{1-\omega_n^k}=\dfrac{0}{1-\omega_n^k}=0 j=0∑n−1(ωnk)j=1−ωnk1−(ωnk)n=1−ωnk0=0
FFT
我们考虑将
ω
n
0
,
ω
n
1
,
ω
n
2
,
…
,
ω
n
n
−
1
\omega_n^0,\omega_n^1,\omega_n^2,\dots,\omega_n^{n-1}
ωn0,ωn1,ωn2,…,ωnn−1代入
n
−
1
n-1
n−1次多项式求值。
(下文将
n
n
n视作2的幂,大于等于
n
n
n项可看作系数为0)
有多项式
A
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
⋯
+
a
n
−
1
x
n
−
1
A(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}
A(x)=a0+a1x+a2x2+⋯+an−1xn−1
将
A
(
x
)
A(x)
A(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
0
+
a
2
x
2
+
⋯
+
a
n
−
2
x
n
−
2
)
+
x
(
a
1
+
a
3
x
2
+
⋯
+
a
n
−
1
x
n
−
2
)
A(x)=(a_0+a_2x^2+\cdots+a_{n-2}x^{n-2})+(a_1x+a_3x^3+\cdots+a_{n-1}x^{n-1}) \\ \qquad \\ =(a_0+a_2x^2+\cdots+a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots+a_{n-1}x^{n-2})
A(x)=(a0+a2x2+⋯+an−2xn−2)+(a1x+a3x3+⋯+an−1xn−1)=(a0+a2x2+⋯+an−2xn−2)+x(a1+a3x2+⋯+an−1xn−2)
令
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^{n/2-1}\\ A_2(x)=a_1+a_3x+\cdots+a_{n-1}x^{n/2-1} A1(x)=a0+a2x+⋯+an−2xn/2−1A2(x)=a1+a3x+⋯+an−1xn/2−1
可得
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)
设
0
≤
k
<
n
/
2
0\leq k<n/2
0≤k<n/2,将
x
=
ω
n
k
x=\omega_n^k
x=ωnk代入
A
(
x
)
A(x)
A(x),得
A
(
ω
n
k
)
=
A
1
(
(
ω
n
k
)
2
)
+
ω
n
k
A
2
(
(
ω
n
k
)
2
)
A
1
(
ω
n
2
k
)
+
ω
n
k
A
2
(
ω
n
2
k
)
=
A
1
(
ω
n
/
2
k
)
+
ω
n
k
A
2
(
ω
n
/
2
k
)
A(\omega_n^k)=A_1((\omega_n^k)^2)+\omega_n^kA_2((\omega_n^k)^2)\\ \qquad \\A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})=A_1(\omega_{n/2}^k)+\omega_n^kA_2(\omega_{n/2}^k)
A(ωnk)=A1((ωnk)2)+ωnkA2((ωnk)2)A1(ωn2k)+ωnkA2(ωn2k)=A1(ωn/2k)+ωnkA2(ωn/2k)
将
x
=
ω
n
k
+
n
/
2
x=\omega_n^{k+n/2}
x=ωnk+n/2代入
A
(
x
)
A(x)
A(x),得
A
(
ω
n
k
+
n
/
2
)
=
A
1
(
(
ω
n
k
+
n
/
2
)
2
)
+
ω
n
k
+
n
/
2
A
2
(
(
ω
n
k
+
n
/
2
)
2
)
A
1
(
ω
n
2
k
)
+
ω
n
k
⋅
ω
n
n
/
2
A
2
(
ω
n
2
k
)
=
A
1
(
ω
n
/
2
k
)
−
ω
n
k
A
2
(
ω
n
/
2
k
)
A(\omega_n^{k+n/2})=A_1((\omega_n^{k+n/2})^2)+\omega_n^{k+n/2}A_2((\omega_n^{k+n/2})^2)\\ \qquad \\A_1(\omega_n^{2k})+\omega_n^k\cdot \omega_n^{n/2} A_2(\omega_n^{2k})=A_1(\omega_{n/2}^k)-\omega_n^kA_2(\omega_{n/2}^k)
A(ωnk+n/2)=A1((ωnk+n/2)2)+ωnk+n/2A2((ωnk+n/2)2)A1(ωn2k)+ωnk⋅ωnn/2A2(ωn2k)=A1(ωn/2k)−ωnkA2(ωn/2k)
我们可以发现,
A
(
ω
n
k
)
A(\omega_n^k)
A(ωnk)和
A
(
ω
n
k
+
n
/
2
)
A(\omega_n^{k+n/2})
A(ωnk+n/2)用
A
1
(
x
)
A_1(x)
A1(x)和
A
2
(
x
)
A_2(x)
A2(x)表示之后只有符号不同。
A
(
ω
n
k
)
=
A
1
(
ω
n
/
2
k
)
+
ω
n
k
A
2
(
ω
n
/
2
k
)
A
(
ω
n
k
+
n
/
2
)
=
A
1
(
ω
n
/
2
k
)
−
ω
n
k
A
2
(
ω
n
/
2
k
)
A(\omega_n^k)=A_1(\omega_{n/2}^k)+\omega_n^kA_2(\omega_{n/2}^k) \\ \qquad \\ A(\omega_n^{k+n/2})=A_1(\omega_{n/2}^k)-\omega_n^kA_2(\omega_{n/2}^k)
A(ωnk)=A1(ωn/2k)+ωnkA2(ωn/2k)A(ωnk+n/2)=A1(ωn/2k)−ωnkA2(ωn/2k)
于是,当我们求出 A 1 ( ω n / 2 k ) A_1(\omega_{n/2}^k) A1(ωn/2k)和 A 2 ( ω n / 2 k ) A_2(\omega_{n/2}^k) A2(ωn/2k)之后,就能求出 A ( ω n k ) A(\omega_n^k) A(ωnk)和 A ( ω n k + n / 2 ) A(\omega_n^{k+n/2}) A(ωnk+n/2)的值。
那么问题就转化为求 A 1 ( x ) A_1(x) A1(x)和 A 2 ( x ) A_2(x) A2(x)在 ω n / 2 0 , ω n / 2 1 , … , ω n / 2 n / 2 − 1 \omega_{n/2}^0,\omega_{n/2}^1,\dots,\omega_{n/2}^{n/2-1} ωn/20,ωn/21,…,ωn/2n/2−1处的值,可以用分治来解决问题。
以下是递归写法,用于理解,后面会介绍更优的迭代写法。
code
const double pi=acos(-1.0);
int a1[N],a2[N],y[N];
int fft(int *a,int l){
if(l==1) return a;
cp wn=(cp){cos(2*pi/n),sin(2*pi/n)};
cp w=(cp){1,0};
for(int i=0;i<l/2;i++){
a1[i]=a[i*2];
a2[i]=a[i*2+1];
}
int *y0=fft(a1,n/2);
int *y1=fft(a2,n/2);
for(int i=0;i<n/2;i++){
y[i]=y0[i]+y1[i]*w;
y[i+n/2]=y0[i]-y1[i]*w;
w=w*wn;
}
return y;
}
其结构与 c d q cdq cdq分治相似,相当于有 log n \log n logn层,每层都是 n n n个位置,所以时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
IFFT
我们已经可以在 O ( n log n ) O(n\log n) O(nlogn)的时间复杂度下将系数表示法转化为点值表示法。接下来,我们要考虑如何将两个多项式的乘积的点值表示法再变回系数表示法。
对于前面的FFT,其过程可以用以下矩阵表示:
[
1
1
1
⋯
1
1
ω
n
1
ω
n
2
⋯
ω
n
n
−
1
1
ω
n
2
ω
n
4
⋯
ω
n
2
(
n
−
1
)
⋮
⋮
⋮
⋮
⋮
1
ω
n
n
−
1
ω
n
2
(
n
−
1
)
⋯
ω
n
(
n
−
1
)
(
n
−
1
)
]
[
a
0
a
1
a
2
⋮
a
n
−
1
]
=
[
y
0
y
1
y
2
⋮
y
n
−
1
]
\left[ \begin{matrix} \ 1 & 1 & 1 & \cdots & 1 \ \\ \ 1 & \omega_n^1 & \omega_n^2 & \cdots & \omega_n^{n-1} \ \\ \ 1 & \omega_n^2 & \omega_n^4 & \cdots & \omega_n^{2(n-1)} \ \\ \ \vdots & \vdots & \vdots & \vdots & \vdots \ \\ \ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \cdots & \omega_n^{(n-1)(n-1)} \ \end{matrix} \right] \left[ \begin{matrix} \ a_0 \ \\ \ a_1 \ \\ \ a_2 \ \\ \ \vdots \ \\ \ a_{n-1} \ \end{matrix} \right] =\left[ \begin{matrix} \ y_0 \ \\ \ y_1 \ \\ \ y_2 \ \\ \ \vdots \ \\ \ y_{n-1} \ \end{matrix} \right]
1 1 1 ⋮ 11ωn1ωn2⋮ωnn−11ωn2ωn4⋮ωn2(n−1)⋯⋯⋯⋮⋯1 ωnn−1 ωn2(n−1) ⋮ ωn(n−1)(n−1)
a0 a1 a2 ⋮ an−1
=
y0 y1 y2 ⋮ yn−1
左边的矩阵是范德蒙德矩阵, V i , j = ω n i j V_{i,j}=\omega_n^{ij} Vi,j=ωnij,可以构造出它的逆矩阵 V − 1 V^{-1} V−1,其中 ( V − 1 ) i , j = ω n − i j n (V^{-1})_{i,j}=\dfrac{\omega_n^{-ij}}{n} (V−1)i,j=nωn−ij
[ 1 1 1 ⋯ 1 1 ω n − 1 / n ω n − 2 / n ⋯ ω n − ( n − 1 ) / n 1 ω n − 2 / n ω n − 4 / n ⋯ ω n − 2 ( n − 1 ) / n ⋮ ⋮ ⋮ ⋮ ⋮ 1 ω n − ( n − 1 ) / n ω n − 2 ( n − 1 ) / n ⋯ ω n − ( n − 1 ) ( n − 1 ) / n ] [ y 0 y 1 y 2 ⋮ y n − 1 ] = [ a 0 a 1 a 2 ⋮ a n − 1 ] \left[ \begin{matrix} \ 1 & 1 & 1 & \cdots & 1 \ \\ \ 1 & \omega_n^{-1}/n & \omega_n^{-2}/n & \cdots & \omega_n^{-(n-1)}/n \ \\ \ 1 & \omega_n^{-2}/n & \omega_n^{-4}/n & \cdots & \omega_n^{-2(n-1)}/n \ \\ \ \vdots & \vdots & \vdots & \vdots & \vdots \ \\ \ 1 & \omega_n^{-(n-1)}/n & \omega_n^{-2(n-1)}/n & \cdots & \omega_n^{-(n-1)(n-1)}/n \ \end{matrix} \right] \left[ \begin{matrix} \ y_0 \ \\ \ y_1 \ \\ \ y_2 \ \\ \ \vdots \ \\ \ y_{n-1} \ \end{matrix} \right] =\left[ \begin{matrix} \ a_0 \ \\ \ a_1 \ \\ \ a_2 \ \\ \ \vdots \ \\ \ a_{n-1} \ \end{matrix} \right] 1 1 1 ⋮ 11ωn−1/nωn−2/n⋮ωn−(n−1)/n1ωn−2/nωn−4/n⋮ωn−2(n−1)/n⋯⋯⋯⋮⋯1 ωn−(n−1)/n ωn−2(n−1)/n ⋮ ωn−(n−1)(n−1)/n y0 y1 y2 ⋮ yn−1 = a0 a1 a2 ⋮ an−1
相当于在做FFT时在原来的基础上将 ω n \omega_n ωn变成 ω n − 1 \omega_n^{-1} ωn−1,然后对结果的每一项除以 n n n即可,时间复杂度也为 O ( n log n ) O(n\log n) O(nlogn)。
蝴蝶变换
由上文可得知,可以将 A ( x ) A(x) A(x)拆成 A 0 ( x ) A_0(x) A0(x)和 A 1 ( x ) A_1(x) A1(x)来求值。
但是用上述的递归代码跑的速度较慢,所以我们考虑用更高效的算法。
观察分治的过程,看看有什么规律。
我们发现,最底层的系数排列顺序,正好是其二进制的镜面翻转。
如第 1 1 1个, 1 1 1的二进制是 001 001 001,翻转后是 100 100 100,即十进制的 4 4 4,所以从第 0 0 0个位置开始的第一个位置是 a 4 a_4 a4。
为什么呢?因为在每次分治的时候,我们都将低位为 0 0 0的放在左,为 1 1 1的放在右。但如果将每个数的下标镜面翻转,我们就发现这是在将 a a a序列按二进制位从高到低来分,最后自然就是从小到大。再镜面翻转回来,即可得到原来的系数。这个变换称为蝴蝶变换。
code
void ch(cp *a,int l){
for(int i=1,j=l/2,k;i<l-1;i++){
if(i<j) swap(a[i],a[j]);
k=l/2;
while(j>=k){
j-=k;k>>=1;
}
j+=k;
}
}
以下是FFT的迭代写法。
void fft(cp *a,int l){
for(int i=2;i<=l;i<<=1){
wn=(cp){cos(2*pi/i),sin(2*pi/i)};
for(int j=0;j<l;j+=i){
w=(cp){1,0};
for(int k=j;k<j+i/2;k++,w=w*wn){
cp t=a[k],u=w*a[k+i/2];
a[k]=t+u;
a[k+i/2]=t-u;
}
}
}
}
总结
解决多项式乘法的过程:
- 用FFT将系数表示法转化为点值表示法
- 用点值表示法来求多项式乘法
- 用IFFT将点值表示法再变回系数表示法
这样即可用 O ( n log n ) O(n\log n) O(nlogn)的时间复杂度求出多项式的乘积。
例题
多项式乘法(FFT)
模板题
令 l l l表示最后多项式的次数,则 n n n需满足 n ≥ l n\geq l n≥l且 n n n为 2 2 2的若干次幂。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5000005;
const double pi=acos(-1.0);
char s1[N],s2[N];
long long ans[N];
struct cp{
double a,b;
cp operator +(const cp ax)const{
return (cp){a+ax.a,b+ax.b};
}
cp operator -(const cp ax)const{
return (cp){a-ax.a,b-ax.b};
}
cp operator *(const cp ax)const{
return (cp){a*ax.a-b*ax.b,b*ax.a+a*ax.b};
}
}w,wn,a1[N],a2[N];
void ch(cp *a,int l){
for(int i=1,j=l/2,k;i<l-1;i++){
if(i<j) swap(a[i],a[j]);
k=l/2;
while(j>=k){
j-=k;k>>=1;
}
j+=k;
}
}
void fft(cp *a,int l,int fl){
for(int i=2;i<=l;i<<=1){
wn=(cp){cos(fl*2*pi/i),sin(fl*2*pi/i)};
for(int j=0;j<l;j+=i){
w=(cp){1,0};
for(int k=j;k<j+i/2;k++,w=w*wn){
cp t=a[k],u=w*a[k+i/2];
a[k]=t+u;
a[k+i/2]=t-u;
}
}
}
}
int main()
{
int n,l,l1,l2,x;
scanf("%d%d",&l1,&l2);++l1;++l2;
l=l1+l2;n=1;
while(n<l) n<<=1;
for(int i=0;i<l1;i++) scanf("%d",&x),a1[l1-i-1]=(cp){(double)(x),0};
for(int i=l1;i<n;i++) a1[i]=(cp){0,0};
for(int i=0;i<l2;i++) scanf("%d",&x),a2[l2-i-1]=(cp){(double)(x),0};
for(int i=l2;i<n;i++) a2[i]=(cp){0,0};
ch(a1,n);ch(a2,n);
fft(a1,n,1);
fft(a2,n,1);
for(int i=0;i<n;i++) a2[i]=a1[i]*a2[i];
ch(a2,n);
fft(a2,n,-1);
for(int i=0;i<l1+l2-1;i++){
ans[i]=(long long)(a2[i].a/n+0.5);
}
while(ans[l]==0&&l>0) --l;
for(int i=l;i>=0;i--) printf("%lld ",ans[i]);
return 0;
}
A*B Problem 升级版(FFT 快速傅里叶变换)
将 a a a和 b b b看作两个多项式 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x),求多项式的乘积,再代入 x = 10 x=10 x=10即可。
code
#include<bits/stdc++.h>
using namespace std;
const double pi=acos(-1.0);
char s1[5000005],s2[5000005];
long long ans[5000005];
struct cp{
double a,b;
cp operator +(const cp ax)const{
return (cp){a+ax.a,b+ax.b};
}
cp operator -(const cp ax)const{
return (cp){a-ax.a,b-ax.b};
}
cp operator *(const cp ax)const{
return (cp){a*ax.a-b*ax.b,b*ax.a+a*ax.b};
}
}w,wn,a1[5000005],a2[5000005];
void ch(cp *a,int l){
for(int i=1,j=l/2,k;i<l-1;i++){
if(i<j) swap(a[i],a[j]);
k=l/2;
while(j>=k){
j-=k;k>>=1;
}
j+=k;
}
}
void fft(cp *a,int l,int fl){
for(int i=2;i<=l;i<<=1){
wn=(cp){cos(fl*2*pi/i),sin(fl*2*pi/i)};
for(int j=0;j<l;j+=i){
w=(cp){1,0};
for(int k=j;k<j+i/2;k++,w=w*wn){
cp t=a[k],u=w*a[k+i/2];
a[k]=t+u;
a[k+i/2]=t-u;
}
}
}
}
int main()
{
int n,l,l1,l2;
scanf("%s%s",s1,s2);
l1=strlen(s1);
l2=strlen(s2);
l=l1+l2;n=1;
while(n<l) n<<=1;
for(int i=0;i<l1;i++) a1[l1-i-1]=(cp){(double)(s1[i]-'0'),0};
for(int i=l1;i<n;i++) a1[i]=(cp){0,0};
for(int i=0;i<l2;i++) a2[l2-i-1]=(cp){(double)(s2[i]-'0'),0};
for(int i=l2;i<n;i++) a2[i]=(cp){0,0};
ch(a1,n);ch(a2,n);
fft(a1,n,1);
fft(a2,n,1);
for(int i=0;i<n;i++) a2[i]=a1[i]*a2[i];
ch(a2,n);
fft(a2,n,-1);
for(int i=0;i<l1+l2-1;i++){
ans[i]+=(long long)(a2[i].a/n+0.5);
ans[i+1]+=ans[i]/10;
ans[i]%=10;
}
while(ans[l]==0&&l>0) --l;
for(int i=l;i>=0;i--) printf("%lld",ans[i]);
return 0;
}