【算法讲15:多项式卷积】FFT | NTT

【算法讲15:多项式卷积】

  • 模板题
  • 多项式的表示法
  • 多项式的乘法与卷积
    • 多项式乘法
    • ⌈ \lceil 快速傅里叶变换 ⌋ \rfloor F F T FFT FFT
    • 单位根
    • 离散傅里叶变换 D F T DFT DFT
    • D F T DFT DFT 的详细讨论
    • 离散傅里叶逆变换 I D F T IDFT IDFT
    • 减小常数 (蝴蝶变换)
  • ⌈ \lceil 快速数论变换 ⌋ \rfloor (NTT)
    • 原根
    • 同样的性质
  • 参考与查阅

模板题

  • 【模板】多项式乘法(FFT) | 洛谷 P3803
    求两个多项式的乘积

多项式的表示法

  • 系数表示: n n n 个系数确定一个 n − 1 n-1 n1 次多项式
    ( a 0 , a 1 , a 2 , ⋯   , a n − 1 ) (a_0,a_1,a_2,\cdots,a_{n-1}) (a0,a1,a2,,an1)
  • 点值表示: n n n 个点确定一个 n − 1 n-1 n1 刺多项式
    ( x 0 , x 1 , x 1 , ⋯   , x n − 1 ) ( y 0 , y 1 , y 1 , ⋯   , y n − 1 ) (x_0,x_1,x_1,\cdots,x_{n-1})\\ (y_0,y_1,y_1,\cdots,y_{n-1})\\ (x0,x1,x1,,xn1)(y0,y1,y1,,yn1)
    其中满足
    y i = ∑ j = 0 n − 1 a j x i j y_i=\sum_{j=0}^{n-1}a_jx_i^j yi=j=0n1ajxij

多项式的乘法与卷积

多项式乘法

  • 有两个多项式 A ( x ) , B ( x ) A(x),B(x) A(x)B(x),两个多项式相乘结果为 C ( x ) C(x) C(x)
  • 新的多项式系数可以表示为:
    c k = ∑ i + j = k a i b j = ∑ i = 0 k a i b k − i c_k=\sum_{i+j=k}a_ib_j=\sum_{i=0}^ka_ib_{k-i} ck=i+j=kaibj=i=0kaibki
  • 多项式乘法得到的结果又可以叫做这两个多项式的卷积,可以用 F F T FFT FFT 进行快速计算。

⌈ \lceil 快速傅里叶变换 ⌋ \rfloor F F T FFT FFT

  • 如果暴力求解两个多项式的卷积,时间复杂度为 O ( n m ) O(nm) O(nm)
    利用 F F T FFT FFT 可以把时间复杂度降到 O ( n log ⁡ n ) O(n\log n) O(nlogn)
    具体分为两个过程:
  • A ( x ) 、 B ( x ) A(x)、B(x) A(x)B(x) 的系数表示转化成点值表示,然后用 O ( n ) O(n) O(n) 的方法求得 C ( x ) C(x) C(x) 的点值表示,然后还原成 C ( x ) C(x) C(x) 的系数表示。
  • 那么第一步,我们想要找一个快速的方法把 A ( x ) 、 B ( x ) A(x)、B(x) A(x)B(x) 的点值表示求出来。

单位根

  • F F T FFT FFT 的方法是找到了一组特殊的点代到多项式当中去,可以快速找到一组点的数值。那组特殊的点就是复数中的单位根
  • 一个复数可以在复平面上用一个向量来表示。在单位元上的向量都可以成为单位根。
    我们把一个单位元分成 n n n 份,我们就得到了 n n n 个单位根,其中的第 k k k 个可以即为 ω n k \omega_n^k ωnk,并且 ω n 0 = 1 \omega_n^0=1 ωn0=1
  • 欧拉公式: e i θ = c o s θ + i ⋅ sin ⁡ θ e^{i\theta}=cos\theta+i\cdot\sin\theta eiθ=cosθ+isinθ
    于是,
    ω n k = e 2 π k n i \omega_n^k=e^{2\pi\frac{k}{n}i} ωnk=e2πnki
  • 性质:
    ω n k = ω 2 n 2 k \omega_n^k=\omega_{2n}^{2k} ωnk=ω2n2k
    ω n k + 2 n = − ω n k \omega_n^{k+\frac{2}{n}}=-\omega_n^k ωnk+n2=ωnk
    ω n k ⋅ ω n l = ω n k + l \omega_n^k\cdot\omega_n^l=\omega_n^{k+l} ωnkωnl=ωnk+l

