• FFT学习笔记


    FFT学习笔记

    前置知识:

    前言:

    • 假设我们现在有一个(n-1)次多项式,通项为(sum_{i=0}^na_i*x^i)
    • 比如:(A(x)=x^2+2x+1,B(x)=3x^3+2x^2+5x+1)
    • 我们想将这两个多项式相乘,采用朴素算法只能老老实实的将每一项对应相乘再相加,时间复杂度达到了(O(nm))
    • 当然可以转换一种思路,将(n)个不同的(x)带入这个(n-1)次多项式,会取得不同的(y)(n个)((x,y))唯一确定了这个多项式
    • 这里我们可以发现,两个用点值表示的多项式相乘,我们如果(O(n))的时间枚举一下(x_i),可以得到:(C(x_i)=A(x_i)*B(x_i))
    • 这样可以在(O(n))的时间内完成多项式乘法。(因为(n)个点可以唯一的确定一个(n-1)次多项式)
    • 但是可惜的是,取(x_i)(A(x_i)),这样的复杂度是(O(n))的,如果取(n)(x),那整体复杂度就是(O(n^2))的,和朴素枚举差不多。
    • 但如果可以快速的将多项式转换为点值表示(以及其反向操作),之后就可以快速地完成多项式乘法。
    • 快速傅里叶变换((FFT))是一种能在(O(nlogn))的时间内将一个多项式转换成点值表示的算法。
    • 所以算法流程是这样的:
      • 将两个式子从系数用(O(nlogn))时间表示转化为点值表示。
      • 然后以(O(n))的时间完成两个式子相乘。
      • 最后以(O(nlogn))时间将点值表示再转换为多项式系数表示。

    几个容易混淆的缩写:

    • (DFT:)离散傅里叶变换,(O(n^2))(FFT)的朴素版。
    • (FFT:)快速傅里叶变换,(O(nlogn)).
    • (FNTT/NTT:FFT)的升级版。
    • (FWT:)快速沃尔什变换。

    单位根:

    • 下文中,默认(n)(2)的正整数次幂。

    • 在二维平面,原点为圆心,(1)为半径做圆,所得圆为单位圆。

    • 那么我们先把圆切成(n)等份,然后对应(n)个向量。

    • 画个图看看吧:

    • 这样就把一个单位圆分为了(8)等份。

    • 根据上一篇博文的知识,其实这就是(z=e^{i})开了(n)次方。

    • (sqrt[n]{z}=e^{ifrac{2kpi}{n}}=cosfrac{2kpi}{n}+isinfrac{2kpi}{n})

    • 我们设(w_n^k=e^{ifrac{2kpi}{n}}).

    • (w_n^0=e^{0}=cos0+isin0,w_n^1=e^{frac{i2pi}{n}}=cosfrac{2pi}{n}+isinfrac{2pi}{n},...,)

    • (w_n^{n-1}=e^{i2(n-1)pi}=cosfrac{2(n-1)pi}{n}+isinfrac{2(n-1)pi}{n}).

    • 也就对应的是圆上的几个点。

    单位根的性质:

    • (1:w_{2n}^{2k}=w_n^k).
      • 证明:(w_{2n}^{2k}=e^{ifrac{2(2k)pi}{2n}}=e^{ifrac{2kpi}{n}}=w_n^k).证毕。
    • (2:w_n^{k+frac{n}{2}}=-w_n^k).
      • 就相当于一个点(+frac{n}{2})变成对面那个点,也就是这个点的反方向。
      • 或者也可以用欧拉公式展开一下用三角函数变换证明。(我太懒了)

    快速傅里叶变换:

    • 我们知道一个(n-1)次多项式可以被(n)个点确定。
    • 假设(A(x)=(a_0,a_1,...,a_{n-1}))
    • 那么有(A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1})
    • 接下来按照下标奇偶性分类。
    • (A(x)=(a_0+a_2x^2+a_4x^4+...+a_{n-2}x^{n-2})+(a_1x+a_3x^3+...+a_{n-1}x^{n-1})).
      • (A_1(x)=a_0+a_2x+a_4x^2+...+a_{n-2}x^{frac{n}{2}-1}).
      • (A_2(x)=a_1+a_3x+a_5x^2+...+a_{n-1}x^{frac{n}{2}-1}).
    • 那么有:
      • (A(x)=A_1(x^2)+xA_2(x^2)).
    • (w_n^k(k<frac{n}{2}))代入得:
      • (A(w_n^k)=A_1(w_n^{2k})+w_n^kA_2(w_n^{2k})).
    • 同理将(w_n^{k+frac{n}{2}})代入得:
      • (A(w_n^{k+frac{n}{2}})=A_1(w_n^{2k+n})+w_n^{k+frac{n}{2}}A_2(w_n^{2k+n})).
      • (=A_1(w_n^{2k}*w_n^n)-w_n^kA_2(w_n^{2k}*w_n^n)).
      • (=A_1(w_n^{2k})-w_n^kA_2(w_n^{2k})).
    • 可以发现,这两个式子只有一个常数项不同。
    • 也就是在枚举第一个式子的时候,可以(O(1))的得到第二个式子的值。
    • 而我们又知第一个式子中(kin[0,frac{n}{2}-1],k+frac{n}{2}in[frac{n}{2},n-1]).
    • 所以也就是将原问题缩小了一半。
    • 满足分治性质,递归后合并求解即可。
    • 于是就做到了在(O(nlogn))时间完成了多项式的系数表示到点值表示。

    快速傅里叶逆变换:

    • 接下来我们需要将点值表示法还原回系数表示,这个过程为傅里叶逆变换。

    • 我们先假设((y_0,y_1,...,y_{n-1}))为多项式(A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1})(A(x))的离散傅里叶变换。

    • 再设(B(x)=y_0+y_1x+y_2x^2+...+y_{n-1}x^{n-1}),现在我们代入单位根的倒数(w_n^0,w_n^{-1},w_n^{-2},...,w_n^{-(n-1)})可以得到一个新的离散傅里叶变换((z_0,z_1,...,z_{n-1}))

    • 可以得到

      • (z_k=y_0+y_1(w_n^{-1})^1+y_2(w_n^{-2})^2+...+y_{n-1}(w_n^{-(n-1)})^{n-1}=sum_{i=0}^{n-1}y_i(w_n^{-k})^i).
      • (=sum_{i=0}^{n-1}(sum_{j=0}^{n-1}a_j(w_n^i)^j)(w_n^{-k})^i).
      • (=sum_{j=0}^{n-1}a_j(sum_{i=0}^{n-1}(w_n^{j-k})^i)).
    • 可以看一下(sum_{j=0}^{n-1}(w_n^{j-k})^i)。当(j-k=0)时,它等于(n);其余的时候,可通过等比数列求和得知:

      • (frac{(w_n^{j-k})^n-1}{w_n^{j-k}-1}=frac{(w_n^n)^{j-k}-1}{w_n^{j-k}-1}=frac{1^{j-k}-1}{w_n^{j-k}-1}=0).
    • 那么就可以得知:(z_k=na_k).

      • (a_i=frac{z_i}{n}).
    • 所以我们可以得到一个结论。多项式(A(x))的离散傅里叶变换的另一个多项式(B(x))的系数,取单位根的倒数(w_n^0,w_n^{-1},...,x_n^{-(n-1)})作为(x)代入(B(x)),得到的每个数再除以(n),得到的是(A(x))的各项系数。这就实现了傅里叶逆变换。

    • 至此(FFT)的理论基础已结束。

    代码实现:

    • 根据上述分析可得,一个序列需要划分成两部分后分治递归即可。

    • 但是可以再度优化。

    • 可以发现原序列和后序列分组其实是按照原序列下标的二进制翻转。

    • 因此按照下标进行奇偶性分类是没有必要的,于是我们可以免去递归的过程。

    • 对于二进制翻转要怎么做呢?

    • 这是一个(trick:)蝴蝶定理。

    • 假设即将反转的数字为(i),在(i)之前的数字都已经翻转好了。

    • 那么对于(i)来说就是右移后翻转后再右移,如果是奇数为在最高位补(1,)也就是(r[i] = (r[i>>1]>>1)|((i&1)<<(l-1)))

    • 弄不明白可以手摸几个例子。

    • 之后直接枚举子区间后向上合并即可。

    • 枚举分割的中点的时间复杂度为(O(logn)),合并复杂度为(O(n)),总时间复杂度为(O(nlogn))

    • #include<bits/stdc++.h>
      using namespace std;
      const int maxn = 1e7 + 10;
      const double PI = acos(-1.0);
      int n, m, limit, l, r[maxn];
      inline int read() {
          char c = getchar(); int x = 0, f = 1;
          while (c < '0' || c > '9') {if (c == '-')f = -1; c = getchar();}
          while (c >= '0' && c <= '9') {x = x * 10 + c - '0'; c = getchar();}
          return x * f;
      }
      
      //手写复数类
      struct Complex
      {
          double x, y;
          Complex (double xx=0, double yy=0){
              x = xx; y = yy;
          }
          Complex operator + (const Complex b) const{
              return Complex(x+b.x, y+b.y);
          }
          Complex operator - (const Complex b) const{
              return Complex(x-b.x, y-b.y);
          }
          Complex operator * (const Complex b) const{
              return Complex(x*b.x-y*b.y, x*b.y+y*b.x);
          }
      }a[maxn], b[maxn];
      
      void fft(Complex c[], int type)
      {
          for(int i = 0; i < limit; i++)
              if(i < r[i]) swap(c[i], c[r[i]]);
          //枚举待合并区间的中点的长度
          for(int mid = 1; mid < limit; mid <<= 1)
          {
              //设立单位根
              Complex wn(cos(PI/mid), type*sin(PI/mid));
              //R是区间的长度,j表示当前已经到哪个位置了,而且是左端点
              for(int R = mid<<1, j = 0; j < limit; j += R)
              {
                  Complex w(1, 0); //初始幂次
                  for(int k = 0; k < mid; k++, w = w*wn) //枚举左半部分
                  {
                      Complex x = c[j+k], y = w*c[j+mid+k];
                      c[j+k] = x+y;
                      c[j+mid+k] = x-y; //右半部分减去即可
                  }
              }
          }
      }
      
      int main()
      {
          //1e6的读入,需要写快读
          n = read(), m = read();
          for(int i = 0; i <= n; i++) a[i].x = read();
          for(int i = 0; i <= m; i++) b[i].x = read();
          limit = 1;
          //要求limit一定是2的整次幂
          while(limit <= n+m) limit <<= 1, l++;
          
          for(int i = 0; i < limit; i++)
              r[i] = (r[i>>1]>>1)|((i&1)<<(l-1));
      
          //对a序列和b序列分别处理
          fft(a, 1); fft(b, 1);
          for(int i = 0; i <= limit; i++)
              a[i] = a[i]*b[i];
          fft(a, -1);
          for(int i = 0; i <= n+m; i++) //四舍五入
              printf("%d ", (int)(a[i].x/limit+0.5));
          return 0;
      }
      
      
  • 相关阅读:
    CSS3box-shadow属性的使用
    IE7下z-index失效问题
    CSS3的writing-mode属性
    eclipse Java EE安装和web项目的创建
    用JS去掉前后空格或中间空格大全
    webstorm自带debugger服务器
    用meta标签让网页用360打开时默认为极速模式
    移动端可拖动导航菜单小demo
    DCL并非单例模式专用
    领域专用语言
  • 原文地址:https://www.cnblogs.com/zxytxdy/p/12078627.html
Copyright © 2020-2023  润新知