FFT Fast Fourier Transform Learning Notes

FFT Fast Fourier Transform Learning Notes

Basic Information

Uses: polynomial multiplication

Time complexity: \ (O (nlogn) \) (slightly larger constant)

Algorithmic process

The basic idea

\(H(x) = G(x) \times F(x)\)

Expression directly from the conversion coefficient is a coefficient comparison downright expression, so consider first \ (F (x), \ G (x) \) into an expression point value, then \ (O (n) \) is determined \ (H (x) \) of the point value of the expression, and from (H (x) \) \ point value expressions into \ (H (x) \) coefficient expression.

Wherein the conversion from the expression for the coefficient of the expression process set point is called \ (the DFT \) , also known as evaluation operators .

From the expressions into expressions coefficient set point process is called \ (the IDFT \) , also known as interpolation .

Evaluation operators

Consider evaluation calculation process, set \ (F (x), G (x) \) are \ (n-\) times, \ (m \) polynomial of degree, the \ (H (x) \) is \ (n + m \) polynomial of degree,

So we need to find \ (F (x), G (x) \) in \ (m-1 \ n + ) values at different points, in order to ensure that the final determined \ (H (x) \) uniqueness, (can be compared to find the conditions required Resolution function).

If direct hard count, complexity will reach \ (O (the n-^ 2) \) , so we need the help called unit root of magical things.


plural

Before the introduction of unit root, we must first explain the complex .

First, we define a number \ (I \) , so \ (I ^ 2 = -1 \) (all hereinafter \ (I \) have expressed this thing).

The form \ (a + bi \) of the complex number is called, where \ (A, B \ in \ R & lt mathbb {} \) .

Complex and real numbers, there are also four operations (in fact, is analogous to the operation polynomial).

6,187,655 \ (X = A Tasu Bi, Y = C Tasu Di \) , Provisions

  1. $ x+y = (a+c)+(b+d)i $
  2. $ x-y = (a-c)+(b-d)i $
  3. $ X \ times y = (ac -bd) + (ad + cb) i $ ( the \ (x, y \) as polynomial multiplication can be opened).
  4. $ \ Frac {x} {y} = \ frac {a + bi} {c + di} = \ frac {(a + bi) (c-di)} {(c + di) (c-di)} = \ frac {(ac + bd) + (ad + cb) i} {c ^ 2 + d ^ 2} $ (similar to the irrational numbers in the denominator of the calculation process).

Next, we introduce one called "complex plane" thing.

So long

img

A number-axis point uniquely represents a real number similar to a point on the complex plane can uniquely represent a complex number.

Wherein, \ (X \) number is a real number axis \ ((Real \ Axis) \) , \ (Y \) number is an imaginary number axis \ ((Imaginary \ Axis) \) .

We set up a complex argument for a plurality of points in the complex plane corresponding to the vector and \ (X \) angle counterclockwise shaft,

A plurality of module length for the die corresponding to a plurality of length vectors.

We will get a magical nature:

Set \ (x, y, z \ ) are plural, and the \ (X \ Y = Z Times \) , then \ (Z \) web angle is equal to \ (x, y \) of the argument are added , \ (Z \) of die length is equal to \ (x, y \) die length multiplied.

FIG follows ( FIG source )

img

The argument can be summed trigonometric proof, mold can grow by multiplying the coordinates directly into the count is like. (Written proof is too much trouble, forgive my limited time)

Unit Root

With the above basis, we can come to know the unit root up.

Definition: If a plurality of \ (n-X ^ =. 1, \ (n-\ in \ mathbb + {N}) \) , called \ (X \) is the \ (n-\) th Root.

Consider the nature of the complex multiplication can be found, \ (X \) of die length is bound to \ (1 \) , (greater than \ (1 \) , then will be more by the larger, less than \ (1 \) , then it will be more take smaller),

And \ (X \) web angle \ (\ FRAC {2 \ {n-PI K}}, \ (K \ in [0, n-)) \) .

That means, \ (the X-\) must be in the complex plane unit circle on, and the unit circle \ (n \) decile.

3 th Root

为了便于称呼, 我们用 \(\omega_n\) 来表示 \(n\) 单位根, 并从 \(1\) 开始将他们逐个编上号, \(\omega_n^0 = 1\).

