FFT前置知识及FFT\FFT分治

本文深入解析快速傅里叶变换(FFT)原理及其在多项式乘法中的应用,涵盖复数运算、单位根概念、DFT与FFT算法,以及逆FFT实现。通过对比系数与点值表示法,阐述了FFT在提升多项式运算效率上的关键作用。

首先依次介绍FFT(快速傅里叶变换)前置知识:
复数及单位根
复变函数欧拉定理
单位根三大引理
多项式的系数与点值表示法
多项式的求值与插值(顺介绍拉格朗日插值法)
向量卷积

复数及单位根

我们知道复数可以写成 z = a + b i z=a+bi z=a+bi,其中 a a a为实部, b i bi bi为虚部.而且一个复数可以在一个复平面上表示出来,而我们可以将其写成极坐标的形式 z = r ( c o s θ + i s i n θ ) z=r(cos\theta+isin\theta) z=r(cosθ+isinθ)而根据欧拉定理:
r e θ i = = c o s θ + i s i n θ re^{\theta i} == cos\theta + isin\theta reθi==cosθ+isinθ
则有: z = r e θ i z=re^{\theta i} z=reθi,这里给出乘法的例子:
z 1 z 2 = = ( r 1 e θ 1 i ) ( r 2 e θ 2 i ) = = r 1 r 2 e ( θ 1 + θ 2 ) i z_1z_2 == (r_1e^{\theta_1 i})(r_2e^{\theta_2 i})==r_1r_2e^{(\theta_1+\theta_2)i} z1z2==(r1eθ1i)(r2eθ2i)==r1r2e(θ1+θ2)i
下面介绍单位根:
如果存在复数 ω n = = 1 \omega^n==1 ωn==1,则其根被称作单位根,显然单位根的模为1,其中我们定义单位根:
ω n k = = e i 2 k π n \omega_n^k==e^{i\frac{2k\pi}{n}} ωnk==ein2kπ
其中定义主n次单位根(当k==1时):
ω n = = e i 2 π n \omega_n==e^{i\frac{2\pi}{n}} ωn==ein2π
而在程序中我们一般写成三角函数形式:
c o s 2 k π n + i s i n 2 k π n cos\frac{2k\pi}{n} + isin\frac{2k\pi}{n} cosn2kπ+isinn2kπ
这里给出复数运算模板代码:

struct Complex
{
    double r, i;
    Complex() {}//无参函数不要写东西!易超时
    Complex(double r, double i)
    :r(r), i(i) {}
}f[N], g[N];
Complex operator + (const Complex & a, const Complex & b)
{return Complex(a.r + b.r, a.i + b.i);}
Complex operator - (const Complex & a, const Complex & b)
{return Complex(a.r - b.r, a.i - b.i);}
Complex operator * (const Complex & a, const Complex & b)

