FFT:
引言
FFT最广泛使用的一个领域就是加速多项式相乘。
并且能够把多项式乘法的复杂度从 O ( n 2 ) O(n^2) O(n2)降低到 O ( n l o g n ) O(nlogn) O(nlogn)。传统的我们计算两个多项式相乘如: A ( x ) = x 2 + 3 x + 2 A(x)=x^2+3x+2 A(x)=x2+3x+2与 B ( x ) = 2 x 2 + 1 B(x)=2x^2+1 B(x)=2x2+1计算 C ( x ) = A ( x ) ⋅ B ( x ) C(x)=A(x)\cdot B(x) C(x)=A(x)⋅B(x)则
C ( x ) = ( x 2 + 3 x + 2 ) ( 2 x 2 + 1 ) = x 2 ( 2 x 2 + 1 ) + 3 x ( 2 x 2 + 1 ) + 2 ( 2 x 2 + 1 ) = 2 x 4 + x 2 + 6 x 3 + 3 x + 4 x 2 + 2 = 2 x 4 + 6 x 3 + 5 x 2 + 3 x + 2 \begin{aligned} C(x) &= (x^2 + 3x + 2)(2x^2 + 1) \\ &= x^2(2x^2 + 1) + 3x(2x^2 + 1) + 2(2x^2 + 1) \\ &= 2x^4 + x^2 + 6x^3 + 3x + 4x^2 + 2 \\ &= 2x^4 + 6x^3 + 5x^2 + 3x + 2 \end{aligned} C(x)=(x2+3x+2)(2x2+1)=x2(2x2+1)+3x(2x2+1)+2(2x2+1)=2x4+x2+6x3+3x+4x2+2=2x4+6x3+5x2+3x+2
上面是我们通常计算多项式相乘的时候所用到的朴素算法(如果两个相乘的多项式都是n次的话,那么计算其相乘的效率就是 O ( n 2 ) O(n^2) O(n2)),不同于此基于FFT可以构造一种更加复杂但是有效的计算方式。
多项式的存储
首先:我们思考要如何存储一个多项式?
答案有两个:
1.系数表示法
A ( x ) = x 2 + 3 x + 2 → A = [ 2 , 3 , 1 ] A(x)=x^2+3x+2 \quad\rightarrow \quad A=[2,3,1] A(x)=x2+3x+2→A=[2,3,1]
B ( x ) = 2 x 2 + 1 → B = [ 1 , 0 , 2 ] B(x)=2x^2+1\quad\rightarrow \quad B=[1,0,2] B(x)=2x2+1→B=[1,0,2],
C ( x ) = 2 x 4 + 6 x 3 + 5 x 2 + 3 x + 2 → C = [ 2 , 3 , 5 , 6 , 2 ] C(x)=2x^4+6x^3+5x^2+3x+2 \quad \rightarrow \quad C=[2,3,5,6,2] C(x)=2x4+6x3+5x2+3x+2→C=[2,3,5,6,2]
2.值表示法
注:值表示法并不具有普遍性,因为通过值表示我们无法便捷得知x任意取值所得到的y的值
多项式插值的唯一性定理:
若给定 d + 1 d + 1 d+1 个横坐标互不相同的点 ( x 0 , y 0 ) , ( x 1 , y 1 ) , … , ( x d , y d ) (x_0, y_0), (x_1, y_1), \dots, (x_d, y_d) (x0,y0),(x1,y1),…,(xd,yd)(即所有 x i x_i xi 互不相等),则存在且仅存在一个次数不超过 d d d 的多项式 P ( x ) P(x) P(x),使得对所有 i = 0 , 1 , … , d i = 0, 1, \dots, d i=0,1,…,d,都满足 P ( x i ) = y i P(x_i) = y_i P(xi)=yi。
解释:任意一d阶多项式都由d+1个点唯一确定。
证明:
对于d次多项式: P ( x ) = p 0 + p 1 x + p 2 x 2 + ⋯ + p d x d P(x) = p_0 + p_1 x + p_2 x^2 + \dots + p_d x^d P(x)=p0+p1x+p2x2+⋯+pdxd
其存在d+1个互不相同的点: ( x 0 , P ( x 0 ) ) , ( x 1 , P ( x 1 ) ) , ⋯ , ( x d , P ( x d ) ) {(x_0,P(x_0)),(x_1,P(x_1)),\cdots,(x_d,P(x_d))} (x0,P(x0)),(x1,P(x1)),⋯,(xd,P(xd))
代入点,可得方程组
{ P ( x 0 ) = p 0 + p 1 x 0 + p 2 x 0 2 + ⋯ + p d x 0 d P ( x 1 ) = p 0 + p 1 x 1 + p 2 x 1 2 + ⋯ + p d x 1 d ⋮ P ( x d ) = p 0 + p 1 x d + p 2 x d 2 + ⋯ + p d x d d \begin{cases} P(x_0) = p_0 + p_1 x_0 + p_2 x_0^2 + \dots + p_d x_0^d \\ P(x_1) = p_0 + p_1 x_1 + p_2 x_1^2 + \dots + p_d x_1^d \\ \vdots \\ P(x_d) = p_0 + p_1 x_d + p_2 x_d^2 + \dots + p_d x_d^d \end{cases} ⎩
⎨
⎧P(x0)=p0+p1x0+p2x02+⋯+pdx0dP(x1)=p0+p1x1+p2x12+⋯+pdx1d⋮P(xd)=p0+p1xd+p2xd2+⋯+pdxdd
矩阵形式:
[ P ( x 0 ) P ( x 1 ) ⋮ P ( x d ) ] = [ 1 x 0 x 0 2 … x 0 d 1 x 1 x 1 2 … x 1 d ⋮ ⋮ ⋮ ⋱ ⋮ 1 x d x d 2 … x d d ] [ p 0 p 1 ⋮ p d ] \begin{bmatrix} P(x_0) \\ P(x_1) \\ \vdots \\ P(x_d) \end{bmatrix} =\begin{bmatrix} 1 & x_0 & x_0^2 & \dots & x_0^d \\ 1 & x_1 & x_1^2 & \dots & x_1^d \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_d & x_d^2 & \dots & x_d^d \end{bmatrix} \begin{bmatrix} p_0 \\ p_1 \\ \vdots \\ p_d \end{bmatrix}
P(x0)P(x1)⋮P(xd)
=
11⋮1x0x1⋮xdx02x12⋮xd2……⋱…x0dx1d⋮xdd
p0p1⋮pd
(中间的矩阵为范德蒙德矩阵(Vandermonde matrix))
其中因为 x 0 , x 1 , … , x d x_0, x_1, \dots, x_d x0,x1,…,xd 各不相同,则矩阵 M M M 可逆
⇒ \Rightarrow ⇒ 系数 p 0 , p 1 , … , p d p_0, p_1, \dots, p_d p0,p1,…,pd 存在且唯一
⇒ \Rightarrow ⇒ 多项式 P ( x ) P(x) P(x) 存在且唯一
多项式相乘计算的新方法:
方法概述:
通常情况下我们习惯于使用系数表示法,但是这种表示方法对于两个多项式相乘运算的效率是 O ( n 2 ) O(n^2) O(n2),如何改变这种困局,利用值表示法?:
例如:
A ( x ) = x 2 + 2 x + 1 A(x) = x^2 + 2x + 1 A(x)=x2+2x+1
值表示法: [ ( − 2 , 1 ) , ( − 1 , 0 ) , ( 0 , 1 ) , ( 1 , 4 ) , ( 2 , 9 ) ] [(-2, 1), (-1, 0), (0, 1), (1, 4), (2, 9)] [(−2,1),(−1,0),(0,1),(1,4),(2,9)]
B ( x ) = x 2 − 2 x + 1 B(x) = x^2 - 2x + 1 B(x)=x2−2x+1
值表示法: [ ( − 2 , 9 ) , ( − 1 , 4 ) , ( 0 , 1 ) , ( 1 , 0 ) , ( 2 , 1 ) ] [(-2, 9), (-1, 4), (0, 1), (1, 0), (2, 1)] [(−2,9),(−1,4),(0,1),(1,0),(2,1)]
使用值表示法的时:如果想要计算 A × B A \times B A×B所得到的 C C C的值表示法其实很简单,就是选择大于【A的最大阶数和B的最大阶数相乘所得到阶数(这个阶数是C的最大阶数)】个数的横坐标,然后让相同横坐标的纵坐标相乘,横坐标保持不变,所得到的就是 C C C上某一点的坐标。例如上面: A A A的最大阶数是2, B B B的最大阶数也是2,可得 C C C的最大阶数应该是4,选择的坐标个数应该大于其阶数
[ ( − 2 , 1 ) , ( − 1 , 0 ) , ( 0 , 1 ) , ( 1 , 4 ) , ( 2 , 9 ) ] [(-2, 1), (-1, 0), (0, 1), (1, 4), (2, 9)] [(−2,1),(−1,0),(0,1),(1,4),(2,9)]
× [ ( − 2 , 9 ) , ( − 1 , 4 ) , ( 0 , 1 ) , ( 1 , 0 ) , ( 2 , 1 ) ] \times[(-2, 9), (-1, 4), (0, 1), (1, 0), (2, 1)] ×[(−2,9),(−1,4),(0,1),(1,0),(2,1)]
= [ ( − 2 , 9 ) , ( − 1 , 0 ) , ( 0 , 1 ) , ( 1 , 0 ) , ( 2 , 9 ) ] =[(-2, 9), (-1, 0), (0, 1), (1, 0), (2, 9)] =[(−2,9),(−1,0),(0,1),(1,0),(2,9)]
将上面的值表示法 [ ( − 2 , 9 ) , ( − 1 , 0 ) , ( 0 , 1 ) , ( 1 , 0 ) , ( 2 , 9 ) ] [(-2, 9), (-1, 0), (0, 1), (1, 0), (2, 9)] [(−2,9),(−1,0),(0,1),(1,0),(2,9)]转化为系数表示法:
C ( x ) = x 4 − 2 x 2 + 1 C(x) = x^4 - 2x^2 + 1 C(x)=x4−2x2+1
存在的问题:
通过上面的运算看似更加简便了,然而复杂度却提升到了 O ( n d ) O(nd) O(nd)(n是选择的坐标点数,d是阶数)注: O ( d ) O(d) O(d)是因为对每一个点带入计算值的复杂度, O ( n ) O(n) O(n)是因为坐标点的个数,因为每一个点都要带入计算值,所以可得到所有的值计算的效率就是 O ( n d ) O(nd) O(nd)
上述的操作可以用下面的图来表示:
解决问题:
如何对其进行优化?这里的优化主要是对点带入求值的优化,也就是系数表示法转化为值表示法的优化。这正是FFT发挥作用的时候了!
利用奇偶性和递归思想:
简化问题:
对于 P ( x ) = x 2 P(x) = x^2 P(x)=x2
如果要选择8个点应该选哪些点?
在奇偶函数情况下
只需要计算一半数量点,另一半可以从奇偶性中得出:
( 1 , 1 ) → ( − 1 , 1 ) ( 2 , 4 ) → ( − 2 , 4 ) ( 3 , 9 ) → ( − 3 , 9 ) ( 4 , 16 ) → ( − 4 , 16 ) \begin{aligned} (1, 1) &\rightarrow (-1, 1) \\ (2, 4) &\rightarrow (-2, 4) \\ (3, 9) &\rightarrow (-3, 9) \\ (4, 16) &\rightarrow (-4, 16) \end{aligned} (1,1)(2,4)(3,9)(4,16)→(−1,1)→(−2,4)→(−3,9)→(−4,16)
举例:
P ( x ) = 3 x 5 + 2 x 4 + x 3 + 7 x 2 + 5 x + 1 P(x) = 3x^5 + 2x^4 + x^3 + 7x^2 + 5x + 1 P(x)=3x5+2x4+x3+7x2+5x+1
P ( x ) = ( 2 x 4 + 7 x 2 + 1 ) + ( 3 x 5 + x 3 + 5 x ) P(x) = (2x^4 + 7x^2 + 1) + (3x^5 + x^3 + 5x) P(x)=(2x4+7x2+1)+(3x5+x3+5x)
把多项式拆奇偶两部分
= ( 2 x 4 + 7 x 2 + 1 ) ⏟ P e ( x 2 ) + x ⋅ ( 3 x 4 + x 2 + 5 ) ⏟ P o ( x 2 ) = \underbrace{(2x^4 + 7x^2 + 1)}_{P_e(x^2)} + x \cdot \underbrace{(3x^4 + x^2 + 5)}_{P_o(x^2)} =Pe(x2)
(2x4+7x2+1)+x⋅Po(x2)
(3x4+x2+5)
{ P ( x ) = P e ( x 2 ) + x ⋅ P o ( x 2 ) P ( − x ) = P e ( x 2 ) − x ⋅ P o ( x 2 ) \begin{cases} P(x) = P_e(x^2) + x \cdot P_o(x^2) \\ P(-x) = P_e(x^2) - x \cdot P_o(x^2) \end{cases} {P(x)=Pe(x2)+x⋅Po(x2)P(−x)=Pe(x2)−x⋅Po(x2) ← lot of overlap
P e ( x 2 ) = 2 x 4 + 7 x 2 + 1 P_e(x^2) = 2x^4 + 7x^2 + 1 Pe(x2)=2x4+7x2+1
P o ( x 2 ) = 3 x 4 + x 2 + 5 P_o(x^2) = 3x^4 + x^2 + 5 Po(x2)=3x4+x2+5
P e ( x 2 ) P_e(x^2) Pe(x2) P o ( x 2 ) P_o(x^2) Po(x2)都仅仅只有两阶 ,且和原始多项式 P ( x ) P(x) P(x)类似的有奇有偶的多项式
这让我们想到了递归的思想
抽象如下:
目的:多项式系数表示法转化为值表示法,所以我们需要选点和求值
1.对于n-1阶的多项式 P ( x ) P(x) P(x),我们需要选择n个点,要求n个点在原点两侧均匀对称分布(奇偶性)
2.根据每一项 x x x的阶数将P分为奇偶两部分分别为 P o P_o Po和 P e P_e Pe,将 P 0 P_0 P0和 P e P_e Pe对应的二次项作为整体(类似换参)又可以得到和原始多项式 P ( x ) P(x) P(x)类似的有奇有偶的多项式,只不过其最高阶已经变为 n / 2 − 1 n/2-1 n/2−1,简化了问题(递归思想)
3.回到第一步(如此递归)
出现新问题:(递归条件不符合)
但是这样做我们也引入了新的问题:
在上述的递归操作中我们假设了每个多项式都选择相反数(对称点)来求值,然而对于子问题而言,每一个求值点都是平方数即都是正数,所以递归操作不成立!!
引入复数域单位根:
我们能不能把这些新的求值点弄成相反数呢?———复数!
所以我们要挑选一些复数使得其平方之后依旧是正负成对出现的
例子:
对于多项式 P ( x ) = x 3 + x 2 − x − 1 P(x)=x^3+x^2-x-1 P(x)=x3+x2−x−1
递归函数真正开始运算的过程是如下图:
即:从 x 1 , − x 1 , x 2 , − x 2 x_1,-x_1,x_2,-x_2 x1,−x1,x2,−x2到 x 1 2 , − x 1 2 x_1^2,-x_1^2 x12,−x12最后到 x 1 4 x_1^4 x14
因为对 x 1 x_1 x1的赋值可以由我们掌控,不妨引入 x 1 = 1 x_1=1 x1=1,(引入四次单位根问题)
则求解所有的根就是解方程: x 1 4 = 1 x_1^4=1 x14=1
同样的思路对于 P ( x ) = x 5 + 2 x 4 − x 3 + x 2 + 1 P(x)=x^5+2x^4-x^3+x^2+1 P(x)=x5+2x4−x3+x2+1
为求解8次单位根问题
接下来我们将了解一下1的n次方根问题(n次单位根问题)提问:为什么n次方根就可以用于递归过程呢?
1的n次方根可以被解释为复平面上沿着单位圆等距排布的一系列点,且任意两点之间的夹角就是 2 π n \frac{2\pi} {n} n2π,在复平面我们使用欧拉公式来描述此: e i θ = cos θ + i sin θ e^{i\theta} = \cos\theta + i\sin\theta eiθ=cosθ+isinθ
[欧拉公式描述了复数域中虚指数函数 e i θ e^{i\theta} eiθ 与三角函数 $\cos\theta , , ,i\sin\theta 的等价关系,复数极坐标下的角度(辐角 的等价关系,复数极坐标下的角度(辐角 的等价关系,复数极坐标下的角度(辐角 \theta )则是复平面内该复数对应点与原点连线和实轴正方向的夹角,满足 )则是复平面内该复数对应点与原点连线和实轴正方向的夹角,满足 )则是复平面内该复数对应点与原点连线和实轴正方向的夹角,满足\tan\theta = \frac{\text{复数虚部}}{\text{复数实部}}$,并决定复数极坐标形式的方向]
为了更方便的表示,我们定义** ω = e 2 π i n \omega = e^{\frac{2\pi i}{n}} ω=en2πi**(这里的角度 θ = 2 π n \theta=\frac{2\pi}{n} θ=n2π取决于n的值)
之后我们选择的点就是 [ 1 , ω 1 , ω 2 , ⋯ , ω n − 1 ] [1,\omega^1,\omega^2,\cdots,\omega^{n-1}] [1,ω1,ω2,⋯,ωn−1]
一共n个点,如下图所示(i不是变量只是虚数单位):
引入复数域单位根之后递归成立的解释:
回到最初的问题:为什么n次方根就可以用于递归过程呢?
reason1:
我们要求所取的点是正负成对的,而这里的 ω ( j + n 2 ) = − ω j \omega^{(j+\frac{n}{2})}=-\omega^j ω(j+2n)=−ωj正好是这一对(见下面的虚线)
reason2:
平方之后的 n 2 \frac{n}{2} 2n个 n 2 \frac{n}{2} 2n次方根,它们也是正负配对的,完美适配本算法的递归条件
FFT流程和伪代码:
其他都好理解,主要是这一块:
首先数组 y y y是用来存储值的,第m个位置对的是次根 ω m \omega^m ωm( ω m = e 2 π m i n \omega^m = e^{\frac{2\pi m i}{n}} ωm=en2πmi)
最开始执行这段代码时候是在递归的最底层
P ( x j ) = P e ( x j 2 ) + x j P o ( x j 2 ) P ( − x j ) = P e ( x j 2 ) − x j P o ( x j 2 ) j ∈ { 0 , 1 , … ( n / 2 − 1 ) } \begin{aligned} P(x_j) &= P_e(x_j^2) + x_j P_o(x_j^2) \\ P(-x_j) &= P_e(x_j^2) - x_j P_o(x_j^2) \\ j &\in \{0, 1, \dots (n/2 - 1)\} \end{aligned} P(xj)P(−xj)j=Pe(xj2)+xjPo(xj2)=Pe(xj2)−xjPo(xj2)∈{0,1,…(n/2−1)}
由于我们规定 x j = w j x_j=w^j xj=wj上式可以改写为:
P ( ω j ) = P e ( ω 2 j ) + ω j P o ( ω 2 j ) P ( − ω j ) = P e ( ω 2 j ) − ω j P o ( ω 2 j ) j ∈ { 0 , 1 , … , ( n / 2 − 1 ) } \begin{aligned} P(\omega^j) &= P_e(\omega^{2j}) + \omega^j P_o(\omega^{2j}) \\ P(-\omega^j) &= P_e(\omega^{2j}) - \omega^j P_o(\omega^{2j}) \\ j &\in \{0, 1, \dots, (n/2 - 1)\} \end{aligned} P(ωj)P(−ωj)j=Pe(ω2j)+ωjPo(ω2j)=Pe(ω2j)−ωjPo(ω2j)∈{0,1,…,(n/2−1)}
而我们知道 − ω j = w j + n / 2 -\omega^j=w^{j+n/2} −ωj=wj+n/2带入可得:
P ( ω j ) = P e ( ω 2 j ) + ω j P o ( ω 2 j ) P ( ω j + n / 2 ) = P e ( ω 2 j ) − ω j P o ( ω 2 j ) j ∈ { 0 , 1 , … , ( n / 2 − 1 ) } \begin{aligned} P(\omega^j) &= P_e(\omega^{2j}) + \omega^j P_o(\omega^{2j}) \\ P(\omega^{j+n/2}) &= P_e(\omega^{2j}) - \omega^j P_o(\omega^{2j}) \\ j &\in \{0, 1, \dots, (n/2 - 1)\} \end{aligned} P(ωj)P(ωj+n/2)j=Pe(ω2j)+ωjPo(ω2j)=Pe(ω2j)−ωjPo(ω2j)∈{0,1,…,(n/2−1)}
右侧都写成y的形式:
P ( ω j ) = y e [ j ] + ω j y o [ j ] P ( ω j + n / 2 ) = y e [ j ] − ω j y o [ j ] j ∈ { 0 , 1 , … , ( n / 2 − 1 ) } \begin{aligned} P(\omega^j) &= y_e[j] + \omega^j y_o[j] \\ P(\omega^{j+n/2}) &= y_e[j] - \omega^j y_o[j] \\ j &\in \{0, 1, \dots, (n/2 - 1)\} \end{aligned} P(ωj)P(ωj+n/2)j=ye[j]+ωjyo[j]=ye[j]−ωjyo[j]∈{0,1,…,(n/2−1)}
由于:数组 y y y是用来存储值(理解为函数值或者多项式值)的,第m个位置对的是次根 ω m \omega^m ωm
y ( ω j ) = y e [ j ] + ω j y o [ j ] y ( ω j + n / 2 ) = y e [ j ] − ω j y o [ j ] j ∈ { 0 , 1 , … , ( n / 2 − 1 ) } \begin{aligned} y(\omega^j) &= y_e[j] + \omega^j y_o[j] \\ y(\omega^{j+n/2}) &= y_e[j] - \omega^j y_o[j] \\ j &\in \{0, 1, \dots, (n/2 - 1)\} \end{aligned} y(ωj)y(ωj+n/2)j=ye[j]+ωjyo[j]=ye[j]−ωjyo[j]∈{0,1,…,(n/2−1)}
最后以n=4为例对这个分割做一个理解首先我们需要明确P是一个数组,先获取P的长度,需要确保原始的P的长度是2的幂次方。
n=1的时候此时P无法再分割,则其为递归出口
n不等于1时候,对P进行分割为奇 P o P_o Po偶 P e P_e Pe两部分
因为这两部分的性质和原始P类似,所以可以递归处理这两部分。
递归到底层之后n等于1,以下图蓝色箭头这一支为例:
可以得到 y e = P ( ω 0 ) , y o = 0 y_e=P(\omega^0),y_o=0 ye=P(ω0),yo=0,确实无法再次递归了,然后开始执行递归之后的代码:
y = [ 0 ] × n y=[0]\times n y=[0]×n这里所得到的 y y y是包含一个元素的数组
此时j只能等于0,可以得到 y [ 0 ] = y e − ω j y o [ j ] y[0]=y_e-\omega^jy_o[j] y[0]=ye−ωjyo[j],即可得到 y [ 0 ] y[0] y[0]也就是 p 0 p_0 p0( ω 0 \omega^0 ω0对应的函数值)
同理可以得到 p 2 p_2 p2也就是 y [ 1 ] y[1] y[1]的值。这两次函数返回的值都是单个的取值,等到他们返回上一个递归时候,
继续往下执行此时n=2,所以此时y的长度就是2,在for循环里会执行两次,正好将 p 0 p_0 p0和 p 2 p_2 p2嵌到y中。
FFT的逆(IFFT):通过系数表示法到值表示法:
结论:IFFT的操作和FFT一模一样,只是把里面的 ω \omega ω换成了 1 n ω − 1 \frac{1}{n}\omega^{-1} n1ω−1
代码也仅仅有一处改动
推导:
对于多项式 P ( x ) = p 0 + p 1 x + p 2 x 2 + ⋯ + p n − 1 x n − 1 P(x) = p_0 + p_1 x + p_2 x^2 + \dots + p_{n-1} x^{n-1} P(x)=p0+p1x+p2x2+⋯+pn−1xn−1
FFT的通过系数求值的过程可以通过矩阵的形式表示:
[ P ( x 0 ) P ( x 1 ) P ( x 2 ) ⋮ P ( x n − 1 ) ] = [ 1 x 0 x 0 2 … x 0 n − 1 1 x 1 x 1 2 … x 1 n − 1 1 x 2 x 2 2 … x 2 n − 1 ⋮ ⋮ ⋮ ⋱ ⋮ 1 x n − 1 x n − 1 2 … x n − 1 n − 1 ] [ p 0 p 1 p 2 ⋮ p n − 1 ] \begin{bmatrix} P(x_0) \\ P(x_1) \\ P(x_2) \\ \vdots \\ P(x_{n-1}) \end{bmatrix} = \begin{bmatrix} 1 & x_0 & x_0^2 & \dots & x_0^{n-1} \\ 1 & x_1 & x_1^2 & \dots & x_1^{n-1} \\ 1 & x_2 & x_2^2 & \dots & x_2^{n-1} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{n-1} & x_{n-1}^2 & \dots & x_{n-1}^{n-1} \end{bmatrix} \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} P(x0)P(x1)P(x2)⋮P(xn−1) = 111⋮1x0x1x2⋮xn−1x02x12x22⋮xn−12………⋱…x0n−1x1n−1x2n−1⋮xn−1n−1 p0p1p2⋮pn−1
[ P ( ω 0 ) P ( ω 1 ) P ( ω 2 ) ⋮ P ( ω n − 1 ) ] = [ 1 1 1 … 1 1 ω ω 2 … ω n − 1 1 ω 2 ω 4 … ω 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 ω n − 1 ω 2 ( n − 1 ) … ω ( n − 1 ) ( n − 1 ) ] [ p 0 p 1 p 2 ⋮ p n − 1 ] \begin{bmatrix} P(\omega^0) \\ P(\omega^1) \\ P(\omega^2) \\ \vdots \\ P(\omega^{n-1}) \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & \dots & 1 \\ 1 & \omega & \omega^2 & \dots & \omega^{n-1} \\ 1 & \omega^2 & \omega^4 & \dots & \omega^{2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{n-1} & \omega^{2(n-1)} & \dots & \omega^{(n-1)(n-1)} \end{bmatrix} \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} P(ω0)P(ω1)P(ω2)⋮P(ωn−1) = 111⋮11ωω2⋮ωn−11ω2ω4⋮ω2(n−1)………⋱…1ωn−1ω2(n−1)⋮ω(n−1)(n−1) p0p1p2⋮pn−1
图中的矩阵我们称为离散傅里叶变换(DFT)矩阵
而通过值求系数(即IFFT)也可以通过矩阵表示:
[ p 0 p 1 p 2 ⋮ p n − 1 ] = [ 1 1 1 ⋯ 1 1 ω ω 2 ⋯ ω n − 1 1 ω 2 ω 4 ⋯ ω 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 ω n − 1 ω 2 ( n − 1 ) ⋯ ω ( n − 1 ) ( n − 1 ) ] − 1 [ P ( ω 0 ) P ( ω 1 ) P ( ω 2 ) ⋮ P ( ω n − 1 ) ] \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega & \omega^2 & \cdots & \omega^{n-1} \\ 1 & \omega^2 & \omega^4 & \cdots & \omega^{2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{n-1} & \omega^{2(n-1)} & \cdots & \omega^{(n-1)(n-1)} \end{bmatrix}^{-1} \begin{bmatrix} P(\omega^0) \\ P(\omega^1) \\ P(\omega^2) \\ \vdots \\ P(\omega^{n-1}) \end{bmatrix}
p0p1p2⋮pn−1
=
111⋮11ωω2⋮ωn−11ω2ω4⋮ω2(n−1)⋯⋯⋯⋱⋯1ωn−1ω2(n−1)⋮ω(n−1)(n−1)
−1
P(ω0)P(ω1)P(ω2)⋮P(ωn−1)
化简可得:
[ p 0 p 1 p 2 ⋮ p n − 1 ] = 1 n [ 1 1 1 … 1 1 ω − 1 ω − 2 … ω − ( n − 1 ) 1 ω − 2 ω − 4 … ω − 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 ω − ( n − 1 ) ω − 2 ( n − 1 ) … ω − ( n − 1 ) ( n − 1 ) ] [ P ( ω 0 ) P ( ω 1 ) P ( ω 2 ) ⋮ P ( ω n − 1 ) ] \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} = \frac{1}{n} \begin{bmatrix} 1 & 1 & 1 & \dots & 1 \\ 1 & \omega^{-1} & \omega^{-2} & \dots & \omega^{-(n-1)} \\ 1 & \omega^{-2} & \omega^{-4} & \dots & \omega^{-2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{-(n-1)} & \omega^{-2(n-1)} & \dots & \omega^{-(n-1)(n-1)} \end{bmatrix} \begin{bmatrix} P(\omega^0) \\ P(\omega^1) \\ P(\omega^2) \\ \vdots \\ P(\omega^{n-1}) \end{bmatrix}
p0p1p2⋮pn−1
=n1
111⋮11ω−1ω−2⋮ω−(n−1)1ω−2ω−4⋮ω−2(n−1)………⋱…1ω−(n−1)ω−2(n−1)⋮ω−(n−1)(n−1)
P(ω0)P(ω1)P(ω2)⋮P(ωn−1)
这个得到的矩阵和之前的DFT矩阵简直是一模一样的!!!实际上我们只需要把原来的 ω \omega ω换成 1 n ω − 1 \frac{1}{n}\omega^{-1} n1ω−1即可得到新的矩阵
对比二者:
蝴蝶结构:
https://www.youtube.com/watch?v=FaWSGmkboOs
印度老哥讲的视频,还是要对上面的
y [ j ] = y e [ j ] + ω j y o [ j ] y [ j + n 2 ] = y e [ j ] − ω j y o [ j ] \begin{align} y[j] &= y_e[j] + \omega^j y_o[j] \\ y\left[j + \frac{n}{2}\right] &= y_e[j] - \omega^j y_o[j] \end{align} y[j]y[j+2n]=ye[j]+ωjyo[j]=ye[j]−ωjyo[j]
深刻理解才能得到这个蝴蝶结构
蝴蝶结构从左到右的每一层对应的是递归从低到上的每一层
FFT代码:
import math
def FFT(P):
"""
快速傅里叶变换:系数形式 → 点值形式
P: 系数列表 [p0, p1, ..., pn-1],n必须是2的幂
返回:点值列表 [P(ω⁰), P(ω¹), ..., P(ωⁿ⁻¹)],其中ω是n次单位根
"""
n = len(P)
if n == 1:
return P # 递归基:长度为1时直接返回
# 计算n次单位根:ω = e^(2πi/n)
omega = complex(math.cos(2 * math.pi / n), math.sin(2 * math.pi / n))
# 分治:奇偶项分离
Pe = P[::2] # 偶下标系数
Po = P[1::2] # 奇下标系数
ye = FFT(Pe)
yo = FFT(Po)
# 蝶形运算合并结果
y = [0] * n
for j in range(n // 2):
y[j] = ye[j] + (omega ** j) * yo[j]
y[j + n//2] = ye[j] - (omega ** j) * yo[j]
return y
def IFFT(P):
"""
逆快速傅里叶变换:点值形式 → 系数形式
P: 点值列表 [P(ω⁰), P(ω¹), ..., P(ωⁿ⁻¹)]
返回:系数列表 [p0, p1, ..., pn-1]
"""
n = len(P)
if n == 1:
return P # 递归基:长度为1时直接返回
# 计算逆单位根:ω = (1/n) * e^(-2πi/n)
omega = complex(math.cos(2 * math.pi / n), -math.sin(2 * math.pi / n)) / n
# 分治:奇偶项分离(与FFT对称)
Pe = P[::2]
Po = P[1::2]
ye = IFFT(Pe)
yo = IFFT(Po)
# 蝶形运算合并结果(使用逆单位根)
y = [0] * n
for j in range(n // 2):
y[j] = ye[j] + (omega ** j) * yo[j]
y[j + n//2] = ye[j] - (omega ** j) * yo[j]
return y
NTT:
FFT的缺点:
上面我们已经对FFT有了一定的了解,但是FFT存在一些问题就是里面用到了大量的三角函数和幂函数,导致运算时候的精度会有所损失,所以我们引入了新的算法NTT,其基本思想与FFT一致,但是在精度问题上大大减低1.复数表示
2.浮点数表示
所以我们解决这些缺点自然是想到:
复数表示–>实数表示
浮点数表示–>整数表示
前置数论知识:
欧拉函数 ϕ ( n ) \phi(n) ϕ(n):
小于或等于n的正整数当中与n互质数的个数
费马小定理:
假设a为整数,p为质数,则 a p = a ( m o d p ) a^p=a\quad (mod \quad p) ap=a(modp),若a,p互素的话,则 a p − 1 = 1 ( m o d p ) a^{p-1}=1\quad (mod \quad p) ap−1=1(modp)
欧拉定理:
a ϕ ( n ) = 1 ( m o d n ) a^{\phi(n)}=1 \quad (mod\quad n) aϕ(n)=1(modn)
阶:
满足同余式 a n = 1 ( m o d m ) a^n=1 (mod \quad m) an=1(modm)的最小正整数n存在,则这个n称为a模m的阶,记作: δ m ( a ) \delta_m(a) δm(a)
假设 a=2, m=7
KaTeX parse error: {align*} can be used only in display mode.
因此当 ( a=2, m=7 ) 时阶数为3。(这里体现了一个==周期性!==即余数会以2,4,1循环)
原根:
设 m ∈ N ∗ , a ∈ Z m\in \mathbb{N^*},a\in \mathbb{Z} m∈N∗,a∈Z。如果 g c d ( a , m ) = 1 gcd(a,m)=1 gcd(a,m)=1,且 δ m ( a ) = ϕ ( m ) \delta_m(a)=\phi(m) δm(a)=ϕ(m),则称a为模m的原根。如下图中的紫色部分
图注: a b m o d 17 a^b mod 17 abmod17
NTT的核心思想
在有限域中用原根构造的单位根替代 FFT(快速傅里叶变换)中的单位复根,从而在整数模运算下实现多项式的高效卷积,避免复数运算带来的精度误差。
替代的原因有三:
本质是代数结构的 “同构替代”
FFT中: ω n k = e 2 π k i n \omega^k_n = e^{\frac{2\pi k i}{n}} ωnk=en2πki
NTT中: g n k = ( G ϕ ( n ) n ) k m o d p g_n^k = \left( G^{\frac{\phi(n)}{n}} \right)^k \mod p gnk=(Gnϕ(n))kmodp
其中G就是原根,是乘法群的生成元; g n g_n gn是 n 次单位根:n 阶子群的生成元。
FFT中,单位复根 ω n k = e 2 π i k n \omega_n^k = e^{\frac{2\pi i k}{n}} ωnk=en2πik满足周期性: ω n k + n = ω n k \omega_n^{k + n} = \omega_n^k ωnk+n=ωnk(周期为 n n n)。
NTT中,构造的有限域单位根 g n g_n gn满足 g n n ≡ 1 m o d p g_n^n \equiv 1 \mod p gnn≡1modp,因此 g n k + n ≡ g n k m o d p g_n^{k + n} \equiv g_n^k \mod p gnk+n≡gnkmodp,同样具有周期为 n n n的周期性。
周期性是FFT/NTT“分治迭代”的基础(每一层的计算模式可重复)。
FFT中,在一个周期内( k = 0 , 1 , … , n − 1 k = 0,1,\dots,n-1 k=0,1,…,n−1), ω n k \omega_n^k ωnk的每个值互不相同(否则变换会丢失唯一性)。
NTT中,由于 g n g_n gn的阶为 n n n(即 g n k ≡ 1 m o d p g_n^k \equiv 1 \mod p gnk≡1modp当且仅当 n ∣ k n \mid k n∣k),因此当 k = 0 , 1 , … , n − 1 k = 0,1,\dots,n-1 k=0,1,…,n−1时, g n k m o d p g_n^k \mod p gnkmodp也互不相同。
这一性质保证了“不同位置的系数在变换中能被唯一区分”,是变换可逆性的前提。
FFT中,单位复根满足 ω n k + n / 2 = − ω n k \omega_n^{k + n/2} = -\omega_n^k ωnk+n/2=−ωnk(因 e π i = − 1 e^{\pi i} = -1 eπi=−1),这是“蝶形运算”中奇偶分离、符号翻转的核心依据。
NTT中,由于 g n n ≡ 1 m o d p g_n^n \equiv 1 \mod p gnn≡1modp,故 ( g n n / 2 ) 2 ≡ 1 m o d p (g_n^{n/2})^2 \equiv 1 \mod p (gnn/2)2≡1modp。又因为 g n g_n gn的阶为 n n n( n / 2 < n n/2 < n n/2<n),所以 g n n / 2 ≢ 1 m o d p g_n^{n/2} \not\equiv 1 \mod p gnn/2≡1modp,因此 g n n / 2 ≡ − 1 m o d p g_n^{n/2} \equiv -1 \mod p gnn/2≡−1modp(有限域中, x 2 ≡ 1 m o d p x^2 \equiv 1 \mod p x2≡1modp的非平凡解为 x ≡ − 1 x \equiv -1 x≡−1)。
由此可得: g n k + n / 2 = g n k ⋅ g n n / 2 ≡ − g n k m o d p g_n^{k + n/2} = g_n^k \cdot g_n^{n/2} \equiv -g_n^k \mod p gnk+n/2=gnk⋅gnn/2≡−gnkmodp,与FFT的性质完全匹配,支持蝶形运算的符号逻辑。
具体例子( p = 17 , G = 3 , n = 8 p = 17, G = 3, n = 8 p=17,G=3,n=8 时):
( 3 17 − 1 8 ) k m o d 17 \left( 3^{\frac{17 - 1}{8}} \right)^k \mod 17 (3817−1)kmod17
NTT变换矩阵形式
[ P ( g 0 ) P ( g 1 ) P ( g 2 ) ⋮ P ( g n − 1 ) ] = [ 1 1 1 … 1 1 g g 2 … g n − 1 1 g 2 g 4 … g 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 g n − 1 g 2 ( n − 1 ) … g ( n − 1 ) ( n − 1 ) ] [ p 0 p 1 p 2 ⋮ p n − 1 ] m o d p \begin{bmatrix} P(g^0) \\ P(g^1) \\ P(g^2) \\ \vdots \\ P(g^{n-1}) \end{bmatrix} =\begin{bmatrix} 1 & 1 & 1 & \dots & 1 \\ 1 & g & g^2 & \dots & g^{n-1} \\ 1 & g^2 & g^4 & \dots & g^{2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & g^{n-1} & g^{2(n-1)} & \dots & g^{(n-1)(n-1)} \end{bmatrix} \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} \mod p
P(g0)P(g1)P(g2)⋮P(gn−1)
=
111⋮11gg2⋮gn−11g2g4⋮g2(n−1)………⋱…1gn−1g2(n−1)⋮g(n−1)(n−1)
p0p1p2⋮pn−1
modp
其中:
- P ( g k ) P(g^k) P(gk) 表示多项式 P ( x ) = p 0 + p 1 x + p 2 x 2 + ⋯ + p n − 1 x n − 1 P(x) = p_0 + p_1 x + p_2 x^2 + \dots + p_{n-1} x^{n-1} P(x)=p0+p1x+p2x2+⋯+pn−1xn−1 在点 g k g^k gk 处的值(模 p p p)。
- 矩阵中的每个元素 g i j g^{ij} gij 都计算模 p p p。
- 这个矩阵是一个范德蒙德矩阵(Vandermonde matrix),基于原根 g g g 的幂次。
逆NTT(INTT)变换矩阵形式
逆变换使用原根的逆元 g − 1 m o d p g^{-1} \mod p g−1modp 和一个缩放因子 n − 1 m o d p n^{-1} \mod p n−1modp(其中 n − 1 n^{-1} n−1 是 n n n 模 p p p 的乘法逆元)。INTT矩阵形式为:
[ p 0 p 1 p 2 ⋮ p n − 1 ] = n − 1 [ 1 1 1 … 1 1 g − 1 g − 2 … g − ( n − 1 ) 1 g − 2 g − 4 … g − 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 g − ( n − 1 ) g − 2 ( n − 1 ) … g − ( n − 1 ) ( n − 1 ) ] [ P ( g 0 ) P ( g 1 ) P ( g 2 ) ⋮ P ( g n − 1 ) ] m o d p \begin{bmatrix} p_0 \\ p_1 \\ p_2 \\ \vdots \\ p_{n-1} \end{bmatrix} = n^{-1} \begin{bmatrix} 1 & 1 & 1 & \dots & 1 \\ 1 & g^{-1} & g^{-2} & \dots & g^{-(n-1)} \\ 1 & g^{-2} & g^{-4} & \dots & g^{-2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & g^{-(n-1)} & g^{-2(n-1)} & \dots & g^{-(n-1)(n-1)} \end{bmatrix} \begin{bmatrix} P(g^0) \\ P(g^1) \\ P(g^2) \\ \vdots \\ P(g^{n-1}) \end{bmatrix} \mod p p0p1p2⋮pn−1 =n−1 111⋮11g−1g−2⋮g−(n−1)1g−2g−4⋮g−2(n−1)………⋱…1g−(n−1)g−2(n−1)⋮g−(n−1)(n−1) P(g0)P(g1)P(g2)⋮P(gn−1) modp
代码:
def NTT(P, g, p):
"""
数论变换:将系数表示的多项式转换为点值表示
P: 系数列表 [p0, p1, ..., pn-1],n必须是2的幂
g: 模p的原根(需满足n | p-1,即p ≡ 1 mod n)
p: 模数(大质数,保证存在n次单位根)
返回:点值列表 [P(ω^0), P(ω^1), ..., P(ω^{n-1})],其中ω是n次单位根
"""
n = len(P)
if n == 1:
return P # 递归基:长度为1时直接返回
# 计算n次单位根:ω = g^((p-1)/n) mod p
omega = pow_mod(g, (p - 1) // n, p)
# 分治:奇偶分离
Pe = P[::2] # 偶下标系数
Po = P[1::2] # 奇下标系数
ye = NTT(Pe, g, p)
yo = NTT(Po, g, p)
# 合并结果(蝶形运算)
y = [0] * n
for j in range(n // 2):
omega_j = pow_mod(omega, j, p) # ω^j mod p
y[j] = (ye[j] + omega_j * yo[j]) % p
y[j + n//2] = (ye[j] - omega_j * yo[j]) % p
return y
def INTT(P, g, p):
"""
逆数论变换:将点值表示转换为系数表示
P: 点值列表 [P(ω^0), P(ω^1), ..., P(ω^{n-1})]
g: 模p的原根(与NTT一致)
p: 模数(与NTT一致)
返回:系数列表 [p0, p1, ..., pn-1]
"""
n = len(P)
if n == 1:
return P # 递归基:长度为1时直接返回
# 计算逆单位根:ω^(-1) = ω^(n-1) mod p(因ω^n ≡ 1)
omega = pow_mod(g, (p - 1) // n, p)
omega_inv = pow_mod(omega, n - 1, p) # ω的逆元
# 分治:奇偶分离(与NTT对称)
Pe = P[::2]
Po = P[1::2]
ye = INTT(Pe, g, p)
yo = INTT(Po, g, p)
# 合并结果(蝶形运算,使用逆单位根)
y = [0] * n
for j in range(n // 2):
omega_j = pow_mod(omega_inv, j, p) # (ω^(-1))^j mod p
y[j] = (ye[j] + omega_j * yo[j]) % p
y[j + n//2] = (ye[j] - omega_j * yo[j]) % p
# 乘以n的模逆元(类似FFT中的1/n)
n_inv = pow_mod(n, p - 2, p) # 费马小定理:n^(p-2) ≡ n^(-1) mod p(p是质数)
for j in range(n):
y[j] = (y[j] * n_inv) % p
return y