接下来, 我们介绍一些单位根的性质 (原谅我真的没时间....)

  1. \(\omega_n^k = (\omega_n^1)^k\)
  2. $\omega_n^0 \omega_n^1 \dots \omega_n^{n-1} $ 互不相等.
  3. \(\omega_n^{k+\frac{n}{2}} = -\omega_n^k\) (\(n\) 为偶数)
  4. \(\omega_{2n}^{2k} = \omega_n^k\)
  5. \(\sum_{k=0}^{n-1} \omega_n^k = 0\) (带入等差数列求和公式即可)

好了, 复数和单位根就介绍到这里, 还记得我们原来要干什么吗?

我们想把 \(F(x)\)系数表达式 转化为 点值表达式 .

求点值表达式, 就需要选择 \(n+m-1\) 个自变量 \(x\) 带入求值.

通常情况下, 这个操作的复杂度是 \(O(n^2)\) 级别的, 但我们的傅里叶大大发现, 把单位根带入求值, 会有神奇的效果.

为了方便描述, 我们这里把 \(n\) 重定义为大于 \(n+m-1\) 的第一个 \(2\) 的正数次方, 并把 \(F(x)\) 重定义为 \(n-1\) 次多项式, 后面多出的系数默认为 \(0\).

\(\omega_n^k\) ($ k \in [0,\frac{n}{2})$)带入 \(F(x)\), 得到
\[ F(\omega_n^k) = f[0]\omega_n^0 + f[1]\omega_n^1 + \dots + f[n-1]\omega_n^{n-1} \]
尝试使用分值的思想, 把奇偶次项分开, 得到
\[ F(\omega_n^k) = f[0]\omega_n^0 + f[2]\omega_n^2 + \dots + f[n-2]\omega_n^{n-2} + f[1]\omega_n^1 + f[3]\omega_n^3 + \dots + f[n-1]\omega_n^{n-1} \]
两部分似乎有相似之处,

\(G1(x) = f[0]x^0 + f[2]x^1 + f[n-2]x^{\frac{n}{2}-1}\)

\(G2(x) = f[1]x^0 + f[1]x^1 + f[n-1]x^{\frac{n}{2}-1}\)


\[ \begin{aligned} F(\omega_n^k) & = G1(\omega_n^{2k}) + \omega_n^kG2(\omega_n^{2k}) \\ & = G1(\omega_{\frac{n}{2}}^{k}) + \omega_n^kG2(\omega_{\frac{n}{2}}^{k}) \end{aligned} \]
若再把 \(\omega_n^{k+\frac{n}{2}}\) 带入 \(F(x)\), 由于 \(\omega_n^{k+\frac{n}{2}} = -\omega_n^k\), 所以他们的偶次项是相同的, 而奇次项是相反的.

也就是
\[ \begin{aligned} F(\omega_n^{k+\frac{n}{2}}) & = G1(\omega_n^{2k + n}) + \omega_n^{k+\frac{n}{2}}G2(\omega_n^{2k + n}) \\ & = G1(\omega_{\frac{n}{2}}^{k}) - \omega_n^kG2(\omega_{\frac{n}{2}}^{k}) \end{aligned} \]

发现 \(F(\omega_n^k)\)\(F(\omega_n^k)\) 化简后得到的式子只有一个符号的差别, 那么意味着, 我们只需算出当 \(k \in [0,\frac{n}{2})\) 时的
\[ G1(\omega_{\frac{n}{2}}^{k}) \]

\[ G2(\omega_{\frac{n}{2}}^{k}) \]
这两个式子, 就可以算出 \(\omega_n^0\)\(\omega_n^{n-1}\) 的所有点值.

而上面那两个式子显然 (应该显然吧...) 是可以递归处理的, 那么每次就减少计算一半的点, 时间复杂度就降低到了 \(O(n\log n)\).

放个代码

void trans(cn *f,int len,bool id){
  if(len==1) return;
  cn *g1=f,*g2=f+len/2;   // 直接在 f 数组的地址上修改, 防止使用内存过多
  for(int i=0;i<len;i++) tmp[i]=f[i];  // 由于是之间在 f 数组的地址上修改, 所以要备份 
  for(int i=0;2*i<len;i++){ g1[i]=tmp[i<<1]; g2[i]=tmp[i<<1|1]; }
  trans(g1,len/2,id);   // 递归处理
  trans(g2,len/2,id);
  cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)},wi=(cn){1,0};
  if(id) w1.b*=-1;
  for(int i=0;2*i<len;i++){
    tmp[i]=g1[i]+wi*g2[i];          // 上面的两个式子
    tmp[i+len/2]=g1[i]-wi*g2[i];
    wi=wi*w1;   // 处理出每个单位根
  }
  for(int i=0;i<len;i++) f[i]=tmp[i];
}

