FFT 快速傅里叶变换 与 NTT 快速数论变换 学习笔记

这个东西是个玄乎的东西,反正最后还是背板子 理论知识也不用管太多吧

这里搬一些简单的证明

复数运算法则 设复数 A=a+bi,B=c+di A = a + b i , B = c + d i

A+B=(a+c)+(b+d)i A + B = ( a + c ) + ( b + d ) i
AB=(ac)+(bd)i A − B = ( a − c ) + ( b − d ) i
A×B=(acbd)+(ad+bc)i A × B = ( a c − b d ) + ( a d + b c ) i
除法转化为乘共轭复数就好了

一般系统有自带的 std::complex<double> s t d :: c o m p l e x < d o u b l e > 可以直接用
但是手写速度要有保证一些

首先整篇文章讨论的多项式的次数都是 2k1 2 k − 1 次的,以下不作说明

ωkn=cos(k2πn)+i×sin(k2πn) ω n k = c o s ( k 2 π n ) + i × s i n ( k 2 π n ) 这个根据复数乘法可知

ωkn=ω2k2n ω n k = ω 2 n 2 k 这个根据上面那条东西可以得到

ωk+n2n=ωkn ω n k + n 2 = − ω n k 代入第一个式子得出 ωn2=1 ω n 2 = − 1 即可

那么我们假设原多项式是 A(x)=n1i=0aixi A ( x ) = ∑ i = 0 n − 1 a i x i

奇偶分组后两个多项式分别为
A1(x)=a0+a2x+...+an2xn12 A 1 ( x ) = a 0 + a 2 x + . . . + a n − 2 x n − 1 2
A2(x)=a1+a3x+...+an1xn12 A 2 ( x ) = a 1 + a 3 x + . . . + a n − 1 x n − 1 2
A(x)=A1(x2)+x×A2(x2) A ( x ) = A 1 ( x 2 ) + x × A 2 ( x 2 )

我们试着推导以下已知 A1,A2 A 1 , A 2 怎么还原出 A A
k<n2, 则 kk+n2 k 和 k + n 2 可以取遍 [0,n1] [ 0 , n − 1 ] 的所有值
A(ωkn)=A1(ω2kn)+ωkn×A2(ω2kn) A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k × A 2 ( ω n 2 k )
          =A1(ωkn2)+ωkn×A2(ωkn2)                     = A 1 ( ω n 2 k ) + ω n k × A 2 ( ω n 2 k )
A(ωk+n2n)=A1(ω2k+nn)+ωk+n2n×A2(ω2k+nn) A ( ω n k + n 2 ) = A 1 ( ω n 2 k + n ) + ω n k + n 2 × A 2 ( ω n 2 k + n )
               =A1(ωkn2)ωkn×A2(ωkn2)                               = A 1 ( ω n 2 k ) − ω n k × A 2 ( ω n 2 k )
具体推式子只需要之前推出的单位复根的性质即可
这样子就可以分治下去直到一个常数项为止,我们得到了一个在 O(nlogn) O ( n l o g n ) 时间把多项式从系数转化为点值的算法

一般点值表示法不是很常用,所以我们还要考虑怎么把一个多项式从点值转换为系数

ck=n1i=0yi(ωkn)i c k = ∑ i = 0 n − 1 y i ( ω n − k ) i
也就是多项式 C(x)=n1i=0yixi C ( x ) = ∑ i = 0 n − 1 y i x i x x ωnk
其中 yi y i 表示 xωinA(x) x 取 ω n i 时 的 A ( x ) 的 值
下面开始推式子

