算法学习FFT系列(1):初习快速傅里叶变换

算法学习FFT系列(1):初习快速傅里叶变换

引入

这个坑已经在我脑海里占了很久了,但是一直没有水平写,今天尝试着写写看FFT的算法学习。
FFT在OI中最大的作用是加速卷积。理论上背板子是没毛病的,但是仍然遇到了一些考定义的毒瘤题,所以还是理解比较好。

多项式乘法

定义

多项式 A(x)=inaixi,B(x)=inbixi A ( x ) = ∑ i n a i ∗ x i , B ( x ) = ∑ i n b i ∗ x i
多项式乘法就是

C(x)=i2n1(jiajbij)xi C ( x ) = ∑ i 2 n − 1 ( ∑ j i a j ∗ b i − j ) x i

显然,多项式乘法需要 O(n2) O ( n 2 ) 的复杂度。

表示方法

普通的多项式表示方法把n阶多项式 A(x) A ( x ) 表示为向量 A A
这里需要引入一种全新的表示方法——点值表示法。
也就是A(x)表示为 A(A(x0),A(x1),A(x2)A(xn)) A ( A ( x 0 ) , A ( x 1 ) , A ( x 2 ) ⋯ A ( x n ) )
n阶多项式和n个互不相同的点值表示一一对应。
这样的表示方法的优点是什么?考虑多项式乘法的过程。

C(x)=i2n1(jiajbij)xi C ( x ) = ∑ i 2 n − 1 ( ∑ j i a j ∗ b i − j ) x i
=i2n1ji(ajxj)(bijxij) = ∑ i 2 n − 1 ∑ j i ( a j ∗ x j ) ∗ ( b i − j ∗ x i − j )
=j2n1(ajxj)i2n1(bixi) = ∑ j 2 n − 1 ( a j ∗ x j ) ∗ ∑ i 2 n − 1 ( b i ∗ x i )

转化成点值表示的两个式子,不难发现,其乘法复杂度是 O(n) O ( n )

FFT的总路线:系数表达式->点值表达式->乘法->点值表达式->系数表达式

插值(*)

这个东西是顺便一提,FFT中系数表达式<->点值表达式过程并不是FFT的专属,但是确是FFT的关键。这一过程被称之为插值。证明插值的唯一性(也就是n阶多项式和n个互不相同的点值表示一一对应。)需要通过范德蒙德矩阵的可逆性。
111x0x1xnx20x21x2nxn0xn1xnna0a1an=y0y1yn [ 1 x 0 x 0 2 … x 0 n 1 x 1 x 1 2 ⋯ x 1 n … … … … … 1 x n x n 2 ⋯ x n n ] [ a 0 a 1 … a n ] = [ y 0 y 1 … y n ]
左边的矩阵表示为 V(x0,x1xn) V ( x 0 , x 1 … x n ) 就是范德蒙德矩阵
证明不会,自行百度。
插值相关还有这些东西,可以看看算法学习:拉格朗日插值
既然是点值表达式,最重要的就是带入什么点值,这里我们引入一个新的概念——单位复根。

单位复根

n n 次 单 位 复 根 的表达式是 ωn=1 ω n = 1 ,更形象地,我们可以通过复数运算的几何意义(幅角相加, 模相乘)得到下面两张图
这里写图片描述这里写图片描述
我们可以得到n次单位复根有n个,均匀分布在复平面上半径为1的圆上。

欧拉公式与单位复根表示

eix=cosx+isinx e i x = c o s x + i s i n x
证明的方法是泰勒展开。
带入 x=2πk x = 2 π k 得到
e2πki=cos(2πk)+isin(2πk)=1=ωn e 2 π k i = c o s ( 2 π k ) + i s i n ( 2 π k ) = 1 = ω n
于是 ωkn=e2πkin,k=0,1n ω n k = e 2 π k i n , k = 0 , 1 … n
其实 ωkn ω n k 构成了一个乘法群。。。不加以赘述。
有了表达式,我们就可以挖掘 ωkn ω n k 的性质了。

各种定理
消去引理

ωdkdn=ωkn ω d n d k = ω n k
证明: ωdkdn=e2πdkidn=e2πkin=ωkn ω d n d k = e 2 π d k i d n = e 2 π k i n = ω n k

折半引理

如果 n>0 n > 0 且n为偶数,那么n个n次单位复根的平方的集合就是 n2 n 2 n2 n 2 次单位复根的集合
证明:其实就是证明两个东西(1) (ωkn)2=ωkn2 ( ω n k ) 2 = ω n 2 k (2) ) (ωk+n2n)2=(ωkn)2 ( ω n k + n 2 ) 2 = ( ω n k ) 2
(1)(ωkn)2=ω2kn=ωkn2 ( 1 ) ( ω n k ) 2 = ω n 2 k = ω n 2 k
(2)(ωk+n2n)2=ω2k+nn=ωknωnn=(ωkn)2 ( 2 ) ( ω n k + n 2 ) 2 = ω n 2 k + n = ω n k ω n n = ( ω n k ) 2

