快速傅里叶变换

FFT

最后的最后还是回到了这个玩意上…

前置

单位根

记为 w n w_n wn ,表示将圆 n n n 等分后的第一块对应的复数.

于是我们可以写出 w n = cos ⁡ 2 π n + i sin ⁡ 2 π n w_n=\cos \frac{2\pi}{n}+i\sin \frac{2\pi}{n} wn=cosn2π+isinn2π .

同样可以定义 k k k 次方,根据复数的三角运算可知:

w n k = cos ⁡ k × 2 π n + i sin ⁡ k × 2 π n w_n^k=\cos k\times \frac{2\pi}{n}+i\sin k\times \frac{2\pi}{n} wnk=cosk×n2π+isink×n2π .

  1. w 2 n 2 k = w n k w_{2n}^{2k}=w_n^{k} w2n2k=wnk .

注意到 w 2 n 2 k = cos ⁡ 2 k × 2 π 2 n + i sin ⁡ 2 k × 2 π 2 n = cos ⁡ k × 2 π n + i sin ⁡ k × 2 π n w_{2n}^{2k}=\cos 2k\times \frac{2\pi}{2n}+i\sin 2k\times \frac{2\pi}{2n}=\cos k\times \frac{2\pi}{n}+i\sin k\times \frac{2\pi}{n} w2n2k=cos2k×2n2π+isin2k×2n2π=cosk×n2π+isink×n2π .

  1. w n k + n 2 = w n k w_n^{k+\frac{n}{2}}=w_{n}^{k} wnk+2n=wnk .

显然 w n n 2 = cos ⁡ n 2 × 2 π n + i sin ⁡ n 2 × 2 π n = − 1 + 0 = − 1 w_{n}^{\frac{n}{2}}=\cos \frac{n}{2}\times \frac{2\pi}{n}+i\sin \frac{n}{2}\times \frac{2\pi}{n}=-1+0=-1 wn2n=cos2n×n2π+isin2n×n2π=1+0=1 ,乘起来即可.

快速傅里叶变换(FFT)

这一步需要我们将系数表达式变成点值表达式.

F ( x ) = ∑ k = 0 n a k x k F(x)=\sum\limits_{k=0}^n a_kx^k F(x)=k=0nakxk 为我们要处理的 n n n 次多项式,我们的操作是将他们的系数奇偶分类:

F ( x ) = ( a 0 + a 2 x 2 + … ) + x ( a 1 + a 3 x 2 + … ) F(x)=(a_0+a_2x^2+\ldots)+x(a_1+a_3x^2+\ldots) F(x)=(a0+a2x2+)+x(a1+a3x2+) .

注意到后面我们设成新的两个多项式 A ( x ) A(x) A(x) B ( x ) B(x) B(x) ,则整体就可以写成:

F ( x ) = A ( x 2 ) + x B ( x 2 ) F(x)=A(x^2)+xB(x^2) F(x)=A(x2)+xB(x2) .

w n k w_n^k wnk 扔进去得到:

F ( w n k ) = A ( w n 2 k ) + w n k B ( w n 2 k ) F(w^k_n)=A(w_{\frac{n}{2}}^{k})+w_{n}^{k}B(w_{\frac{n}{2}}^k) F(wnk)=A(w2nk)+wnkB(w2nk) .

然后还有一个:

F ( w n k + n 2 ) = A ( w n 2 k ) − w n k B ( w n 2 k ) F(w_{n}^{k+\frac{n}{2}})=A(w_{\frac{n}{2}}^k)-w_{n}^{k}B(w_\frac{n}{2}^k) F(wnk+2n)=A(w2nk)wnkB(w2nk) .

所以我们在处理 k ∈ [ 0 , n 2 ] k\in [0,\frac{n}{2}] k[0,2n] 的时候可以顺便把 k ∈ ( n 2 , n ] k\in (\frac{n}{2},n] k(2n,n] 的给弄出来.