ck=n1i=0yi(ωkn)i c k = ∑ i = 0 n − 1 y i ( ω n − k ) i
ck=n1i=0(ωkn)i×(n1j=0aj(ωin)j) c k = ∑ i = 0 n − 1 ( ω n − k ) i × ( ∑ j = 0 n − 1 a j ( ω n i ) j )
ck=n1i=0n1j=0(ωjkn)iaj c k = ∑ i = 0 n − 1 ∑ j = 0 n − 1 ( ω n j − k ) i a j
ck=n1j=0ajn1i=0(ωjkn)i c k = ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( ω n j − k ) i
我们发现 n1i=0(ωjkn)i ∑ i = 0 n − 1 ( ω n j − k ) i 是一个等比数列求和的形式
带入高中数学和蔼可亲的公式 Sn=a11qn1q S n = a 1 1 − q n 1 − q
我们发现公比 q=ωjkna1=1 q = ω n j − k , a 1 = 1
Sn=1(ωjkn)n1q S n = 1 − ( ω n j − k ) n 1 − q
当分母不为 0 0 时, Sn=0, 否则 Sn=n S n = n
ck=akn c k = a k n
ak=ckn a k = c k n
我们可以通过之前得到的 O(nlogn)) O ( n l o g n ) ) 计算多项式点值的方法来实现
最后记得除以 n n 就好了

总体就是快速将系数转为点值为了O(n)实现两个多项式相乘
最后再快速转为系数,通过单位复根优秀的性质来实现

贴个递归的板子

Codes
#include<bits/stdc++.h>
#define For(i, a, b) for(register int i = a; i <= b; ++ i)

using namespace std;

const int maxn = 4e6 + 10;
const double Pi = acos(-1);
int n, m;

struct Complex {
    double x, y;
    Complex (double xx = 0, double yy = 0) {
        x = xx, y = yy;
    }
    Complex operator + (const Complex &T) {
        return Complex{x + T.x, y + T.y};
    }
    Complex operator - (const Complex &T) {
        return Complex{x - T.x, y - T.y};
    }
    Complex operator * (const Complex &T) {
        return Complex{x * T.x - y * T.y, x * T.y + y * T.x};
    }
}a[maxn], b[maxn];

void FFT(int limit, Complex *a, int fh) {
    if(limit == 1) return;
    Complex a1[limit >> 1], a2[limit >> 1];
    for(int i = 0; i <= limit; i += 2)  
        a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];    
    FFT(limit >> 1, a1, fh), FFT(limit >> 1, a2, fh);
    Complex wn1 = Complex(cos(2.0 * Pi / limit), sin(2.0 * Pi / limit) * fh), w = Complex(1, 0);
    for(int i = 0; i < (limit >> 1); ++ i, w = w * wn1) 
        a[i] = a1[i] + w * a2[i], a[i + (limit >> 1)] = a1[i] - w * a2[i];
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("FFT.in", "r", stdin);
    freopen("FFT.out", "w", stdout);
#endif
    int x, limit = 1; scanf("%d%d", &n, &m);
    For(j, 0, n) scanf("%d", &x), a[j].x = x;
    For(j, 0, m) scanf("%d", &x), b[j].x = x;

    while(limit <= n + m) limit <<= 1;
    FFT(limit, a, 1), FFT(limit, b, 1);
    For(i, 0, limit - 1) a[i] = a[i] * b[i];
    FFT(limit, a, -1);

    For(i, 0, n + m) printf("%d ", int(a[i].x / limit + 0.5)); 

    return 0;
}

我们发现递归的效率不是很高 考虑迭代实现
盗用网上的一张图
这里写图片描述
发现最后每一位上数的下标为原下标的二进制位反转
对于 0,1,2,3,4,5,6,7 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7
翻转后就是 0,4,2,6,1,5,3,7 0 , 4 , 2 , 6 , 1 , 5 , 3 , 7
把这几个数都写成二进制形式规律就很清楚了

那么迭代实现就直接加上

void FFT_Init(int limit, int *a) {
    int k = log2(limit);
    for(int j = 0; j < limit; ++ j) {
        int t = 0;
        for(int i = 0; i < k; ++ i)
            if((1 << i) & j)
                t |= 1 << (k - j - 1);
        if(t < j) swap(a[t], a[j]);
    }
}

我们来考虑一种更为优秀的基于递推的 O(n) O ( n ) 预处理

k = log2(limit);
for(int i = 0; i < limit; ++ i)
    rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (k - 1));

如果这个数是偶数,把它最后一位去掉后翻转,就相当于后面多了一个 0 0 所以删去这个0即可
如果是奇数那第一位是 1 1 ,所以补上就好了

