快速傅里叶变换

来源:互联网 发布:怎么把数据透视表还原 编辑:程序博客网 时间:2024/05/18 15:25

快速傅里叶变换(FFT),常用于解答多项式乘法相关内容。
吐槽时间:
见了网上太多太多的资料都是物理的QAQ
不给OIers活路这样真的好么……
都是什么频率啊振幅啊什么乱七八糟的东西TAT
欺负我什么都不懂是吧QAQ
还是自己写一个比较好QAQ
背景故事:
在我们平时计算多项式乘法的时候,我们把第一个多项式的每一项都和第二个多项式的每一项相乘,复杂度为O(n ^ 2),此时我们所使用的表示法就是系数表示法
现在我们可以有一种比较强大的方式:
点值表示法
众所周知,假设f(x)的最高次数为n1,即次数界为n,那么我们只要知道了n个不相同的x及f(x)值,就能确定出f(x)的多项式。
有一种算法叫做秦九韶算法,它能得出的结论是:
对于一个n次多项式,至多做n次乘法和n次加法。
所以我们知道了n个点的x值之后,我们在O(n^2)的时间内就能计算出所有的y值,然而如果通过快速傅里叶变换的方法,可以在O(nlogn)的时间内求出所有的y值。
引入新定义:
求值:通过多项式的系数表示法求其点值表示法。
插值:通过多项式的点值表示法求其系数表示法。
显然上面两个定义是互逆的关系,FFT就是用来解决求值的过程的。
引入新定义:
n次单位复根:在复平面内,n次单位复根能把整个平面分成n块。它的严格定义是:满足ωn=1的复数ω值,它一共有n个,分别为ωkn(k=0...n1),其数值为e2πik/n
由复数幂的定义,可知:
(ω1n)k=ωkn
它有很多性质:
1.相消引理:

nN,xN,dN,ωdkdn=ωkn

这是显然的,你可以考虑这个复平面等分成6个平面取第2个根和把这个复平面等分成3个平面取第一个根没什么区别。
2.折半引理:
nevennN,(ωk+n/2n)2=(ωkn)2

证明:左边 = ω2k+nn=ω2knωnn=ω2kn=
3.几何级数:
对于在复数域上的x1,有下列等式成立:
k=0nxk=1+x+x2+x3+xn=xn+11x1

嗯其实就是等比数列求和。
4.求和引理:
对于任意:n >= 1 且 n不能整除非零整数k ,有:
i=0n1(ωkn)i=0

证明:
由几何级数的计算过程可知:
i=0n1(ωkn)i=(ωkn)n1ωkn1=(ωnn)k1ωkn1=0

证毕。


接下来我们分析一下如何得到最终的多项式吧。
1.求A的n个单位根的点值,求B的n个单位根的点值
2.点值相乘,得到C的点值。
3.计算C的多项式。


我们先考虑第一步。
我们希望计算多项式:

A(x)=j=0n1ajxj

在n个单位根处的值。
设多项式A以系数形式给出,系数向量a⃗ =(a0,a1,...,an1),定义其结果
y=A(ωkn)=j=0n1ajωkjn

则向量y⃗ =y0,y1,...,yn1就是系数向量a⃗ 离散傅里叶变换(DFT),记作:y = DFTn(a).


也就是说,点值的结果就是DFT,那么我们只需要快速计算DFT的值即可。
如果按照正常的算法,时间复杂度显然是O(n2)的,所以有快速傅里叶变换(FFT),它采取的是分治的思想。
我们考虑原来的多项式A(x),定义两个新的次数界为n / 2的多项式:

A[0](x)=a0+a2x+a4x2+...+an2xn/21

A[1](x)=a1+a3x+a5x2+...+an1xn/21

则有
A(x)=A[0](x2)+xA[1](x2)

我们如果只是这样的话是没办法减少计算量的,因为这只是相当于展开。
我们考虑一个显然的事实:
对于能够计算A(x)的那两个多项式,实际上它们也是可以用FFT求值的,它们的次数界减少了一半
我们仔细回忆下单位根的性质,发现:
A(k)=A[0](ωkn)2+ωknA[1](ωkn)2;

A(k+n/2)=A[0](ωk+n/2n)2+ωk+n/2nA[1](ωk+n/2n)2;

化简一下第二个式子,发现:
A(k+n/2)=A[0](ωkn)2ωknA[1](ωkn)2;

嗯……
A(x)=u+xv,A(x+n/2)=uxv.