那么求值运算, 也就是 \(DFT\) 就大功告成了.


差值运算

我们先用矩阵乘法来表示一下求点值的过程.

设 矩阵\(A\) 为要带入的 \(n\)自变量以及它们的 \(0 \sim n\) 次方,

矩阵 \(B\)\(F(x)\)系数,

矩阵 \(C\) 为自变量对应的 \(n\)点值.

则有
\[ AB = C \]

image-20191229195524817

现在我们知道了 \(A\), 知道了 \(C\), 要求 \(B\), 那一般思路就是把 \(A\) 除过去, 即
\[ B = CA^{-1} \]
其中 \(A^{-1}\)\(A\)逆矩阵, 它们的乘积为单位矩阵.

经过一系列复杂的运算后, 发现 \(A^{-1}\) 是长这样的, (可以尝试自己手推一下, 需要用到上面单位根的第 4 个性质)

image-20191229195821568

是不是很眼熟,

没错, 实际上就是把 \(A\)\(\omega_n^k\) 全都换成了 \(\omega_n^{-k}\), 并在前面加了个系数.

\(CA^{-1}\) 究竟要怎么算呢?

是不是完全没有头绪? (还是只有我一个人是这样)

答案是, 把 \(A^{-1}\) 看做 \(A\), 把 \(C\) 看做 \(B\), 把 \(B\) 看做 \(C\) , 再进行一遍 \(DFT\) 就行了. (说人话).

就是 把点值看做一个新函数的系数, 然后把 \(\omega_n^0 \sim \omega_n^{-(n-1)}\) 带入这个新函数, 求值, 得到的点值再乘上一个 \(\frac{1}{n}\) 就得到了\(H(x)\), 也就是 \(F(x) \times G(x)\) 的系数.


ok, 到此为止, 我们搞定了 \(DFT\)\(IDFT\) ,\(FFT\) 的流程也就到这里了,

放代码.

#include<bits/stdc++.h>
#define _USE_MATH_DEFINES
using namespace std;
const int N=3e6+7; 
const double Pi=M_PI;
struct cn{
  double a,b;
  cn operator + (const cn &x) const{
    return (cn){x.a+a,x.b+b};
  }
  cn operator - (const cn &x) const{
    return (cn){a-x.a,b-x.b};
  }
  cn operator * (const cn &x) const{
    return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
  }
  cn operator *= (const cn &x) const{
    return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
  }
};
int n,m;
cn f[N],g[N],tmp[N];
void trans(cn *f,int len,bool id){
  if(len==1) return;
  cn *g1=f,*g2=f+len/2;   // 直接在 f 数组的地址上修改, 防止使用内存过多
  for(int i=0;i<len;i++) tmp[i]=f[i];  // 由于是之间在 f 数组的地址上修改, 所以要备份 
  for(int i=0;2*i<len;i++){ g1[i]=tmp[i<<1]; g2[i]=tmp[i<<1|1]; }
  trans(g1,len/2,id);   // 递归处理
  trans(g2,len/2,id);
  cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)},wi=(cn){1,0};
  if(id) w1.b*=-1;
  for(int i=0;2*i<len;i++){
    tmp[i]=g1[i]+wi*g2[i];          // 上面的两个式子
    tmp[i+len/2]=g1[i]-wi*g2[i];
    wi=wi*w1;   // 处理出每个单位根
  }
  for(int i=0;i<len;i++) f[i]=tmp[i];
}
int main(){
  //  freopen("FFT.in","r",stdin);
  cin>>n>>m;
  for(int i=0;i<=n;i++) scanf("%lf",&f[i].a);
  for(int i=0;i<=m;i++) scanf("%lf",&g[i].a);
  int t=1;
  while(t<=n+m) t<<=1;  
  trans(f,t,0);
  trans(g,t,0);
  for(int i=0;i<t;i++) f[i]=f[i]*g[i];
  trans(f,t,1);
  for(int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].a/t+0.49));   //+0.49 减小因精度产生的误差 (我也不知道为什么这样就可减小误差...)
  return 0;
}

但是, 当你把这份代码交上去后, 会发现只有 77pts, 后面两点会 TLE.

这是因为复数运算的常数本身就比较大, 再加上递归带来的常数, 你不T谁T.

所以, 继续下一个内容.

FFT的优化

复数运算带来的常数是优化不了了, 毕竟 \(FFT\) 的关键步骤 ---- 分治 要依靠它才能进行.