FFT板子

#include<bits/stdc++.h>
#define For(i, a, b) for(register int i = a; i <= b; ++ i)

using namespace std;

const int maxn = 4e6 + 10;
const double Pi = acos(-1);

struct Complex {
    double x, y;
    Complex(double xx = 0, double yy = 0) {
        x = xx, y = yy;
    }
    Complex operator + (const Complex &T) {
        return Complex(x + T.x, y + T.y);
    }
    Complex operator - (const Complex &T) {
        return Complex(x - T.x, y - T.y);
    }
    Complex operator * (const Complex &T) {
        return Complex(x * T.x - y * T.y, x * T.y + y * T.x);
    }
}a[maxn], b[maxn];

void FFT(int n, Complex *a, int fh) {
    int k = log2(n);
    For(i, 0, n - 1) {
        int t = 0;
        For(j, 0, k - 1)
            if((1 << j) & i)
                t |= (1 << (k - j - 1));
        if(i < t) swap(a[i], a[t]);
    }
    for(int limit = 2; limit <= n; limit <<= 1) {
        double xita = 2.0 * Pi / limit;
        Complex Wn1 = Complex(cos(xita), sin(xita) * fh), W = Complex(1, 0);
        for(int j = 0; j < n; j += limit, W = Complex(1, 0))    
            for(int i = j; i < j + (limit >> 1); ++ i, W = W * Wn1) {
                Complex a1 = a[i], a2 = W * a[i + (limit >> 1)];
                a[i] = a1 + a2, a[i + (limit >> 1)] = a1 - a2;
            }
    }
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("3803.in", "r", stdin);
    freopen("3803.out", "w", stdout);
#endif
    int n, m, limit = 1, x;
    scanf("%d%d", &n, &m);
    while(limit <= (n + m))
        limit <<= 1;
    For(i, 0, n) 
        scanf("%d", &x), a[i].x = x;
    For(i, 0, m) 
        scanf("%d", &x), b[i].x = x;
    FFT(limit, a, 1), FFT(limit, b, 1);
    For(i, 0, limit - 1)
        a[i] = a[i] * b[i];
    FFT(limit, a, -1);
    For(i, 0, n + m)
        printf("%d ", int(a[i].x / limit + 0.5));
    return 0;
}

FFT F F T NTT N T T

 NTT 快速数论变换

听到这个名字就知道 NTT N T T 这个是在数论中使用的
FFT F F T 已经很优秀了,那我们为什么还需要这样的东西代替它呢
因为 FFT F F T 在复数运算的时候会有精度误差,不能保证得到答案的精确性
这个时候我们就想能不能找到一个东西代替复数的运算
这个时候 NTT N T T 就横空出世了,首先 NTT N T T 针对的是模意义下的运算

那么我们考虑怎么找到一个跟单位复根有着相同性质的东西
那就是原根 (本文讨论的模数为 p=qn+1(n=2k) p = q n + 1 ( n = 2 k ) )

原根的定义
m m 是正整数, a 是整数,若 a a m 的阶等于 ϕ(m) ϕ ( m ) ,则称 a a 为模 m 的一个原根

阶的定义
a,p a , p 互素,且 p>1 p > 1 ,对于 an1(mod p) a n ≡ 1 ( m o d   p ) 最小的n,我们称之为 a a p的阶,记做 δp(a) δ p ( a ) ,例如 δ7(2)=3 δ 7 ( 2 ) = 3 ,因为 231(mod 7) 2 3 ≡ 1 ( m o d   7 )

这样子我们就可以发现原根这个东西是不唯一的

并且原根有一个很重要的定理我不会证
p p 为素数,假设一个数g p p 的原根,那么gi mod p(1<g<p,0<i<p)的结果两两不同,这样子就满足了可以代不同的 x x 得到多项式的点值了
上面这个东西我在网上找了很多资料也没有证明 可能可以问一下数竞的选手应该会

1.令ωn=gq ω0n,ω1n,...,ωn1n ω n 0 , ω n 1 , . . . , ω n n − 1 互不相同