这个玩意叫做蝴蝶变换
减少了一半的计算量。
我们来计算下复杂度:
T(n) = 2 * T(n / 2) + n ;
T(n / 2) = 2 * T(n / 4) + n / 2;

显然,复杂度为nlog2n
我们终于讲完了如何求出n个单位根的点值,现在来说第二步:


把点值相乘。
显而易见的一点是,我们如果有两个多项式A(x),B(x),它们相乘得到多项式C(x),则:

A(k)B(k)=C(k)k

那么我们把n个点值相乘的复杂度自然是O(n)的。
只需要记录c[i] = a[i] * b[i]即可。


接下来我们要做的就是插值了。[忘了定义的请自觉往回翻]
因为我不知道拉格朗日插值公式是什么鬼(划掉),所以我决定还是不讲了。
//别打我别打我……我讲…………
嗯我们考虑把DFT写成矩阵的形式:

y0y1y2y3...yn1=1  111...1 1 ω1nω2nω3nωn1n  1ω2nω4nω6nω2(n1)n  1ω3nω6nω9nω3(n1)n...............1ωn1nω2(n1)nω3(n1)nω(n1)2na0a1a2a3...an1

终于挤下来了。
我们发现这个X的矩阵叫做范德蒙德矩阵。
发现我们现在要求的就是a⃗ ,即DFT1n(y)
那么怎么求系数矩阵呢?
则:若有矩阵AB = C,已知C,A则有:B = A1C
证明:A1AB=B=A1C
证毕。
显然的一点是:
范德蒙德矩阵的逆是存在的,我们想求a⃗ 也不是很难,但是,矩阵乘法在这里的复杂度是O(n2)的。
对于中间那个矩阵,我们设其为Vn,则有:
V1n(j,k)ωkjn/n
证明:
……不想写了。
我们给定逆矩阵V1n,可以推导出DFT1n(y):
aj=1nk=0n1ykωkjn.

然而对比之前的式子……?
y=A(ωkn)=j=0n1ajωkjn

长得一模一样……
用a数组直接存储a数组 * b数组的点值,对当前的a数组做FFT,对其结果除以n就能得到答案。
但是我们现在的东西还停留在递归阶段,我们知道,可以对这个数组进行元素上的调整,这样我们可以先计算那些子节点,省去了递归的过程,这个过程叫做:
雷(二)德(进)算(制)法
大概意思就是把每一个数的二进制头尾翻转过来:
即100 - > 001这样子……
我们可以知道的一件事情就是:0 - > 7这个序列,在递归到叶子节点之后,是:0,4,2,6,1,5,3,7。我们发现把二进制转过来之后按照顺序排序之后,再反转回来就能得到原来的序列了。


嗯……发现自己还是介绍一下雷德算法比较好,它有一个O(n)求每个数的二进制的反转的方法,但是请记住,这个O(n)并不如平时我们写的O(nlogn)要快,但是极其优美。
我们考虑一个数x的二进制反转和x / 2的二进制反转,发现如果x是偶数的话,那么[x / 2]的实际二进制和x的实际二进制只差了在二进制中移一次位。用实际二进制数组表达,那么设二进制数组为A[],则A[x] = A[x >> 1] << 1;
在反转的二进制中,设其数组为Rev[]:

Rev[x]=Rev[x>>1]>>1;

发现,一个数如果是奇数,那么也仅仅是改变了右移之后的最左端的一位。因为:A[x + 1] = A[x] | 1,那么我们就能同样改变反转数组:
Rev[x] = Rev[x >> 1] >> 1 | ((i & 1)  << (LG  - 1))

左移的位数大家仔细考虑一下发现,我们只需要使用 < n 的那些数字,也就意味着,在0 - > 3区间内:
Rev[0] = 00,Rev[1] = 10,Rev[2] = 01,Rev[3] = 11。
发现如果我们左移Log次好像也是……
可以的……吗?
显然是不能的,因为我们希望的是:
在将其二进制反转之后,用O(n)的方法对其调整计算的顺序。
我们如果二进制每次左移log位,看样子是可行的,但是我们只用到了0 - > n的,而左移log位就会使得除0以外的二进制反转>=n,因为用快速排序会使得整个程序再次陷入O(nlog2n),所以我们每次显然不能左移log位。
我们发现:左移(log - 1)位之后,O(n)扫一遍整个序列就能排序。