求和引理

jn1(ωkn)j=0 ∑ j n − 1 ( ω n k ) j = 0
证明: j=0n1(ωkn)j=(wkn)n1wkn1=(wnn)k1wkn1=0(kmodn0) ∑ j = 0 n − 1 ( ω n k ) j = ( w n k ) n − 1 w n k − 1 = ( w n n ) k − 1 w n k − 1 = 0 ( k mod n ≠ 0 )
特别地: j=0n1(ωkn)j=n(kmodn=0) ∑ j = 0 n − 1 ( ω n k ) j = n ( k mod n = 0 )

前置技能已经get得差不多了,开始表演

离散型傅里叶变换和逆离散型傅里叶变换(DFT和IDFT)

应该还记得快速傅里叶变换要干什么吧。
快速插值。
我们要做的事情就是

(1) A(x)=(a0,a1an),A(x)=(A(ω0),A(ω1)A(ωn)) 已 知 A ( x ) = ( a 0 , a 1 … a n ) , 求 A ( x ) = ( A ( ω 0 ) , A ( ω 1 ) … A ( ω n ) )
(2) A(x)=(A(ω0),A(ω1)A(ωn)),A(x)=(a0,a1an) 已 知 A ( x ) = ( A ( ω 0 ) , A ( ω 1 ) … A ( ω n ) ) , 求 A ( x ) = ( a 0 , a 1 … a n )

刚才介绍了这么多优秀的单位复根的性质,于是我们容易想到,把单位复根带入多项式里面。(为了方便,我们用 ω ω 表示 ωn ω n )
假设序列 AB A 和 B 乘法之后得到的序列是 C C ,其长度是AB长度之和减一。我们找到一个最小n使得n大于C的长度并且n是2的整数次幂(为啥?之后会提到。)
我们可以把卷积变成这样的形式。
cr=p,q[p+qmodn=r]apbq c r = ∑ p , q [ p + q mod n = r ] a p b q
由求和引理可得 1nj=0n1(ωk)j=[kmodn=0] 1 n ∑ j = 0 n − 1 ( ω k ) j = [ k mod n = 0 ]
于是 [p+qmodn=r]=[p+qrmodn=0]=1nk=0n1(ωp+qr)k=1nk=0n1ωrkωpkωqk [ p + q mod n = r ] = [ p + q − r mod n = 0 ] = 1 n ∑ k = 0 n − 1 ( ω p + q − r ) k = 1 n ∑ k = 0 n − 1 ω − r k ω p k ω q k
cr=p,q[p+qrmodn=0]apbq c r = ∑ p , q [ p + q − r mod n = 0 ] a p b q
=p,q1nk=0n1ωrkωpkωqkapbq = ∑ p , q 1 n ∑ k = 0 n − 1 ω − r k ω p k ω q k a p b q
=1nk=0n1ωrkpωpkapqωqkbq = 1 n ∑ k = 0 n − 1 ω − r k ∑ p ω p k a p ∑ q ω q k b q
=1nk=0n1ωrkA(ωk)B(ωk)=1nk=0n1ωrkC(ωk) = 1 n ∑ k = 0 n − 1 ω − r k A ( ω k ) B ( ω k ) = 1 n ∑ k = 0 n − 1 ω − r k C ( ω k )
刚才经过一波推导我们成功地找到了 C C 点值表达式和C系数表达式的关系。
这样子的话问题转化为

(1) A(ωk)=pωkpap 求 A ( ω k ) = ∑ p ω k p a p
(2) ar=1nrωkrA(ωk) 求 a r = 1 n ∑ r ω − k r A ( ω k )
前者即为DFT,后者即是IDFT

我们不难发现,两者的过程惊人的相似,其实只是多了 1n 1 n 和一个-
于是我们使用同一个算法——快速傅里叶算法(FFT)来实现这玩意儿。

快速傅里叶变换(FFT)