(当然, 有人用其他更优的东西把它替代了, 不过这属于下一个内容 ---- \(NTT\) )

那我们就考虑如何优化递归带来的常数吧.

我们发现, 递归的下传过程并没有进行什么操作, 在上传过程中才处理出了点值.

那我们可以这样理解 : 递归的下传过程就是为了寻找每个数的对应位置.

那么, 这个对应位置是否存在某种规律, 能让我们免去递归的过程, 直接把它们放在应该放的位置?

经过前人的不懈努力和细心观察发现, 每个数最终的位置是该数的 二进制翻转

比如, 当 \(n = 8\) 的时候.

0   1   2   3   4   5   6   7   
0   2   4   6 | 1   3   5   7
0   4 | 2   6 | 1   5 | 3   7
0 | 4 | 2 | 6 | 1 | 5 | 3 | 7

化为二进制就是

000 001 010 011 100 101 110 111

000 100 010 110 001 101 011 111

是不是非常神奇

然后我们可以用一个类似递归的过程来处理他们的位置

for(int i=0;i<n;i++)
    num[i]=(num[i>>1]>>1])|((i&1) ?n>>1 :0)

可以这样理解,

假设你有一个数 \(x\), 它的二进制为

xxxxxxxxxx

把它拆成这两部分

xxxxxxxxx | x

前半部分的翻转, 就相当于 \(x>>1\) 的翻转再左移一位. (可以自己模拟一下)

然后再根据最后一位是 \(0\)\(1\) , 在前面补上相应的一位.

ok, 这样, 我们就避免了递归带来的常数.

还有一个小地方

for(int i=0;2*i<len;i++){
    tmp[i]=g1[i]+wi*g2[i];          // 上面的两个式子
    tmp[i+len/2]=g1[i]-wi*g2[i];
    wi=wi*w1;   // 处理出每个单位根
  }

我们可以把它改成

for(int i=0;2*i<len;i++){
    cn tmp=wi*g2[i];
    tmp[i]=g1[i]+tmp;           // 上面的两个式子
    tmp[i+len/2]=g1[i]-tmp;
    wi=wi*w1;   // 处理出每个单位根
  }

减少了一下复数的运算量.

最终代码 【模板】多项式乘法(FFT)

#include<bits/stdc++.h>
#define _USE_MATH_DEFINES
using namespace std;
const int N=3e6+7; 
const double Pi=M_PI;
struct cn{
  double a,b;
  cn operator + (const cn &x) const{
    return (cn){x.a+a,x.b+b};
  }
  cn operator - (const cn &x) const{
    return (cn){a-x.a,b-x.b};
  }
  cn operator * (const cn &x) const{
    return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
  }
};
int n,m,t=1,num[N];
cn f[N],g[N],tmp[N];
void trans(cn *f,int id){
  for(int i=0;i<t;i++)
    if(i<num[i]) swap(f[i],f[num[i]]);
  for(int len=2;len<=t;len<<=1){
    int gap=len>>1;
    cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)*id};
    for(int i=0;i<t;i+=len){
      cn wj=(cn){1,0};
      for(int j=i;j<i+gap;j++){
        cn tt=wj*f[j+gap];
        f[j+gap]=f[j]-tt;   // 这里需要注意一下赋值的顺序
        f[j]=f[j]+tt;
        wj=wj*w1;
      }
    }
  }
}
int main(){
  //freopen("FFT.in","r",stdin);
  //freopen("x.out","w",stdout);
  cin>>n>>m;
  for(int i=0;i<=n;i++) scanf("%lf",&f[i].a);
  for(int i=0;i<=m;i++) scanf("%lf",&g[i].a);
  while(t<=n+m) t<<=1;   // 保证 t > n+m
  for(int i=1;i<t;i++) num[i]=(num[i>>1]>>1)|((i&1)?t>>1:0);
  trans(f,1);
  trans(g,1);  
  for(int i=0;i<t;i++) f[i]=f[i]*g[i];
  trans(f,-1);
  for(int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].a/t+0.49));
  return 0;
}

推荐题目

[ZJOI2014]力

下面三道是 \(NTT\) 的题.

[AH2017/HNOI2017]礼物

[SDOI2015]序列统计

幼儿园篮球题

参考资料

Fourier Transform (FFT) study notes by command_block

Oh, and one more thing,

Typora nice with

Guess you like

Origin www.cnblogs.com/brucew-07/p/12116397.html