2. ωkn=ω2k2n ω n k = ω 2 n 2 k 代入第一个式子即可
3. ωk+n2n=ωkn ω n k + n 2 = − ω n k
ωnn=gqn=gp11(mod p) ω n n = g q n = g p − 1 ≡ 1 ( m o d   p ) (费马小定理可知)
(ωn2n)21(mod p) ( ω n n 2 ) 2 ≡ 1 ( m o d   p )
又因为 ωn2n1(mod p) ω n n 2 ≠ 1 ( m o d   p )
所以 ωn2n1(mod p) ω n n 2 ≡ − 1 ( m o d   p )
ωk+n2n=ωkn ω n k + n 2 = − ω n k

这样子原根就有了单位复根的所有性质了,那么就可以直接用原根代替单位复根,模意义下运算代替复数运算,就得到了可爱的 NTT N T T
还有要注意的几个地方 一般 NTT N T T 模的数是 998244353 998244353 1004535809 1004535809
这两个数都有一个共同的原根为 3 3 OI中用 3 3 就好了,如果你还想模其他的数那么就多用几个模数做NTT,用中国剩余定理合并就好啦
每次分治时用的 ωn=gmod1n ω n = g m o d − 1 n
我的理解是为了保证 ωnn=1 ω n n = 1 因为推 ωk+n2n=ωkn ω n k + n 2 = − ω n k 有用到过
当然逆运算的时候不要忘记把 g g 换成invg就好啦

Codes
#include<bits/stdc++.h>

using namespace std;

const int N = 4e6 + 10;
const int mod = 998244353;
int a[N], b[N];

int qpow(int a, int x) {
    int ret = 1;
    for(; x; a = 1ll * a * a % mod, x >>= 1)
        if(x & 1)
            ret = 1ll * ret * a % mod;
    return ret;
}

void NTT_Init(int limit, int *a, int k) {
    for(int i = 0; i < limit; ++ i) {
        int t = 0;
        for(int j = 0; j < k; ++ j)
            if((1 << j) & i)
                t |= 1 << (k - j - 1);
        if(t < i) swap(a[t], a[i]);
    }
}

void NTT(int n, int *a, int fh) {
    int k = log2(n), inv = qpow(n, mod - 2);
    NTT_Init(n, a, k);
    for(int limit = 2; limit <= n; limit <<= 1) {

        int Wn1 = qpow(3, (mod - 1) / (limit)), w = 1;
        if(fh == -1) Wn1 = qpow(Wn1, mod - 2);

        for(int j = 0; j < n; j += limit, w = 1)
            for(int i = j; i < j + (limit >> 1); ++ i, w = 1ll * w * Wn1 % mod) {
                int a1 = a[i], a2 = 1ll * a[i + (limit >> 1)] * w % mod;
                a[i] = (a1 + a2) % mod, a[i + (limit >> 1)] = (a1 - a2 + mod) % mod;        
            }
    }
    if(fh == -1)
        for(int i = 0; i < n; ++ i)
            a[i] = 1ll * a[i] * inv % mod;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("34.in", "r", stdin);
    freopen("34.out", "w", stdout);
#endif
    int n, m, limit = 1;
    scanf("%d%d", &n, &m);
    while(limit <= (n + m))
        limit <<= 1;
    for(int i = 0; i <= n; ++ i)
        scanf("%d", &a[i]);
    for(int i = 0; i <= m; ++ i)
        scanf("%d", &b[i]);
    NTT(limit, a, 1), NTT(limit, b, 1);
    for(int i = 0; i < limit; ++ i)
        a[i] = 1ll * a[i] * b[i] % mod;
    NTT(limit, a, -1);
    for(int i = 0; i <= n + m; ++ i)
        printf("%d ", a[i]);
    return 0;
}

终于写完了 ovo o v o 这种东西对我这种蒟蒻选手来说真的理解起来不容易,感谢 menci m e n c i 写的博客

引用来源
menci’s 从傅里叶变换到数论变换
menci’s FFT 学习笔记

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值