A0(x)A(x)A1(x)A(x) A 0 ( x ) 是 A ( x ) 偶 次 项 的 和 , A 1 ( x ) 是 A ( x ) 奇 次 项 的 和
注意到 (ωmn)2=(ωm+n2n)2=ωmn2 ( ω n m ) 2 = ( ω n m + n 2 ) 2 = ω n 2 m
A(ωmn)=A0((ωmn)2)+ωmnA1((ωmn)2)=A0(ωmn2)+ωmnA1(ωmn2) A ( ω n m ) = A 0 ( ( ω n m ) 2 ) + ω n m A 1 ( ( ω n m ) 2 ) = A 0 ( ω n 2 m ) + ω n m A 1 ( ω n 2 m )
A(ωm+n2n)=A0((ωmn)2)+ωm+n2nA1((ωmn)2)=A0(ωmn2)ωmnA1(ωmn2) A ( ω n m + n 2 ) = A 0 ( ( ω n m ) 2 ) + ω n m + n 2 A 1 ( ( ω n m ) 2 ) = A 0 ( ω n 2 m ) − ω n m A 1 ( ω n 2 m )
这就是单位复根最英霸的地方,折半引理和消去引理可以使得它能够把插值的过程分治。上述操作被称为蝴蝶操作。
上文有提到,n是2的整数次幂,所以这个过程可以一直递推下去。不难发现,上述算法的时间复杂度递推式是。
T(n)=2T(n2)+O(logn) T ( n ) = 2 T ( n 2 ) + O ( l o g n )
由主定理可得时间复杂度为 O(nlogn) O ( n l o g n )
IDFT有两种写法,第一种是老老实实地带一个负号进去,其实还有第二种写法。
ar=1nrωkrA(ωk)=1nrω(nk)rA(ωk) a r = 1 n ∑ r ω − k r A ( ω k ) = 1 n ∑ r ω ( n − k ) r A ( ω k )
我们把A数组反转一下,DFT之后除以n就好了。

代码实现

注意到 ωkn=e2πkin=cos(2πkn)+isin(2πkn) ω n k = e 2 π k i n = c o s ( 2 π k n ) + i s i n ( 2 π k n )
所以我们全程是用欧拉公式来表示 ω ω
还有一点,蝴蝶操作本来我们是要迭代的,但是这里有一个优化常数的小技巧。
考虑原序列是 (a0,a1,a2,a3,a4,a5,a6,a7) ( a 0 , a 1 , a 2 , a 3 , a 4 , a 5 , a 6 , a 7 )
模拟蝴蝶变换的过程。
(a0,a2,a4,a6,a1,a3,a5,a7) ( a 0 , a 2 , a 4 , a 6 , a 1 , a 3 , a 5 , a 7 )
(a0,a4,a2,a6,a1,a5,a3,a7) ( a 0 , a 4 , a 2 , a 6 , a 1 , a 5 , a 3 , a 7 )
如果用二进制表示原序列和变换后的序列
(000,001,010,011,100,101,110,111) ( 000 , 001 , 010 , 011 , 100 , 101 , 110 , 111 )
(000,100,010,110,001,101,011,111) ( 000 , 100 , 010 , 110 , 001 , 101 , 011 , 111 )
发现其实就是二进制反转了。
然后我们就可以用递推写这个东西了,具体看代码吧。

//luoguP3803 【模板】多项式乘法(FFT) 
//一个比较易于理解的版本,其实可以写得更简洁。
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 5e6 + 10;
const double pi = acos(-1.0);
struct cp {
    double r, i;
    cp(double _r = 0, double _i = 0) : r(_r), i(_i) {}
    cp operator + (cp a) {return cp(r + a.r, i + a.i);}
    cp operator - (cp a) {return cp(r - a.r, i - a.i);}
    cp operator * (double a) {return cp(r * a, i * a);}
    cp operator * (cp a) {return cp(r * a.r - i * a.i, r * a.i + i * a.r);}
}a[N], b[N];
int r[N], m;
void FFT(cp *F, int f) {
    for(int i = 0;i < m; ++i) if(i < r[i]) swap(F[i], F[r[i]]);
    for(int i = 1; i < m; i <<= 1) {
        cp wn(cos(pi / i), f * sin(pi / i));
        for(int j = 0;j < m; j += (i << 1)) {
            cp w(1, 0);
            for(int l = 0;l < i; ++l, w = w * wn) {
                cp x = F[j + l], y = w * F[j + i + l];
                F[j + l] = x + y; F[j + i + l] = x - y;
            }
        }
    }
    if(!~f) for(int i = 0;i < m; ++i) F[i].r /= m;
}
int main() {
    int n1, n2; scanf("%d%d", &n1, &n2);
    for(int i = 0, x;i <= n1; ++i) scanf("%d", &x), a[i] = cp(x, 0);
    for(int i = 0, x;i <= n2; ++i) scanf("%d", &x), b[i] = cp(x, 0);
    int L = 0; for(m = 1; (m <<= 1) <= (n1 + n2); ++L) ;
    for(int i = 1; i < m; ++i) r[i] = (r[i >> 1] >> 1) | (i & 1) << L;
    FFT(a, 1); FFT(b, 1); for(int i = 0;i < m; ++i) a[i] = a[i] * b[i];
    FFT(a, -1); 
    for(int i = 0;i <= n1 + n2; ++i) printf("%d ", (int)(a[i].r + 0.5)); puts("");
    return 0;
}

后记

参考博文

[学习笔记] 多项式与快速傅里叶变换(FFT)基础
Pick‘s Blog 里面有各种FFT系列的东西

后续博文

算法学习FFT系列(2):快速数论变换NTT
算法学习FFT系列(3):多项式求逆详解——NTT+分治
占坑!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值