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

模板题

多项式的表示法

  • 系数表示: 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 + n 2 = − ω n k \omega_n^{k+\frac{n}{2}}=-\omega_n^k ωnk+2n=ω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;
}

参考与查阅

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
FFT快速傅里叶变换)是一种高效的算法,可用于多项式乘法。以下是Java中实现多项式相乘的FFT算法的代码示例: ```java import java.util.Arrays; public class FFT { private Complex[] omega; public Complex[] fft(Complex[] A) { int n = A.length; if (n == 1) return new Complex[] { A[0] }; Complex[] A0 = new Complex[n / 2]; Complex[] A1 = new Complex[n / 2]; for (int i = 0; i < n / 2; i++) { A0[i] = A[2 * i]; A1[i] = A[2 * i + 1]; } Complex[] y0 = fft(A0); Complex[] y1 = fft(A1); Complex[] y = new Complex[n]; for (int k = 0; k < n / 2; k++) { Complex w = omega[n][k]; y[k] = y0[k].plus(w.times(y1[k])); y[k + n / 2] = y0[k].minus(w.times(y1[k])); } return y; } public Complex[] inverseFFT(Complex[] y) { int n = y.length; Complex[] yInv = new Complex[n]; for (int i = 0; i < n; i++) { yInv[i] = y[i].conjugate(); } Complex[] aInv = fft(yInv); for (int i = 0; i < n; i++) { aInv[i] = aInv[i].conjugate().times(1.0 / n); } return aInv; } public void initOmega(int n) { omega = new Complex[n + 1]; for (int i = 0; i <= n; i++) { double angle = 2 * Math.PI * i / n; omega[i] = new Complex(Math.cos(angle), Math.sin(angle)); } } public Complex[] multiply(Complex[] a, Complex[] b) { int n = Integer.highestOneBit(Math.max(a.length, b.length) - 1) << 1; initOmega(n); Complex[] A = Arrays.copyOf(a, n); Complex[] B = Arrays.copyOf(b, n); Complex[] y1 = fft(A); Complex[] y2 = fft(B); Complex[] y = new Complex[n]; for (int k = 0; k < n; k++) { y[k] = y1[k].times(y2[k]); } Complex[] c = inverseFFT(y); return c; } public static void main(String[] args) { FFT fft = new FFT(); Complex[] a = new Complex[] { new Complex(1), new Complex(2), new Complex(3) }; Complex[] b = new Complex[] { new Complex(4), new Complex(5), new Complex(6) }; Complex[] c = fft.multiply(a, b); for (int i = 0; i < c.length; i++) { System.out.print(c[i] + " "); } } } class Complex { public double re; public double im; public Complex(double re, double im) { this.re = re; this.im = im; } public Complex plus(Complex other) { return new Complex(re + other.re, im + other.im); } public Complex minus(Complex other) { return new Complex(re - other.re, im - other.im); } public Complex times(Complex other) { return new Complex(re * other.re - im * other.im, re * other.im + im * other.re); } public Complex conjugate() { return new Complex(re, -im); } public String toString() { return String.format("(%f,%f)", re, im); } } ``` 该代码使用Complex类来表示复数,实现了FFT算法多项式相乘的操作。在主方法中,可以看到如何使用该类来计算多项式a和b的乘积。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值