下面给出单位根的三大引理
ω d n d k = = ω n k (1) \omega_{dn}^{dk}==\omega_n^k\tag1 ωdndk==ωnk(1)
ω n k + n 2 = = ( ω n k ) 2 = = ω n 2 k (2) \omega_{n}^{k+\frac{n}{2}}==(\omega_{n}^{k})^2==\omega_{\frac{n}{2}}^k\tag2 ωnk+2n==(ωnk)2==ω2nk(2)
∑ i = 0 n − 1 ( w n k ) i = = 0 (3) \sum_{i=0}^{n-1}(w_n^k)^i==0\tag3 i=0n1(wnk)i==0(3)
多项式的两种表示方法:
第一种非常常见的系数表示法,及 f ( x ) = ∑ i = 0 n a i x i f(x)=\sum_{i=0}^n a_ix^i f(x)=i=0naixi,另外我们给出另一种表示方法,点值表示法,及用n+1种不同的取值代入 f ( x ) f(x) f(x),组成的集合:
A ( x ) = { ( x 0 , f ( x 0 ) ) , ( x 1 , f ( x 1 ) ) , . . . . ( x n , f ( x n ) ) } A(x)=\{(x_0,f(x_0)),(x_1,f(x_1)),....(x_n,f(x_n))\} A(x)={(x0,f(x0)),(x1,f(x1)),....(xn,f(xn))}
我们分析两种表示方法:
系数表达式:
求值:我们可以使用秦九昭算法 O ( n ) O(n) O(n)得出,
求和:两个表达式的各项系数相加,是 O ( n ) O(n) O(n)
求积:一个表达式相乘的各项都需要和另一个的各项相乘,是 O ( n 2 ) O(n^2) O(n2)
点值表达式:
求值:我们使用拉格朗日插值法可以在 O ( n 2 ) O(n^2) O(n2)的出系数表达式并求值(插值的定义一会给出)
这里给出拉格朗日插值法的公式(n项表达式):
A ( x ) = ∑ k = 0 n − 1 y k ∏ j ! = k ( x − x j ) ∏ j ! = k ( x k − x j ) A(x) = \sum_{k=0}^{n-1}y_k\frac{\prod_{j!=k}(x-x_j)}{\prod_{j!=k}(x_k-x_j)} A(x)=k=0n1ykj!=k(xkxj)j!=k(xxj)
并有模板:
P4781 【模板】拉格朗日插值
下面是模板代码:

const int N = 2e3+5;
const ll mod = 998244353;
ll a[N], b[N];
ll inv(ll a, ll b)
{
    ll res = 1;
    while(b)
    {
        if (b&1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res % mod;
}
int main()
{
    int n; ll k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i], &b[i]);
    ll sum = 0;
    for (int i = 1; i <= n; i++)
    {
        ll ans = b[i];
        ll fa = 1, fb = 1;
        for (int j = 1; j <= n; j++)
        {
            if (j == i) continue;
            fa = (fa * (k - a[j] + mod)%mod)%mod;
            fb = (fb * (a[i] - a[j] + mod)%mod)%mod;
        }
        ans = (ans * fa % mod * inv(fb, mod-2)%mod)%mod;
        sum = (sum + ans)%mod;
    }
    printf("%lld\n", sum);
}

求和:两个表达式的各项点值的 y y y值相加,是 O ( n ) O(n) O(n)
求积:两个表达式的各项点值的 y y y值相乘,是 O ( n ) O(n) O(n)
我们不难发现,如果我们相求两个多项式的积 f , g f,g f,g(系数向量分别是 a , b a,b a,b),在点值表达式的情况下将十分高效.这时新表达式 h h h(系数向量是c)的第i项系数为:
c i = ∑ k = 0 i a k b j − k c_i=\sum_{k=0}^ia_kb_{j-k} ci=k=0iakbjk
我们则称 c i c_i ci a i , b i a_i,b_i ai,bi的卷积,记作 a ⊗ b a\otimes b ab,两个多项式相乘,就是对应的两个系数向量的卷积,
我们将一个点值表达式转化为系数表达式称为插值.
由此可见:如何提高多项式求值与插值的效率.决定着两个表达式相乘的效率!

话讲完,正式下面介绍FFT