离散傅里叶变换 D F T DFT DFT

  • 考虑 n = 2 b n=2^b n=2b 项的多项式 A ( x ) A(x) A(x) 系数表示为 ( a 0 , a 1 , ⋯   , a n − 1 ) (a_0,a_1,\cdots,a_{n-1}) (a0,a1,,an1)
  • 我们现在想导入一组单位根 ( ω n 0 , ω n 1 , ⋯   , ω n n − 1 ) (\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}) (ωn0,ωn1,,ωnn1),快速算出 ( A ( ω n 0 ) , A ( ω n 1 ) , ⋯   , A ( ω n n − 1 ) ) (A(\omega_n^0),A(\omega_n^1),\cdots,A(\omega_n^{n-1})) (A(ωn0),A(ωn1),,A(ωnn1)),这个过程成为 D F T DFT DFT
  • 首先,我们把多项式按奇偶性分类:
    A ( x ) = ( a 0 + a 2 x 2 + ⋯ + a n − 2 x n − 2 ) + x ( a 1 + a 3 x 2 + ⋯ + a n − 1 x n − 2 ) A(x)=(a_0+a_2x^2+\cdots+a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots+a_{n-1}x^{n-2}) A(x)=(a0+a2x2++an2xn2)+x(a1+a3x2++an1xn2)
    现在,我们设两个多项式:
    A 0 ( x ) = a 0 + a 2 x 1 + ⋯ + a n − 2 x n − 2 2 A 1 ( x ) = a 1 + a 3 x 1 + ⋯ + a n − 1 x n − 2 2 A0(x)=a_0+a_2x^1+\cdots+a_{n-2}x^{\frac{n-2}{2}}\\ A1(x)=a_1+a_3x^1+\cdots+a_{n-1}x^{\frac{n-2}{2}} A0(x)=a0+a2x1++an2x2n2A1(x)=a1+a3x1++an1x2n2
    于是,原来的多项式就变为:
    A ( x ) = A 0 ( x 2 ) + x A 1 ( x 2 ) A(x)=A0(x^2)+xA1(x^2) A(x)=A0(x2)+xA1(x2)

D F T DFT DFT 的详细讨论

  • 0 ≤ k ≤ n 2 − 1 0\le k\le \frac{n}{2}-1 0k2n1 时,
    A ( ω n k ) = A 0 ( ( ω n k ) 2 ) + ω n k ⋅ A 1 ( ( ω n k ) 2 ) A(\omega_n^k)=A0((\omega_n^k)^2)+\omega_n^k\cdot A1((\omega_n^k)^2) A(ωnk)=A0((ωnk)2)+ωnkA1((ωnk)2)
    化简得到:
    A ( ω n k ) = A 0 ( ω n 2 k ) + ω n k ⋅ A 1 ( ω n 2 k ) A(\omega_n^k)=A0(\omega_{\frac{n}{2}}^k)+\omega_n^k\cdot A1(\omega_{\frac{n}{2}}^k) A(ωnk)=A0(ω2nk)+ωnkA1(ω2nk)
    可以看到,我们分为了两个子问题,可以递归求解。
  • 那么 n 2 ≤ k + n 2 ≤ n − 1 \frac{n}{2}\le k+\frac{n}{2}\le n-1 2nk+2nn1,我们带入得到:
    A ( ω n k + n 2 ) = A 0 ( ( ω n k + n 2 ) 2 ) + ω n k + n 2 ⋅ A 1 ( ( ω n k + n 2 ) 2 ) A(\omega_n^{k+\frac{n}{2}})=A0((\omega_n^{k+\frac{n}{2}})^2)+\omega_n^{k+\frac{n}{2}}\cdot A1((\omega_n^{k+\frac{n}{2}})^2) A(ωnk+2n)=A0((ωnk+2n)2)+ωnk+2nA1((ωnk+2n)2)
    化简得到:
    A ( ω n k + n 2 ) = A 0 ( ω n 2 k ) − ω n k ⋅ A 1 ( ω n 2 k ) A(\omega_n^{k+\frac{n}{2}})=A0(\omega_{\frac{n}{2}}^k)-\omega_n^k\cdot A1(\omega_{\frac{n}{2}}^k) A(ωnk+2n)=A0(ω2nk)ωnkA1(ω2nk)
  • 时间复杂度: T ( n ) = 2 T ( n 2 ) + O ( n ) = O ( n log ⁡ n ) T(n)=2T(\frac{n}{2})+O(n)=O(n\log n) T(n)=2T(2n)+O(n)=O(nlogn)