递归处理一边即可,时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn) .

至于怎么写,一下给出参考代码(不保证能过编译.

struct complex{
    double x , y;
    complex (double xx = 0 , double yy = 0){x = xx , y = yy;}
} a[MAXN] , b[MAXN] , tmp[MAXN];
/*
math operation
*/
void fft(complex *A , int l , int r)
{
    if(l == r)
    {
        
	}
    int cur = 0 , h = l , t = (l + r >> 1) + 1;
    for(int i = l ; i <= r ; i++)
    {
        if((i - l) & 1) tmp[t++] = A[i];
        else tmp[l++] = A[i];
	}
    fft(A , l , l + r >> 1);
    fft(A , (l + r >> 1) + 1 , r);
    for(int i = l ; i <= r ; i++)
    {
        /*
        fft operation
        */
	}
}

现在转成了点值表达式,但我需要的是系数表达式啊.

所以还有快速傅里叶逆变换.

快速傅里叶逆变换(IFFT)

我们现在得到了点值表达 ( y 0 , y 1 , … , y n − 1 ) (y_0,y_1,\ldots,y_{n-1}) (y0,y1,,yn1) ,构造另一组向量 ( c 0 , c 1 , … , c n − 1 ) (c_0,c_1,\ldots,c_{n-1}) (c0,c1,,cn1) 使其满足:

c k = ∑ i = 0 n − 1 y i ( w n − k ) i c_k=\sum\limits_{i=0}^{n-1}y_i(w_n^{-k})^i ck=i=0n1yi(wnk)i ,即将 c k c_k ck 看成以 ( y 0 , y 1 … , y n − 1 ) (y_0,y_1\ldots,y_{n-1}) (y0,y1,yn1)系数表达的多项式 B ( x ) B(x) B(x) ,其取 x = w n − k x=w_n^{-k} x=wnk 时的值.

至于这是干什么的,先展开化简:

c k = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( w n i ) j ) ( w n − k ) i c_k=\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j(w_{n}^{i})^j)(w_{n}^{-k})^i ck=i=0n1(j=0n1aj(wni)j)(wnk)i

= ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( w n j ) i ( w n − k ) i ) =\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j(w_{n}^j)^i(w_{n}^{-k})^i) =i=0n1(j=0n1aj(wnj)i(wnk)i)

= ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( w n j − k ) i =\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{n-1}a_j(w_{n}^{j-k})^i =i=0n1j=0n1aj(wnjk)i

= ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( w n j − k ) i ) =\sum\limits_{j=0}^{n-1}a_j(\sum\limits_{i=0}^{n-1}(w_n^{j-k})^i) =j=0n1aj(i=0n1(wnjk)i) .

S ( x ) = ∑ i = 0 n − 1 x i S(x)=\sum\limits_{i=0}^{n-1}x^i S(x)=i=0n1xi .

那上面也可以写成:

c k = ∑ j = 0 n − 1 a j S ( w n j − k ) c_k=\sum\limits_{j=0}^{n-1}a_jS(w_n^{j-k}) ck=j=0n1ajS(wnjk) .

S ( w n k ) = 1 + w n k + ( w n k ) 2 + ⋯ + ( w n k ) n − 1 S(w_n^k)=1+w_n^k+(w_n^{k})^2+\cdots+(w_n^{k})^{n-1} S(wnk)=1+wnk+(wnk)2++(wnk)n1 .

发挥人类智慧,当 k ≠ 0 k\not= 0 k=0 时,同时乘一个 w n k w_n^k wnk 调项得到:

( 1 − w n k ) S ( w n k ) = 1 − ( w n k ) n (1-w_{n}^k)S(w_n^k)=1-(w_n^k)^n (1wnk)S(wnk)=1(wnk)n .

S ( w n k ) = 1 − 1 1 − w n k = 0 S(w_n^k)=\frac{1-1}{1-w_n^k}=0 S(wnk)=1wnk11=0 .