首先看DFT(离散傅里叶变换)
我们将单位根带入到系数表达式(方便起见下式中 ω \omega ω w w w替代)中:
y i = A ( w n i ) = ∑ j = 0 n − 1 w n i j a j y_i=A(w_n^i)=\sum_{j=0}^{n-1}w_n^{ij}a_j yi=A(wni)=j=0n1wnijaj
记作 y = D F T ( a ) y=DFT(a) y=DFT(a)
每次求值为 O ( n ) O(n) O(n)所以离散傅里叶变换最终时间复杂度为 O ( n 2 ) O(n^2) O(n2)
这样显然是不行的,毕竟直接相乘的时间复杂度都是 O ( n 2 ) O(n^2) O(n2)
然后就有了FFT:
我们对于一个n项系数表达式(n为2的幂数):
A ( x ) → a = [ a 0 , a 1 . . . . . a n − 1 ] A(x)\rightarrow a =[a_0,a_1.....a_{n-1}] A(x)a=[a0,a1.....an1]
可以分成:
A [ 0 ] ( x ) → a [ 0 ] = [ a 0 , a 2 , a 4 . . . . . . a n − 2 ] A^{[0]}(x)\rightarrow a^{[0]}=[a_0,a_2,a_4......a_{n-2}] A[0](x)a[0]=[a0,a2,a4......an2]
A [ 1 ] ( x ) → a [ 1 ] = [ a 1 , a 3 , a 5 . . . . . . a n − 1 ] A^{[1]}(x)\rightarrow a^{[1]}=[a_1,a_3,a_5......a_{n-1}] A[1](x)a[1]=[a1,a3,a5......an1]
这时我们发现一个及其有趣的事实:
A ( x ) = A [ 0 ] ( x 2 ) + x A [ 1 ] ( x 2 ) A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2) A(x)=A[0](x2)+xA[1](x2)
而根据(1),(2)两个引理:我们最终有:
{ A ( w n k ) = A [ 0 ] ( w n 2 k ) + w n k A [ 1 ] ( w n 2 k ) A ( w n k + n 2 ) = A [ 0 ] ( w n 2 k ) − w n k A [ 1 ] ( w n 2 k ) \left\{ \begin{aligned} A(w_n^k)&=A^{[0]}(w_{\frac{n}{2}}^k)+w_n^kA^{[1]}(w_{\frac{n}{2}}^k)\\ A(w_n^{k+\frac{n}{2}})&=A^{[0]}(w_{\frac{n}{2}}^k)-w_n^kA^{[1]}(w_{\frac{n}{2}}^k) \end{aligned} \right. A(wnk)A(wnk+2n)=A[0](w2nk)+wnkA[1](w2nk)=A[0](w2nk)wnkA[1](w2nk)
于是问题得以递归解决:
T ( n ) = O ( n 2 ) + 2 T ( n 2 ) T(n) = O(\frac{n}{2}) + 2T(\frac{n}{2}) T(n)=O(2n)+2T(2n)
时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

逆FFT

我们的多项式不可以FFT过去弄不过来了!比较简单,
因为:
y k = ∑ j = 0 n − 1 a j w n k j y_k=\sum_{j=0}^{n-1}a_jw_n^{kj} yk=j=0n1ajwnkj
所以:
a j = 1 n ∑ j = 0 n − 1 y k w n − k j a_j=\frac{1}{n}\sum_{j=0}^{n-1}y_kw_n^{-kj} aj=n1j=0n1ykwnkj
证明略,总之就是证明原向量矩阵可逆
所以我们稍加改动就可以逆FFT,就是将单位根的指数取相反数,最后结果再乘1/n
所以两个多项式 f , g f,g f,g的乘积,使其对应系数向量 a , b a,b a,b的卷积!可以有:
a ⊗ b = D F T 2 n − 1 ( D F T 2 n ( a ) D F T 2 n ( b ) ) a\otimes b =DFT^{-1}_{2n}(DFT_{2n}(a) DFT_{2n}(b)) ab=DFT2n1(DFT2n(a)DFT2n(b))

不过我们很少书写递归代码,这里使用迭代写法;
这里还牵扯到两个点:位逆序置换 蝴蝶操作,不说了…直接上代码吧
P3803 【模板】多项式乘法(FFT)

const int N = 3e6+5;
const double pi = acos(-1);
struct Complex
{
    double r, i;
    Complex() {}
    Complex(double r, double i)
    :r(r), i(i) {}
}f[N], g[N];
Complex operator + (const Complex & a, const Complex & b)
{return Complex(a.r + b.r, a.i + b.i);}
Complex operator - (const Complex & a, const Complex & b)
{return Complex(a.r - b.r, a.i - b.i);}
Complex operator * (const Complex & a, const Complex & b)
{return Complex(a.r * b.r - a.i * b.i, a.i * b.r + a.r * b.i);}
int rev[N];
int len, lim = 1;
void init(int n)
{
    while(lim <= n) lim <<= 1, len++;
    for (int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len-1));
}
void FFT(Complex *a, int op)//op为1是FFT运算,-1是FFT逆运算
{
    for (int i = 0; i < lim; i++)
    if (i < rev[i]) swap(a[i], a[rev[i]]);
    for (int dep = 1; dep <= log2(lim); dep++)
    {
        int m = 1 << dep;
        Complex wn = Complex(cos(2.0 * pi / m), op * sin(2.0 * pi / m));
        for (int k = 0; k < lim; k += m)
        {
            Complex w = Complex(1, 0);
            for (int j = 0; j < m / 2; j++)
            {
                Complex t = w * a[k + j + m / 2];
                Complex u = a[k + j];
                a[k + j] = u + t;
                a[k + j + m / 2] = u - t;
                w = w * wn;
            }
        }
    }
    if (op == -1) for (int i = 0; i < lim; i++)
        a[i].r /= lim;
}
int main()
{
    int n, m;
    cin >> n >> m;
    init(n + m);
    for (int i = 0; i <= n; i++) scanf("%lf", &f[i].r);
    for (int i = 0; i <= m; i++) scanf("%lf", &g[i].r);
    FFT(g, 1); FFT(f, 1);
    for (int i = 0; i <= lim; i++) f[i] = f[i] * g[i];
    FFT(f, -1);
    for (int i = 0; i <= n + m; i++)
        printf("%d ", (int)(f[i].r + 0.5));
    return 0;
}

FFT分治
【模板】分治 FFT
根据cdq分治思想,
我们在计算多项式的一个系数的时候,要统计之前的所有影响。我们区间折半递归解决,先解决左区间的数值,然后统计其对右区间的影响,累加到右区间。
洛谷模板需要取模,故模板代码使用NTT,值得注意的是,洛谷模板的模数998244353,符合NTT所要求的素数类型,且存在原根为3。
下面是模板代码:

const int N = 1e6+5;
const int G = 3;
const ll mod = 998244353;
ll f[N], g[N];
ll a[N], b[N];
int rev[N], n;
int len, lim = 1;
void init(int n)
{
    lim = 1; len = 0;
    while(lim <= n) lim <<= 1, len++;
    for (int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len-1));
}
ll _pow(ll a, ll b) {
    //....
}
inline void calcrev(int logn)
{
    rev[0]=0;
    for(int i = 1; i < (1 << logn); i++)
        rev[i]=(rev[i>>1]>>1)|((i&1)<<(logn-1));
}
void NTT(ll *a, int led, int op)//op为1是FFT运算,-1是FFT逆运算
{
    int lim = 1 << led;
    //......

}
void cdq_fft(int l, int r, int logn)
{
    
    if (logn <= 0) return ;
    if (l >= n) return ;
    int mid = (l + r) >> 1;
    ll t = _pow(r - l, mod-2);
    cdq_fft(l, mid, logn - 1);
    calcrev(logn);
    memset(a + (r - l) / 2, 0, sizeof(ll) * (r - l) / 2);
    memcpy(a, f+l, sizeof(ll) * (r - l) / 2);
    memcpy(b, g, sizeof(ll) * (r - l) );
    NTT(a, logn, 1); NTT(b, logn, 1);
    for (int i = 0; i  < r - l; i++) a[i] = a[i] * b[i] % mod;
    NTT(a, logn, -1);
    for (int i = (r - l) / 2; i < r - l; i++)
        f[l + i] = (f[l+i] + a[i]) % mod;
    cdq_fft(mid, r, logn-1);
}
int main()
{
    cin >> n;
    init(n);
    for (int i = 1; i < n; i++) scanf("%lld", &g[i]);
    f[0] = 1;
    cdq_fft(0, lim, len);
    for (int i = 0; i < n; i++)
        printf("%lld ", (f[i] + mod) % mod);
    puts("");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值