题解:P1919 【模板】高精度乘法 | A*B Problem 升级版
Sunrise_beforeglow
·
·
题解
了解 FFT
由于本人是 xxs,数学学的不多,还请多多谅解。
前情提要
我们都知道,高中数学有一个叫做复数的东西。
虚数单位 i 其实就是用来表示 x^2=-1 的根。
为什么一定要弄一个这个东西呢,因为我们知道平方具有非负性,但虚数不一样,i^2=-1,也就是 i=\sqrt{-1}。
那么复数,其实就是 a+bi 的形式,其中 a,b\in \mathbb{R}。
然后这个东西的加减乘除并不复杂,它也满足结合律,所以可以直接使用乘法、加法结合律计算,比如 (a+bi)(c+di)=ac+i^2bd+i(ad+bc)=ac-bd+i(ad+bc)。
这个复数在编程中可以手写,也可以使用 STL 中的 complex 项。
这里我放上自己手写的:
struct node
{
double x,y;
node operator +(const node &g)const{return {g.x+x,g.y+y};}
node operator -(const node &g)const{return {x-g.x,y-g.y};}
node operator *(const node &g)const{return {g.x*x-g.y*y,g.x*y+g.y*x};}
};
那么我们其实可以将它一一映射到平面直角坐标系上,其中 a+bi 在坐标系上可以唯一表示为点 (a,b)。
我们再在坐标系上画出一个单位圆,它的方程为 (x-0)^2+(y-0)^2=1,意思是所有 (x,y) 满足与 (0,0) 距离为 1 的点。
那么了解 FFT 之前,我们需要知道 n 次单位根,我们定义 \omega_n^1 为方程 x^n=1 的根。
那么所有的根分别为 \omega_n^0,\omega_n^1\cdots \omega_{n}^{n-1}。
这几个数的复数表示分别为 1,\cos(\dfrac{2\pi}{n})+i\sin(\dfrac{2\pi}{n})\cdots \cos((n-1)\dfrac{2\pi}{n})+i\sin((n-1)\dfrac{2\pi}{n}),也就是把单位圆平均分成 n 份。
大概的示意图就是这样:
我知道你们有很多疑问,让我们一个个解决。
为了方便,令 \alpha=\dfrac{2\pi}{n}。
首先我们需要知道单位圆的参数方程:
\begin{cases}
x=\cos\alpha\\
y=\sin\alpha
\end{cases}
这个很好证,把这个带入单位圆的方程即可,意思是从 x 轴开始,在单位圆上逆时针转 \alpha 度。
所以这 n 个根都在单位圆上。
接下来,我们需要知道,当我们已经定义 w_n^1=\cos\alpha +i\sin \alpha 时,为什么 w_n^x=\cos x\alpha+i\sin x\alpha。
我们可以使用归纳法证明:
-
当 x=1 时,显然成立。
-
假设 x=k-1 时成立,那么 x=k 时。
w_n^k=w_n^{k-1}\times w_n^1=(\cos(k-1)\alpha+i\sin(k-1)\alpha)(\cos \alpha +i\sin \alpha)
=\cos k\alpha+i\sin k\alpha
所以,(w_n^i)^n=w_n^{ni}=\cos2\pi i+i\sin2\pi i=1。
于是 w_n^0,w_n^1\cdots w_n^{n-1} 为方程 x^n=1 的 n 个复数根。
于是,从几何意义上看,我们知道了 \omega_{2n}^k=-\omega_{2n}^{k+n}。
我们还要了解多项式。
什么是多项式,我们定义一个 n 次多项式 f(x)=a_{n-1}x^{n-1}\cdots a_0x_0=\sum\limits_{i=0}^{n-1}a_ix^i。
那么求 f(x) 的值,就是把 x 代入这个多项式。
我们也可以使用 (x_0,y_0)\cdots (x_{n-1},y_{n-1}) 这 n 个点来唯一确定这个多项式,毕竟这 n 个方程可以唯一确定这 n 个系数。
正片开始
FFT,其实就是把系数表示法转换为点值表示法,再转换为系数表示法,进而加速多项式运算的过程。
FFT 正变换
我们可以做如下变换:
\sum_{i=0}^{n-1}a_ix^i=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}x^{2i}+x\sum_{i=0}^{\frac{n}{2}-1}a_{2i}x^{2i}
然后我们令 F(x)=\sum\limits_{i=0}^{\frac{n}{2}-1}a_{2i}x^{2i},G(x)=\sum\limits_{i=0}^{\frac{n}{2}-1}a_{2i}x^{2i} 不难发现它们均为偶函数。
所以 f(\omega_n^k)=F(\omega_n^k)+\omega_n^kG(\omega_n^k)。
也就是说,我们只要递归求解 $F(\omega_n^k)$ 与 $G(\omega_n^k)$ 的值,就可以知道 $f(\omega_n^k)$ 与 $f(\omega_n^{k+\frac{n}{2}})$ 的值。将它转化为点值表示法。
那么时间复杂度是多少,我们发现,一共会递归 $\log_2n$ 层,每层需要花 $O(n)$ 的代价合并,所以总时间复杂度 $O(n\log_2n)$。
然后我们发现由于总是出现 $\dfrac{n}{2}$ 所以我们不妨把 $n$ 直接补成二的若干次方的形式。
然后我们如果要求 $f(x)g(x)$ 的点值表示法,我们可以求出 $f(\omega_n^i)$ 和 $g(\omega_{n}^i)$ 表示的值,那么 $f(x)g(x)$ 在 $\omega_{n}^i$ 对应的值为 $f(\omega_n^i)g(\omega_{n}^i)$。
### FFT 逆变换
假设一个多项式 $h(x)=\sum\limits_{i=0}^{n-1}a_ix^i$,现在我们已经知道了它的点值表示法 $(\omega_{n}^0,h(\omega_{n}^0))\cdots (\omega_{n}^{n-1},h(\omega_{n}^{n-1}))$。
现在我们需要根据点值表示法反推系数表示法。
我们干一个很匪夷所思的事情。
我们把它的点值表示法看成系数表示法。
也就是定义 $H(x)=\sum\limits_{i=0}^{n-1}x^i(\sum\limits_{j=0}^{n-1}a_j\omega_n^{ij})$。
然后我们以 $H(x)$ 做一次 FFT,只不过这一次单位根要换成 $\omega_n^{-k}$。
那么接下来我们推一下 $H(\omega_n^{-k})$。
$$H(\omega_n^{-k})=\sum\limits_{i=0}^{n-1}\omega_n^{-ik}(\sum\limits_{j=0}^{n-1}a_j\omega_n^{ij})$$
$$=\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j\omega_n^{ij}\omega_n^{-ik})$$
$$=\sum\limits_{i=0}^{n-1}(a_k+\sum\limits_{j=0,j\neq k}^{n-1}a_j\omega_n^{i(j-k)})$$
$$=na_k+\sum_{j=0,j\neq k}^{n-1}\omega_n^{j-k}a_j\sum_{i=0}^{n-1}\omega_{n}^i$$
又 $\sum\limits_{i=0}^{n-1}\omega_n^i=\dfrac{\omega_n^n-\omega_n^0}{\omega_n^1-1}=0$。
所以原式等于 $na_k$。
所以做一次 FFT 之后,把得到的系数除以 $n$ 即可,时间复杂度 $O(n\log_2n)$。
```cpp
//这里node是我自己写的一个复数项
void FFT(node y[],int x,int type)//系数,长度,正变换或逆变换
{
if(x==1)return;//当只剩常数项,直接返回
node u[x/2+1],v[x/2+1];//为了节省空间,保证每一层空间都是O(n)复杂度
for(int i=0;i<x;i+=2)u[i/2]=y[i],v[i/2]=y[i+1];//装填系数
FFT(u,x/2,type);
FFT(v,x/2,type);
node w={cos(2*PI*type/x),sin(2*PI*type/x)},W={1,0};//预处理单位根
//W其实就是单位根的i次方
for(int i=0;i<x/2;i++)
{
y[i]=u[i]+W*v[i];
y[i+x/2]=u[i]-W*v[i];
W=W*w;//每次让W乘上一个单位根,使次数+1
}
}
```
空间复杂度 $O(n\log_2n)$。
不过这个东西可以 AC 另一道模板题。
## 我们继续提高难度
我们发现,递归 FFT 的缺点就是常数太大。
递归的常数过于大了。
那有没有什么办法,使得我们不需要递归呢?
有的兄弟有的。这时候就要讲一下蝶形算法优化。
我们假设函数 $f(n,a)$ 表示编号为 $a$ 的数最后经过 FFT 之后会变到哪里。
1. 如果 $a\equiv 1\pmod 2$。那么偶数项都会排到前面,所以 $f(n,a)=2^{n-1}+f(\dfrac{n}{2},\lfloor\dfrac{a}{2}\rfloor)$。
2. 如果 $2\mid a$,那么它会排到前半列,所以 $f(n,a)=f(\dfrac{n}{2},\dfrac{a}{2})$。
设 $n=2^k$。
我们发现,如果把它看成二进制有 $k$ 位的数,那么如果这个数最低位有一,那么最后的顺序的最高位就有一。
这不是**把这个二进制数直接翻转**的结果吗!
比如我们举个栗子。
$$\omega_8^0,\omega_8^1,\omega_8^2,\omega_8^3,\omega_8^4,\omega_8^5,\omega_8^6,\omega_8^7$$
变换一次:$\omega_8^0,\omega_8^2,\omega_8^4,\omega_8^6|\omega_8^1,\omega_8^3,\omega_8^5,\omega_8^7$。
变换两次:$\omega_8^0,\omega_8^4|\omega_8^2,\omega_8^6|\omega_8^1,\omega_8^5|\omega_8^3,\omega_8^7$。
变换最终结果:$\omega_8^0|\omega_8^4|\omega_8^2|\omega_8^6|\omega_8^1|\omega_8^5|\omega_8^3|\omega_8^7$。
只看次数:$(000)_2,(100)_2,(010)_2,(110)_2,(001)_2,(101)_2,(011)_2,(111)_2$。
最终顺序:$(000)_2,(001)_2,(010)_2,(011)_2,(100)_2,(101)_2,(110)_2,(111)_2$。
如何求解一个数翻转二进制位得到的数?
设它为 $res_i$,那么 $res_i=res_{i\div 2}\div 2+[2\nmid i]\times 2^{n-1}$。
什么意思?$[2\nmid i]$ 是指当 $i$ 为奇数时返回 $1$,否则返回 $0$。所以 $[2\nmid i]\times 2^{n-1}$ 就代表翻转之后 $i$ 的最高位。
$res_{i\div 2}$ 意思就是把 $i$ 的最低位去掉,然后翻转。但我们想要的是最高位空出来,所以再除以 $2$。
```cpp
for(int i=0;i<x;i++)
{
res[i]=res[i/2]/2;
if(i&1)res[i]|=(x/2);
if(i<res[i])swap(y[i],y[res[i]]);
}
```
那么为什么只有当 $i<res_i$ 的时候才会交换呢?
因为 $res_i$ 翻转之后是 $i$,你要是 $i>res_i$ 的时候再交换一遍,那不换回去了吗?
于是我们就有了以下非递归版 FFT。
```cpp
void FFT(node y[],int x,int type)
{
for(int i=0;i<x;i++)
{
res[i]=res[i/2]/2;
if(i&1)res[i]|=(x/2);
if(i<res[i])swap(y[i],y[res[i]]);
}
for(int g=2;g<=x;g*=2)//枚举分割的区间长度
{
node w={cos(PI*2/g),type*sin(PI*2/g)};
//这里利用三角函数的cos(-x)=cos(x),sin(-x)=-sin(x)
for(int i=0;i<x;i+=g)//每次向前跳一个区间
{
node o={1,0};
for(int j=i;j<i+g/2;j++,o=o*w)//用上一层的点值答案推这一层的点值
{
node l=y[j],r=y[j+g/2];
//由于y[j],y[j+g/2]推的刚好是这一层的y[j],y[j+g/2],所以可以优化空间
y[j]=l+o*r;
y[j+g/2]=l-o*r;
}
}
}
if(type==-1)//处理逆变换
{
for(int i=0;i<x;i++)
{
y[i].x/=x;
y[i].y/=x;
}
}
}
```
两个大整数相乘可以看成两个系数为 $10^k$ 的多项式相乘。
于是此题思路显而易见。
# 复杂度分析
我们发现,第一层循环花费 $\log_2n$ 的时间。
第二三层循环相当于把整个序列遍历了一遍。
所以总时间复杂度 $O(n\log_2n)$。
空间复杂度由于我们一直在用一个数组求解,所以总空间复杂度 $O(n)$。
# code
```cpp
#include <bits/stdc++.h>
using namespace std;
int n,m,res[3000005],len;
string u,v;
struct node
{
double x,y;
node operator +(const node &g)const{return {g.x+x,g.y+y};}
node operator -(const node &g)const{return {x-g.x,y-g.y};}
node operator *(const node &g)const{return {g.x*x-g.y*y,g.x*y+g.y*x};}
};
node a[3000005],b[3000005];
double PI=3.1415926535;
void FFT(node y[],int x,int type)
{
for(int i=0;i<x;i++)
{
res[i]=res[i/2]/2;
if(i&1)res[i]|=(x/2);
if(i<res[i])swap(y[i],y[res[i]]);
}
for(int g=2;g<=x;g*=2)
{
node w={cos(PI*2/g),type*sin(PI*2/g)};
for(int i=0;i<x;i+=g)
{
node o={1,0};
for(int j=i;j<i+g/2;j++,o=o*w)
{
node l=y[j],r=y[j+g/2];
y[j]=l+o*r;
y[j+g/2]=l-o*r;
}
}
}
if(type==-1)
{
for(int i=0;i<x;i++)
{
y[i].x/=x;
y[i].y/=x;
}
}
}
int main()
{
cin>>u>>v;
n=u.size(),m=v.size();
for(int i=n-1;i>=0;i--)a[n-1-i].x=u[i]-'0';
for(int i=m-1;i>=0;i--)b[m-1-i].x=v[i]-'0';
len=1;
while(len<n+m)len*=2;
FFT(a,len,1);
FFT(b,len,1);
for(int i=0;i<len;i++)a[i]=a[i]*b[i];
FFT(a,len,-1);
for(int i=0;i<len;i++)
{
int u=a[i].x+0.5;
a[i+1].x+=u/10;
}
while((int)(a[len].x+0.5)<=0)len--;
if(len==0)
{
cout<<0;
return 0;
}
for(int i=len;i>=0;i--)
{
int u=a[i].x+0.5;
cout<<u%10;
}
return 0;
}
```
# 几道例题
都是模板题:
[【模板】高精度乘法 | A*B Problem 升级版](https://www.luogu.com.cn/problem/P1919)。
[【模板】多项式乘法(FFT)](https://www.luogu.com.cn/problem/P3803)。
# 后记
感谢同学 @[_FastFT2013](https://www.luogu.com.cn/user/701478) 的文章赞助:[link](https://www.luogu.me/article/i3tw5t0r)。麻烦关注他谢谢。