void Rader(){    //m 是原来两个多项式中较长的那个的长度的2倍。    for(n = 1;n <= m;n <<= 1,LG ++);    for(int i = 1;i < n;i ++)        Rev[i] = (Rev[i >> 1] >> 1) | ((i & 1) << (LG - 1));}void FFT(Virt *F,int ty = 1){    Rep_0(i,n)if(i < Rev[i])swap(F[i],F[Rev[i]]);//每次FFT的过程中根据二进制的反转调整运算顺序。    ...}

这样的话我们合并相邻的两个,就像线段树一样,这样问题就成功解决了。
请用手写复数类并重载运算符。
请用手写复数类并重载运算符。
请用手写复数类并重载运算符。
题目:
BZOJ2179:给出两个n位10进制整数x和y,你需要计算x*y。
n <= 60000
//据说FFT不开二倍数组长度会跑得更快
//注意:以上内容不保证其正确性与真实性。
//注意:以上内容不保证其正确性与真实性。
//注意:以上内容不保证其正确性与真实性。
我们可以将两个数相乘写成这种形式:

ci=j=0iajbij

然后直接输出就可以了。

bzoj2179,FFT模版,据说我这样不加注释会运行的更快。#include <cstdio>#include <cstring>#include <algorithm>#include <cmath>#define Rep(i,n) for(int i = 1;i <= n;i ++)#define RepG(i,x) for(int i = head[x];~ i;i = edge[i].next)#define RD(i,x,n) for(int i = x;i <= n;i ++)#define Rep_0(i,n) for(int i = 0;i < n;i ++)#define Rep0n(i,n) for(int i = 0;i <= n;i ++)#define PI M_PIusing namespace std;struct Virt {    double r,i;    Virt (double r = 0.0,double i = 0.0)    {        this -> r = r;        this -> i = i;    }    Virt operator+(const Virt &x)    {        return Virt(r + x.r,i + x.i);    }    Virt operator-(const Virt &x)    {        return Virt(r - x.r,i - x.i);    }    Virt operator*(const Virt &x)    {        return Virt(r * x.r - i * x.i,x.r * i + r * x.i);    }};const int N = 131072;int n,m,LG,c[N],Rev[N];Virt a[N],b[N];char s[N >> 1];void Rader(){    for(n = 1;n <= m;n <<= 1,LG ++);    for(int i = 1;i < n;i ++)        Rev[i] = (Rev[i >> 1] >> 1) | ((i & 1) << (LG - 1));}void FFT(Virt *F,int ty = 1){    Rep_0(i,n)if(i < Rev[i])swap(F[i],F[Rev[i]]);    for(int i = 1;i < n;i <<= 1)    {        Virt wn(cos(PI / i),ty * sin(PI / i));        for(int j = 0;j < n;j += (i << 1))        {            Virt w(1,0);            for(int k = 0;k < i;k ++,w = w * wn)            {                Virt u = F[j + k],v = F[j + k + i] * w;                F[j + k] = u + v,F[j + k + i] = u - v;            }        }    }    if(ty == -1)Rep_0(i,n)F[i].r = F[i].r / n;}int main (){    scanf("%d",&n);    m = (n - 1) << 1;    scanf("%s",s);    Rep_0(i,n)a[i] = s[n - i - 1] - '0';    scanf("%s",s);    Rep_0(i,n)b[i] = s[n - i - 1] - '0';    Rader();    FFT(a),FFT(b);    Rep0n(i,n)a[i] = a[i] * b[i];    FFT(a,-1);    Rep0n(i,m)c[i] = (int)(a[i].r + 0.5);    Rep0n(i,m)    {        if(c[i] >= 10)        {            c[i + 1] += c[i] / 10,c[i] %= 10;            if(i == m)m ++;        }    }    for(int i = m;i >= 0;i --)printf("%d",c[i]);    return 0;}

992ms应该也不算特别慢了吧……
再来看一道模版题:
快速傅立叶之二
请计算C[k]=sigma(a[i]*b[i-k]) 其中 k < = i < n ,并且有 n < = 10 ^ 5。 a,b中的元素均为小于等于100的非负整数。
Input
第一行一个整数N,接下来N行,第i+2..i+N-1行,每行两个数,依次表示a[i],b[i] (0 < = i < N)。
Output
输出N行,每行一个整数,第i行输出C[i-1]。
原式 = > ?

Ck=i=kn1aibik=kn1aiRevbn+ki1

让我来冷静一下……
FFT解决的卷积不是:
Fk=i=0kaibki

话说这两个到底有什么相同点啊!
于是我重新冷静了一下:
我们一开始解决的Fk在第k位存储的是能满足把a和b的第i位的元素看作aixi这种形式,实际上根本没有所谓的xi,但是这给了我们一个提示……
从0 - > k:Fk存储的是a和b乘起来之后x的次数等于k的项的系数。
这次的题目k - > n:Ck存储的是a和b乘起来之后x的次数等于n + k的系数。
那就直接输出n - 1 - > 2 * (n - 1)位的结果即可。
其实这个题也给我提了个醒:
不要关心卷积的过程是什么,你只需要知道:
这个东西是一个卷积。
所以你只要求一下对应的次数的那些卷积就可以了。

#include <cstdio>#include <cstring>#include <algorithm>#include <cmath>#define Rep(i,n) for(int i = 1;i <= n;i ++)#define RepG(i,x) for(int i = head[x];~ i;i = edge[i].next)#define RD(i,x,n) for(int i = x;i <= n;i ++)#define Rep_0(i,n) for(int i = 0;i < n;i ++)#define Rep0n(i,n) for(int i = 0;i <= n;i ++)#define PI M_PIusing namespace std;struct Virt {    double r,i;    Virt (double r = 0.0,double i = 0.0)    {        this -> r = r;        this -> i = i;    }    Virt operator+(const Virt &x)    {        return Virt(r + x.r,i + x.i);    }    Virt operator-(const Virt &x)    {        return Virt(r - x.r,i - x.i);    }    Virt operator*(const Virt &x)    {        return Virt(r * x.r - i * x.i,x.r * i + r * x.i);    }};const int N = 131072 * 2;int n,m,LG,c[N],Rev[N];Virt a[N],b[N];char s[N >> 1];void Rader(){    for(n = 1;n <= m;n <<= 1,LG ++);    for(int i = 1;i < n;i ++)        Rev[i] = (Rev[i >> 1] >> 1) | ((i & 1) << (LG - 1));}void FFT(Virt *F,int ty = 1){    Rep_0(i,n)if(i < Rev[i])swap(F[i],F[Rev[i]]);    for(int i = 1;i < n;i <<= 1)    {        Virt wn(cos(PI / i),ty * sin(PI / i));        for(int j = 0;j < n;j += (i << 1))        {            Virt w(1,0);            for(int k = 0;k < i;k ++,w = w * wn)            {                Virt u = F[j + k],v = F[j + k + i] * w;                F[j + k] = u + v,F[j + k + i] = u - v;            }        }    }    if(ty == -1)Rep_0(i,n)F[i].r = F[i].r / n;}int main (){    scanf("%d",&n);    m = (n - 1) << 1;    Rep_0(i,n)        scanf("%lf%lf",&a[i].r,&b[n - i - 1].r);    Rader();    FFT(a),FFT(b);    Rep0n(i,n)a[i] = a[i] * b[i];    FFT(a,-1);    for(int i = m / 2;i <= m;i ++)printf("%d\n",c[i] = (int)(a[i].r + 0.5));    return 0;}

参考资料:
ACdreamers的博客
http://blog.csdn.net/acdreamers/article/details/39005227
zky的博客
http://blog.csdn.net/iamzky/article/details/22712347
算法导论

//请扫描以下二维码关注本人微信。

2 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 手机淘宝太卡怎么办 卖家不同意退货怎么办 游戏退出无响应怎么办 手机淘宝购物车打不开怎么办 淘宝店铺没有访客怎么办 淘宝店铺0流量怎么办 微信经常封号怎么办 网上拍卖堂违约怎么办 dnf4开组队制裁怎么办 红酒木塞丢了怎么办 红酒塞子进去了怎么办 淘金币即将过期怎么办 淘金币过期怎么办2018 换详情排名下降怎么办 长城宽带不用了怎么办 快递到了想退货怎么办 淘宝退货商家拒收怎么办 淘宝运费险失败怎么办 忘记购买运费险怎么办 咸鱼买家申请退款怎么办 熟猪肉有点变味怎么办 和领导意见不一致怎么办 骑手提前点送达怎么办 ubuntu安装报错怎么办 液相色谱两峰分不开怎么办 液相色谱柱老堵怎么办? 没有装usb驱动怎么办 ipad速度越来越慢怎么办 美萍管理软件打不开怎么办 小米4开机黑屏怎么办 小米电脑死机了怎么办 小米8手机死机怎么办 oppa7开不了机怎么办 oppo手机wlan打不开怎么办 三星s6进水黑屏怎么办 银行卡不支持快捷支付怎么办 路由器忘记管理员密码怎么办 云付没有推荐人怎么办 牛呗审核不通过怎么办 华硕笔记本很卡怎么办 淘宝换货没有货怎么办