离散傅里叶逆变换 I D F T IDFT IDFT

  • D F T DFT DFT 得到的点值转化为系数,这一过程就叫做 I D F T IDFT IDFT
  • 假设我们得到了 A ( x ) A(x) A(x) 的点值,设为 ( d 0 , d 1 , ⋯   , d n − 1 ) (d_0,d_1,\cdots,d_{n-1}) (d0,d1,,dn1),我们要获得其多项式 F ( x ) F(x) F(x)
    c ( k ) c(k) c(k) x = ω n − k x=\omega_n^{-k} x=ωnk 时的点值。
    c k = ∑ i = 0 n − 1 d i ⋅ ( ω n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ⋅ ( ω n i ) j ⋅ ( ω n − k ) i = ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( ω n j − k ) i = ∑ j = 0 n − 1 a j ⋅ n ⋅ [ j = k ] = n ⋅ a k c_k=\sum_{i=0}^{n-1}d_i\cdot (\omega_n^{-k})^i=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j\cdot (\omega_n^i)^j\cdot (\omega_n^{-k})^i=\sum_{j=0}^{n-1}a_j \sum_{i=0}^{n-1}(\omega_n^{j-k})^i=\sum_{j=0}^{n-1}a_j\cdot n\cdot [j=k]=n\cdot a_k ck=i=0n1di(ωnk)i=i=0n1j=0n1aj(ωni)j(ωnk)i=j=0n1aji=0n1(ωnjk)i=j=0n1ajn[j=k]=nak

减小常数 (蝴蝶变换)

  • 我们一遍遍递归下去,在最后一层发现一个规律:原序列和置换后的序列的对应二进制位刚好是翻转的。比如 1 → 8 1\rightarrow 8 18 14 → 7 14\rightarrow 7 147
  • 代码:
    2.77 s 2.77s 2.77s
const int MAX = 1e7+50;

const double PI = acos(-1);
struct comp{
     
    double x,y;
    comp(double xx = 0,double yy = 0){
     x = xx,y = yy;}
};
comp operator +(comp a,comp b){
     return comp(a.x+b.x , a.y+b.y);}
comp operator -(comp a,comp b){
     return comp(a.x-b.x , a.y-b.y);}
comp operator *(comp a,comp b){
     return comp(a.x*b.x-a.y*b.y , a.x*b.y+a.y*b.x);}

int lim = 1;
int l,r[MAX];
void FFT(comp *A,int type){
     
    for(int i = 0;i < lim;++i)
        if(i < r[i])swap(A[i],A[r[i]]);
    for(int mid = 1;mid < lim;mid<<=1){
     
        comp omg(cos(PI/mid),type * sin(PI/mid));
        for(int R = mid<<1,j = 0;j < lim;j += R){
     
            comp w(1,0);
            for(int k = 0;k < mid;++k,w = w * omg){
     
                comp x = A[j+k],y = w * A[j + mid + k];
                A[j + k] = x + y;
                A[j + mid + k] = x - y;
            }
        }
    }
}
comp aa[MAX],bb[MAX];
int main()
{
     
    int N,M;N = read();M = read();
    for(int i = 0;i <= N;++i)aa[i].x = read();
    for(int i = 0;i <= M;++i)bb[i].x = read();
    while(lim <= N + M)lim<<=1,l++;
    for(int i = 0;i < lim;++i)
        r[i] = (r[i>>1]>>1) | ((i&1)<<(l-1));
    FFT(aa,1);
    FFT(bb,1);
    for(int i = 0;i <= lim;++i)aa[i] = aa[i] * bb[i];
    FFT(aa,-1);
    for(int i = 0;i <= N + M;++i)
        printf("%d ",(int)(aa[i].x/lim+0.5));
    return 0;
}

⌈ \lceil 快速数论变换 ⌋ \rfloor (NTT)

  • 求的是多项式乘法的系数取模之后的结果。这里模数应该要是一个质数。
  • 我们希望带入一组幂次以便于更好取模运算。

原根

  • 如果 g i   m o d   p ( 1 ≤ i ≤ p − 1 ) g^i\bmod p (1\le i\le p-1) gimodp(1ip1) 的值互不相同,那么 g g g 称为 p p p 的原根。
    并非所有的数都有原根,且如果一个数存在原根,其个数也不唯一。
  • 事实上,原根存在的重要条件是: m = 2 , 4 , p a , 2 p a ( a > 0 ) m=2,4,p^a,2p^a(a>0) m=2,4,pa,2pa(a>0) a a a 为奇素数。
    如果这个数有原根,那么一定有 φ ( φ ( m ) ) \varphi(\varphi(m)) φ(φ(m)) 个原根。
  • 原根没有快速求法,只能通过暴力枚举判断,或者查表。
    常见的 N T T NTT NTT 模数是 998244353 998244353 998244353,其原根为 3 3 3

同样的性质

  • 加入模数为 p p p,我们发现构造一下原根,得到与单位根有同样的性质的数,即:
    ω n i = g i ( p − 1 ) n   m o d   p \omega_n^i=g^{\frac{i(p-1)}{n}}\bmod p ωni=gni(p1)modp
    直接将 F F T FFT FFT 中的原根替换为 N T T NTT NTT 中的原根。
  • 代码
    1.64 s 1.64s 1.64s
const int MAX = 1e7+50;

ll qpow(ll a,ll n){
     /* */ll res = 1LL;while(n){
     if(n&1)res=res*a%MOD;a=a*a%MOD;n>>=1;}return res;}

const int P = 998244353,G = 3,Gi = 332748118;
int lim = 1;
int l,r[MAX];
void NTT(ll *A,int type){
     
    for(int i = 0;i < lim;++i)
        if(i < r[i])swap(A[i],A[r[i]]);
    for(int mid = 1;mid < lim;mid<<=1){
     
        ll omg = qpow(type == 1 ? G : Gi , (P - 1) / (mid << 1));
        for(int j = 0;j < lim;j += (mid << 1)){
     
            ll w = 1;
            for(int k = 0;k < mid;++k,w = w * omg % P){
     
                int x = A[j+k],y = w * A[j + mid + k] % P;
                A[j + k] = (x + y) % P;
                A[j + mid + k] = (x - y + P) % P;
            }
        }
    }
}
ll aa[MAX],bb[MAX];
int main()
{
     
    int N,M;N = read();M = read();
    for(int i = 0;i <= N;++i)aa[i] = (read() + P) % P;
    for(int i = 0;i <= M;++i)bb[i] = (read() + P) % P;
    while(lim <= N + M)lim<<=1,l++;
    for(int i = 0;i < lim;++i)
        r[i] = (r[i>>1]>>1) | ((i&1)<<(l-1));
    NTT(aa,1);
    NTT(bb,1);
    for(int i = 0;i < lim;++i)aa[i] = (aa[i] * bb[i]) % P;
    NTT(aa,-1);
    ll iv = qpow(lim,P - 2);
    for(int i = 0;i <= N + M;++i)
        printf("%d ",(aa[i] * iv) % P);
    return 0;
}

参考与查阅

  • 【SDUACM-暑期专题div1】FFT、NTT
  • 洛谷改题的 d a l a o dalao dalao 们的题解

你可能感兴趣的:(【算法/知识点,浅谈】,算法,快速傅立叶变换,快速数论变换)