专栏文章
题解:P1919 【模板】高精度乘法 | A*B Problem 升级版
P1919题解参与者 9已保存评论 13
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 12 条
- 当前快照
- 1 份
- 快照标识符
- @mip6rygt
- 此快照首次捕获于
- 2025/12/03 07:05 3 个月前
- 此快照最后确认于
- 2025/12/03 07:05 3 个月前
了解 FFT
由于本人是 xxs,数学学的不多,还请多多谅解。
前情提要
我们都知道,高中数学有一个叫做复数的东西。
虚数单位 其实就是用来表示 的根。
为什么一定要弄一个这个东西呢,因为我们知道平方具有非负性,但虚数不一样,,也就是 。
那么复数,其实就是 的形式,其中 。
然后这个东西的加减乘除并不复杂,它也满足结合律,所以可以直接使用乘法、加法结合律计算,比如 。
这个复数在编程中可以手写,也可以使用 STL 中的
complex 项。这里我放上自己手写的:
CPPstruct 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};}
};
那么我们其实可以将它一一映射到平面直角坐标系上,其中 在坐标系上可以唯一表示为点 。
我们再在坐标系上画出一个单位圆,它的方程为 ,意思是所有 满足与 距离为 的点。
那么了解 FFT 之前,我们需要知道 次单位根,我们定义 为方程 的根。
那么所有的根分别为 。
这几个数的复数表示分别为 ,也就是把单位圆平均分成 份。
大概的示意图就是这样:

我知道你们有很多疑问,让我们一个个解决。
为了方便,令 。
首先我们需要知道单位圆的参数方程:
这个很好证,把这个带入单位圆的方程即可,意思是从 轴开始,在单位圆上逆时针转 度。
所以这 个根都在单位圆上。
接下来,我们需要知道,当我们已经定义 时,为什么 。
我们可以使用归纳法证明:
-
当 时,显然成立。
-
假设 时成立,那么 时。
所以,。
于是 为方程 的 个复数根。
于是,从几何意义上看,我们知道了 。
我们还要了解多项式。
什么是多项式,我们定义一个 次多项式 。
那么求 的值,就是把 代入这个多项式。
我们也可以使用 这 个点来唯一确定这个多项式,毕竟这 个方程可以唯一确定这 个系数。
正片开始
FFT,其实就是把系数表示法转换为点值表示法,再转换为系数表示法,进而加速多项式运算的过程。
FFT 正变换
我们可以做如下变换:
然后我们令 不难发现它们均为偶函数。
所以 。
。
也就是说,我们只要递归求解 与 的值,就可以知道 与 的值。将它转化为点值表示法。
那么时间复杂度是多少,我们发现,一共会递归 层,每层需要花 的代价合并,所以总时间复杂度 。
然后我们发现由于总是出现 所以我们不妨把 直接补成二的若干次方的形式。
然后我们如果要求 的点值表示法,我们可以求出 和 表示的值,那么 在 对应的值为 。
FFT 逆变换
假设一个多项式 ,现在我们已经知道了它的点值表示法 。
现在我们需要根据点值表示法反推系数表示法。
我们干一个很匪夷所思的事情。
我们把它的点值表示法看成系数表示法。
也就是定义 。
然后我们以 做一次 FFT,只不过这一次单位根要换成 。
那么接下来我们推一下 。
又 。
所以原式等于 。
所以做一次 FFT 之后,把得到的系数除以 即可,时间复杂度 。
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
}
}
空间复杂度 。
不过这个东西可以 AC 另一道模板题。
我们继续提高难度
我们发现,递归 FFT 的缺点就是常数太大。
递归的常数过于大了。
那有没有什么办法,使得我们不需要递归呢?
有的兄弟有的。这时候就要讲一下蝶形算法优化。
我们假设函数 表示编号为 的数最后经过 FFT 之后会变到哪里。
-
如果 。那么偶数项都会排到前面,所以 。
-
如果 ,那么它会排到前半列,所以 。
设 。
我们发现,如果把它看成二进制有 位的数,那么如果这个数最低位有一,那么最后的顺序的最高位就有一。
这不是把这个二进制数直接翻转的结果吗!
比如我们举个栗子。
变换一次:。
变换两次:。
变换最终结果:。
只看次数:。
最终顺序:。
如何求解一个数翻转二进制位得到的数?
设它为 ,那么 。
什么意思? 是指当 为奇数时返回 ,否则返回 。所以 就代表翻转之后 的最高位。
意思就是把 的最低位去掉,然后翻转。但我们想要的是最高位空出来,所以再除以 。
CPPfor(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]]);
}
那么为什么只有当 的时候才会交换呢?
因为 翻转之后是 ,你要是 的时候再交换一遍,那不换回去了吗?
于是我们就有了以下非递归版 FFT。
CPPvoid 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;
}
}
}
两个大整数相乘可以看成两个系数为 的多项式相乘。
于是此题思路显而易见。
复杂度分析
我们发现,第一层循环花费 的时间。
第二三层循环相当于把整个序列遍历了一遍。
所以总时间复杂度 。
空间复杂度由于我们一直在用一个数组求解,所以总空间复杂度 。
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;
}
几道例题
都是模板题:
后记
感谢同学 _FastFT2013的文章赞助:link。麻烦关注他谢谢。
相关推荐
评论
共 13 条评论,欢迎与作者交流。
正在加载评论...