zoukankan      html  css  js  c++  java
  • 再谈 FTT 与 NTT

    前言

    扯淡

    之间写了一篇比较扯的博客 (FFT/NTT),其实里面很多东西解释的有点复杂,基于本人对 (FFT) 更深入理解的情况下,再次写下这一篇博客。

    此篇博客出于两个目的,一是为了巩固记忆,二是希望对 (FFT/NTT) 有初步了解的同学能更好理解算法。

    阅读本文的一些要求

    1、了解卷积(起码是多项式乘法)。

    2、 有一定的数学基础。

    3、 有较好的代码实现能力和思维能力。

    • 如果不满足第 (1)(2) 点,可以考虑阅读我上文所给出的博文;至于第三点,自己提升吧……

    再探FFT

    step1:优化过程

    给定多项式 (A(x),B(x)),不妨设 (displaystyle A=sum_{i=0}^n a_ix^i)(displaystyle B=sum_{i=0}^n b_ix^i),求 (A*B) 的每项系数。

    此问题暴力算法为 (O(n^2)) 的复杂度,正常水平的同学能够很轻松的实现。

    但是面对数据较大的情况,显然暴力算法是无法满足需求的,应当考虑卷积的过程优化:

    我们知道平面直角坐标系上 (n) 个点能恰好表示一个 (n) 次函数。显然 (n) 次函数的表达式种有 (n) 个未知数,则 (n) 个点创造了 (n) 个一元 (n) 次方程,这样便可以求出整个 (n) 次函数。因为上面的式子其实也是可以看做函数的(只求系数,也就是对应的未知数),所以我们考虑把用点来表示上面的多项式。

    定义多项式(F(x))

    我们知道,(1)个n元一次方程最少需要(n)个式子求解,而我们把系数表达(F(x))抽象为一个函数(F(x)),那么这个函数至少需要(n+1)个点确定下来,那么我们任取(n+1)个不同的数构成集合(U={k_1,k_2,…,k_{n+1}}),对(F(x))分别求值得到一个新的集合,那么将两个集合合并成点集,有:

    [F^{d}(x)={(k_1,F(k_1)),k_2,F(k_2),…,(k_{n+1},F(n+1))} ]

    (F^{d}(x)) 即为多项式 (F(x))点值表达式
    (引用于我之前写的博客)

    那么我们对 (A(x),B(x)) 分别求出其对应的点值表达,则 (A*B iff A^{d}(x)*B^{d}(x))

    进一步的可以简化计算,也就是我们对 (A,B) 取相同的“横坐标”,即集合 ({k_1,k_2,…,k_{2n+1}}),则可以得到:

    [A*B iff A^{d}(x)*B^{d}(x)={(k_1,A(k_1)*b(k_1)),k_2,A(k_2)*B(k_2),…,(k_{2n+1},A(k_{2n+1})*B(k_{2n+1}))} ]

    (2n+1) 个不同的点是因为两个 (n) 次的多项式相乘,最高次项有 (2n) 次,也就需要 (2n+1) 个点来表示。

    • 对点值表示下多项式乘法,可以理解为画两个函数图像,对二图像上横坐标相同的点纵坐标相乘,显然得到的是卷积后的多项式所表示的函数。

    接下来文章中出现的 (n) 默认为 (2) 的次幂。

    step2:处理后事

    问题来了,我们如何将多项式快速转化成点值表达,并且将点值表达转换回系数表达呢?

    为了问题能快速解答,我们需要利用一组特殊的点,也就是 (n) 次单位根。(不知道的回去看博客)

    严格意义上我们定义满足 (x^n=1(x∈ ext{C}))(x) 的解集为 ({omega_{n}^k|k=0,1,2,..,n-1})

    结合上面这张图简单加深下印象。

    显然 (n) 次单位根就是从 (x) 轴(实轴)正半轴开始,将圆等分为 (n) 份。

    简记 (omega_n^1)(omega_{n}),易得 (omega_{n}= ext{exp}frac{2pi i}{n}=(cosig(frac{2π}{n} ig),sinig(frac{2π}{n} ig)))(据欧拉公式)。

    • 单位根几个简单的性质:

    1、(omega_n^0=1)

    2、(omega_n^k=omega_n^{kmod n})(周期性,显然)

    3、(omega_n^{k+n/2}=-omega_n^k)(由三角表达式可以得到)

    4、(omega_{2n}^{2k}=omega_n^k)

    看起来这非常特殊,尤其是第四点,每次范围折半,可能可以分治

    step3:实践,DFT!

    考虑将多项式 (F(x)) 转化为点值表达,带入的点值用单位根。

    • 注意:多项式仅有 (n-1) 项,而不是 (n) 项,不然会少一个点而不能表达!具体操作的时候可以直接把 (n) 拓展到大于 (n)(2) 的整数次幂。

    我们考虑一种分治方式:

    [F(x)=(a_0+a_2x^2+…+a_{n-2}x^{n-2})+(a_1x^1+a_3x^3+…+a_{n-1}x^{n-1}) ]

    设多项式 (FL(x))(FR(x))

    [FL(x)=a_0+a_2x+…+a_{n-2}x^{frac{n}{2}-1} ]

    [FR(x)=a_1+a_3x+…+a_{n-1}x^{frac{n}{2}-1} ]

    则:

    [F(x)=FL(x^2)+xFR(x^2) ]

    这样划分的方式使得分治时左右比较相似。接下来带入单位根并考虑性质:

    (k<frac{n}{2})

    [F(omega_{n}^{k})=FL(omega_{n}^{2k})+omega_{n}^{k}FR(omega_{n}^{2k}) ]

    利用单位根性质得((n)(2) 的整数次幂):

    [F(omega_{n}^{k})=FL(omega_{n/2}^{k})+omega_{n}^{k}FR(omega_{n/2}^{k}) ]

    这样就满足分治的式子了,可以对 (FL(x),FR(x)) 分别计算。

    (k<frac{n}{2}),考虑 (k+n/2)

    [F(omega_{n}^{k+n/2})=FL(omega_{n}^{2k+n})+omega_{n}^{k+n/2}FR(omega_{n}^{2k+n}) ]

    利用性质 (2,3,4) 可以得到:

    [F(omega_{n}^{k+n/2})=FL(omega_{n/2}^{k})-omega_{n}^{k}FR(omega_{n/2}^{k}) ]

    也满足分治的情况,分别计算。

    归纳

    (k<n/2),则:

    [F(omega_{n}^{k})=FL(omega_{n/2}^{k})+omega_{n}^{k}FR(omega_{n/2}^{k}) ]

    [F(omega_{n}^{k+n/2})=FL(omega_{n/2}^{k})-omega_{n}^{k}FR(omega_{n/2}^{k}) ]

    发现两个式子只差了一个 (omega_{n}^{k}) 的符号,因此合并计算,且合并后即为原多项式的点值表达。

    时间复杂度 (O(n ext{ log }n))

    代码比较简单不放了,可以看原博客。

    step4:更进一步,IDFT!

    现在已知 (A(k)=displaystyle sum_{i=0}^{n-1}a_i*(omega_{n}^k)^i),求原多项式系数 (a_i),可以比较简单的构造出矩阵:

    [egin{equation} egin{bmatrix} (omega_n^0)^0 & (omega_n^0)^1 & cdots & (omega_n^0)^{n-1} \ (omega_n^1)^0 & (omega_n^1)^1 & cdots & (omega_n^1)^{n-1} \ vdots & vdots & ddots & vdots \ (omega_n^{n-1})^0 & (omega_n^{n-1})^1 & cdots & (omega_n^{n-1})^{n-1} end{bmatrix} egin{bmatrix} a_0 \ a_1 \ vdots \ a_{n-1} end{bmatrix} = egin{bmatrix} A(omega_n^0) \ A(omega_n^1) \ vdots \ A(omega_n^{n-1}) end{bmatrix} end{equation} ]

    根据单位根的对称性质不妨构造矩阵:

    [egin{equation*} mathbf D = egin{bmatrix} (omega_n^{-0})^0 & (omega_n^{-0})^1 & cdots & (omega_n^{-0})^{n-1} \ (omega_n^{-1})^0 & (omega_n^{-1})^1 & cdots & (omega_n^{-1})^{n-1} \ vdots & vdots & ddots & vdots \ (omega_n^{-(n-1)})^0 & (omega_n^{-(n-1)})^1 & cdots & (omega_n^{-(n-1)})^{n-1} end{bmatrix} end{equation*} ]

    上面的系数矩阵记为 (mathbf V),则可以比较简单的证明:(frac{1}{n}mathbf D = mathbf V^{-1})

    则可以得到 (IDFT) 的式子其实就是 (DFT) 中单位根取相反数:

    [a_i=frac{1}{n}sum_{i=0}^{n-1}(omega_{n}^{-k})^iA(i) ]

    也就是做 (DFT) 的时候把 (omega_n^{k}) 换成 (omega_n^{-k})

    更详细的证明可以看 OI Wiki,或者可以尝试用单位根反演证明(本质上就是反演)。

    一些优化

    递归版的 (FFT) 比较容易 (TLE),空间占用也较大,可以考虑把递归改成循环实现。

    考虑一开始按照奇偶性去分治的性质,可以理解为:

    考虑对 (A) 做一次变换,将 (A) 变为 ({A_0, A_2, A_4, A_6, ...}), ({A_1, A_3, A_5, A_7, ...})

    观察二进制位,可以发现本质上是按第 (1) 位二进制位为关键字进行排序。

    再变换一次,第一个序列变为 ({A_0, A_4, ...}), ({A_2, A_6, ...}) 观察二进制位,可以发现本质上是按第 (2) 位二进制位为关键字进行排序。

    于是整个变换的过程,相当于我们分别以第 (1) 位二进制位、第 (2) 位二进制位、第 (3) 位二进制位……为关键字进行排序,也就是从低位到高位进行比较排序。可以注意到,这一方法和我们平时比较两个整数大小(从高位到低位比较)是相反的,所以整个变换相当于以二进制位翻转后的下标为关键字进行排序。

    这也就是 (FFT) 蝴蝶变换,转化递归为循环迭代。

    可以结合图理解下:

    timg.jpg

    (图源网络)

    实现

    递归

    (先占坑)
    

    蝴蝶迭代

    (先占坑)
    

    再探 NTT

    step1:寻找替身

    (FFT) 拥有了模数,他就裂开了……因为复数域是不好取模的,所以考虑是否可以用一些特殊的整数代替。

    设模数为 (p),考虑模 (p) 意义一一组不同的整数可以代替单位根。我们希望这些数字都有共性,比如可以用一个底数表示,所以选择 数论原根

    原根具体的定义可以看之前那篇博客:

    (g) 为模 (p) 的原根,当且仅当 ({g^0,g^1,…,g^{p-2}}) 在模 (p) 的意义下互不相同。

    注意到之前所用的 (n)(2) 的整数次幂,则需要尽量选择 (p=a*2^k+1) 这一形式的质数;换言之,也就是 (nmid p-1)

    则可以令 (omega_{n}^{1}equiv g^{frac{p-1}{n}}pmod p),显然根据原根的定义,(g^{frac{p-1}{n}*k}(k=0,1,..,n-1)) 在模 (p) 意义下互不相同。

    显然是与我们所要的单位根性质所一致的,并且仍有类似单位根的递推性质和对称性。

    step2:替换,实现!

    考虑多项式 (A(x)),在原根下的点值表达((DFT)):

    [A(k)=sum_{i=0}^{n-1}a_i(g^{frac{p-1}{n}})^k ]

    反演((IDFT)):

    [a_k=frac{1}{n}sum_{i=0}^{n-1}A(i)(g^{-frac{p-1}{n}})^k ]

    然后同样做 (FFT) 即可。

  • 相关阅读:
    内置函数,闭包。装饰器初识
    生成器
    百度ai 接口调用
    迭代器
    HashMap与ConcurrentHashMap的测试报告
    ConcurrentHashMap原理分析
    centos 5.3 安装(samba 3.4.4)
    什么是shell? bash和shell有什么关系?
    Linux中使用export命令设置环境变量
    profile bashrc bash_profile之间的区别和联系
  • 原文地址:https://www.cnblogs.com/Ning-H/p/13693580.html
Copyright © 2011-2022 走看看