而当 k = 0 k=0 k=0 时直接得到:

S ( 1 ) = n S(1)=n S(1)=n .

所以后面那一坨可以扔掉,原式子写成:

c k = a k n c_k=a_kn ck=akn .

a k = c k n a_k=\frac{c_k}{n} ak=nck ,至此我们实现了点值到系数的转化.

实现把 w n k w_n^k wnk 换成 w n − k w_n^{-k} wnk 即可,其余和 FFT \text{FFT} FFT 是一样的.


实现常数太大T了怎么办?

我们又充分发挥人类智慧,观察 FFT \text{FFT} FFT 的过程,操作前相当于将系数按一个次序排列,操作的时候则相当于直接覆盖(专业术语蝴蝶变换.

于是我们找到这个规律即可,由观察法,记 p ( x ) p(x) p(x) 表示系数下标 x x x 的最终下标,则

p ( x ) p(x) p(x) 等价于将 x x x 二进制翻转后的值.

预处理这货即可:

int l = 0 , lmt = 1 , p[N];
int main()
{
    while(lmt <= N + M) lmt <<= 1 , l++;
    for(int i = 0 ; i <= lmt ; i++)
        p[i] = (p[i >> 1] >> 1) | ((i & 1) << (l - 1));
}

FFT \text{FFT} FFT 就这么写:

/*
structrue definition and complex math operation
*/
const double Pi = acos(-1.0);
void fft(complex *A , int type)
{
    for(int i = 0 ; i <= lmt ; i++)
        if(i < p[i]) swap(A[i] , A[p[i]]);
   	for(int mid = 1 ; mid < lmt ; mid <<= 1)//这是待合并的区间的一半
    {
        complex Wn(cos(pi / mid) , type * sin(pi / (mid << 1)));
        for(int R = mid << 1 , j = 0 ; j <= lmt ; j += R)//R是合并区间的总长度,j用来跳段
        {
            complex w(1 , 0);
            for(int k = 0 ; k < mid ; k++ , w = w * Wn)//枚举左半部分
            {
                complex x = A[j + k] , y = w * A[j + mid + k];//人类智慧
                A[j + k] = x + y; A[j + mid + k] = x - y;
            }
        }
    }
}

于是 FFT \text{FFT} FFT 就介绍完了.


然而,信竞里边设计卷积计算方案数的基本都要带模数.

而涉及模数,就意味着会乘爆.

所以普通 FFT \text{FFT} FFT 是不够的,需要用上数论快速傅里叶变换(NTT),最终目的即摆脱单位根.

于是我们用上了原根…

前置

对于 ( a , p ) = 1 (a,p)=1 (a,p)=1 ,定义 a n ≡ 1 ( m o d    p ) a^n\equiv 1(\mod p) an1(modp) 的最小正整数 n n n a a a 在模 p p p 意义下的阶,记作 δ p ( a ) \delta_p(a) δp(a) .

例如 δ 7 ( 3 ) = 6 \delta_7(3) = 6 δ7(3)=6 .

3 m o d    7 = 3 3\mod 7=3 3mod7=3 .

3 2 m o d    7 = 2 3^2\mod 7=2 32mod7=2 .

3 3 m o d    7 = 6 3^3\mod 7=6 33mod7=6 .

3 4 m o d    7 = 4 3^4\mod 7=4 34mod7=4 .

3 5 m o d    7 = 5 3^5\mod 7=5 35mod7=5 .

3 6 m o d    7 = 1 3^6\mod 7=1 36mod7=1 .

原根
Definition

p p p 为正整数, a a a 为整数,若 δ p ( a ) = ϕ ( p ) \delta_p(a)=\phi(p) δp(a)=ϕ(p) ,则称 a a a 为模 p p p 意义下的一个原根.

由上方知 δ 7 ( 3 ) = ϕ ( 7 ) = 6 \delta_7(3)=\phi(7)=6 δ7(3)=ϕ(7)=6 ,所以 3 3 3 是模 7 7 7 意义下的一个原根.

其有一个性质:

P P P 为一个素数,假设一个数 g g g P P P 的原根,那么 g i m o d    P g^i\mod P gimodP 的结果两两不同.

完全剩余系的基本定义.

再者,我们发现 g k g^k gk 的阶恰好等于 p − 1 gcd ⁡ ( k , p − 1 ) \frac{p-1}{\gcd(k,p-1)} gcd(k,p1)p1并且其为 p − 1 p-1 p1 的约数.

于是对于形如 n = 2 k n=2^k n=2k 的, p p p 998244353 = 2 23 × 7 × 17 + 1 998244353=2^{23}\times 7\times 17 + 1 998244353=223×7×17+1 就能基本实现自律.

此时得到 k = p − 1 n k=\frac{p-1}{n} k=np1 ,对应原根 g = 3 g=3 g=3 .

至于怎么摆脱原根…只能祈祷出题人不会这么阴间了.

#define ll long long
const int p = 998244353 , G = 3;
inline int fpow(int x , int y = p - 2)
{
    int ans = 1;
    for(; y ; y >>= 1 , x = 1ll * x * x % p) if(y & 1) ans = 1ll * ans * x % p;
    return ans;
}
const int invG = fpow(G);
void NTT(ll *A , int type)
{
    for(int i = 0 ; i < lmt ; i++)
        if(i < r[i]) swap(A[i] , A[r[i]]);
    for(int mid = 1 ; mid < lmt ; mid <<= 1)
    {
        ll Wn = fpow(type == 1 ? G : invG , (p - 1) / (mid << 1));
        for(int R = (1 << mid) , j = 0 ; j <= lmt ; j += R)
        {
            ll w = 1;
            for(int k = 0 ; k < mid ; k++ , w = 1ll * w * Wn % mod)
            {
                ll x = A[j + k] , y = w * A[j + mid + k] % p;
                A[j + k] = (x + y) % p; A[j + mid + k] = (x - y) % p;
            }
        }
	}
}

以及提供一个封装库:

namespace Poly
{
	#define ll long long
	const int p = 998244353 , G = 3;
	inline ll fpow(ll x , ll y = p - 2)
	{
		ll ans = 1;
		for(; y ; y >>= 1 , x = x * x % p) if(y & 1) ans = ans * x % p;
		return ans;
	}
	const ll invG = fpow(G);
	int r[N] , l , lmt = 1;
	inline void prerev(int n , int m)
	{
		while(lmt <= n + m) l++ , lmt <<= 1;
		for(int i = 0 ; i <= lmt ; i++)
			r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
	}
	inline void NTT(ll *A , int type)
	{
		for(int i = 0 ; i <= lmt ; i++)
			if(i < r[i]) swap(A[i] , A[r[i]]);
		for(int mid = 1 ; mid < lmt ; mid <<= 1)
		{
			ll Wn = fpow(type == 1 ? G : invG , (p - 1ll) / (mid << 1));
			for(int R = mid << 1 , j = 0 ; j < lmt ; j += R)
			{
				ll w = 1;
				for(int k = 0 ; k < mid ; k++ , w = w * Wn % p)
				{
					ll 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;
				}
			}
		}
	}
	inline void solve(ll *a , ll *b , int n , int m)
	{
		prerev(n , m);
		NTT(a , 1); NTT(b , 1);
		for(int i = 0 ; i <= lmt ; i++) a[i] = a[i] * b[i] % p;
		NTT(a , -1);
		ll inv = fpow(lmt);
		for(int i = 0 ; i <= lmt ; i++) a[i] = (a[i] * inv) % p;
	}
}
int main()
{
    /*
    */
    Poly::NTT(a , b , n , m);
    /*
    */
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值