多项式全家桶

记号与约定

F ( x ) = ∑ i = 0 f i x i F(x)=\sum\limits_{i=0} f_i x^i F(x)=i=0fixi 表示一个多项式,用 [ x n ] F ( x ) = f n [x^n]F(x)=f_n [xn]F(x)=fn 表示 F ( x ) F(x) F(x) n n n 项系数。

n n n 为最大的非零的 f n f_n fn,那么 F ( x ) F(x) F(x) 的次数为 n n n ,记作 deg ⁡ ( F ( x ) ) = n \deg(F(x))=n deg(F(x))=n,注意 n n n 次多项式有 n + 1 n+1 n+1 项。

多项式加法: F ( x ) + G ( x ) = ∑ i = 0 ( f i + g i ) x i F(x)+G(x)=\sum\limits_{i=0}(f_i+g_i)x^i F(x)+G(x)=i=0(fi+gi)xi
多项式乘法: F ( x ) × G ( x ) = ∑ i = 0 ( ∑ j = 0 i f j g i − j ) x i F(x)\times G(x)=\sum\limits_{i=0}(\sum\limits_{j=0}^if_jg_{i-j})x^i F(x)×G(x)=i=0(j=0ifjgij)xi

一、多项式乘法

一切的基础

我们希望快速计算两个多项式 F F F G G G 的卷积

考虑对于固定的 x 0 x_0 x0 F ( x 0 ) G ( x 0 ) = ( F × G ) ( x 0 ) F(x_0)G(x_0)=(F\times G)(x_0) F(x0)G(x0)=(F×G)(x0)

因为 n + 1 n+1 n+1 个值可以确定一个 n n n 次多项式,那么我们构造 x 0 ∼ x n x_0\sim x_n x0xn,分别求出 F ( x 0 ) , F ( x 1 ) , . . . , F ( x n ) F(x_0),F(x_1),...,F(x_n) F(x0),F(x1),...,F(xn) G ( x 0 ) , G ( x 1 ) , . . . , G ( x n ) G(x_0),G(x_1),...,G(x_n) G(x0),G(x1),...,G(xn),这也称为点值表示法。

那么只需要求出所有 ( F × G ) ( x i ) = F ( x i ) G ( x i ) (F\times G)(x_i)=F(x_i)G(x_i) (F×G)(xi)=F(xi)G(xi),然后就能插值还原。

复杂度瓶颈在于求点值表示和还原多项式。

方便起见我们认为项数为 2 k 2^k 2k f f f 长度是 2 2 2 的幂,这个可以通过在 F F F G G G 的高项补 0 0 0 系数实现,下面令 n = 2 k n=2^k n=2k

1.1 快速傅里叶变换(FFT)

快速傅里叶变换

用来处理一般的卷积

考虑构造 n n n 次单位根 x i = w n i x_i=w_n^{i} xi=wni 作为点值,因为单位根有很好的性质:

  • w 2 n 2 i = w n i w_{2n}^{2i}=w_n^i w2n2i=wni
  • w n i + n / 2 = − w n i w_{n}^{i+n/2}=-w_n^i wni+n/2=wni

从复数的几何意义角度不难理解

那么 ∀ k = 0 , 1 … n 2 − 1 \forall k=0,1\dots \frac{n}{2}-1 k=0,12n1

F ( w n k ) = ∑ i = 0 n − 1 f i ( w n k ) i F(w_n^k)=\sum\limits_{i=0}^{n-1}f_i(w_n^k)^i F(wnk)=i=0n1fi(wnk)i

= ∑ i = 0 n 2 − 1 f 2 i ( w n k ) 2 i + ∑ i = 0 n 2 − 1 f 2 i + 1 ( w n k ) 2 i + 1 =\sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i}(w_n^k)^{2i} + \sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i+1}(w_n^k)^{2i+1} =i=02n1f2i(wnk)2i+i=02n1f2i+1(wnk)2i+1

= ∑ i = 0 n 2 − 1 f 2 i ( w n 2 k ) i + w n k ∑ i = 0 n 2 − 1 f 2 i + 1 ( w n 2 k ) i =\sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i}(w_n^{2k})^{i} + w_n^k\sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i+1}(w_n^{2k})^{i} =i=02n1f2i(wn2k)i+wnki=02n1f2i+1(wn2k)i

= ∑ i = 0 n 2 − 1 f 2 i ( w n / 2 k ) i + w n k ∑ i = 0 n 2 − 1 f 2 i + 1 ( w n / 2 k ) i =\sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i}(w_{n/2}^{k})^{i} + w_n^k\sum\limits_{i=0}^{\frac{n}{2}-1}f_{2i+1}(w_{n/2}^{k})^{i} =i=02n1f2i(wn/2k)i+wnki=02n1f2i+1(wn/2k)i

F 0 , F 1 F_0,F_1 F0,F1 分别为 F F F 中只保留偶数、奇数项系数的多项式:

F ( w n k ) = F 0 ( w n / 2 k ) + w n k F 1 ( w n / 2 k ) F(w_n^k)=F_0(w_{n/2}^k)+w_n^kF_1(w_{n/2}^k) F(wnk)=F0(wn/2k)+wnkF1(wn/2k)

同理可以得到:

F ( w n k + n / 2 ) = F 0 ( w n / 2 k ) − w n k F 1 ( w n / 2 k ) F(w_n^{k+n/2})=F_0(w_{n/2}^k)-w_n^kF_1(w_{n/2}^k) F(wnk+n/2)=F0(wn/2k)wnkF1(wn/2k)

我们只需要递归求出 F 0 ( x 0 ∼ n / 2 − 1 ) F_0(x_{0\sim n/2-1}) F0(x0n/21) F 1 ( x 0 ∼ n / 2 − 1 ) F_{1}(x_{0\sim n/2-1}) F1(x0n/21) 即可。

这样复杂度是 T ( n ) = T ( n / 2 ) + O ( n ) = O ( n log ⁡ n ) T(n)=T(n/2)+O(n)=O(n\log n) T(n)=T(n/2)+O(n)=O(nlogn)

快速傅里叶逆变换

求出了所有点值后,我们还需要还原系数

快速傅里叶变换的过程可以看成是:

[ 1 1 … 1 1 1 w n … w n n − 2 w n n − 1 … 1 w n n − 1 … w n ( n − 1 ) ( n − 2 ) w n ( n − 1 ) ( n − 1 ) ] [ f 0 f 1 … f n − 1 ] = [ y 0 y 1 … y n − 1 ] \begin{bmatrix} 1 & 1 & \dots & 1&1\\ 1 & w_n & \dots &w_n^{n-2} & w_n^{n-1}\\ \dots \\ 1 & w_n^{n-1}& \dots &w_n^{(n-1)(n-2)} & w_n^{(n-1)(n-1)} \\ \end{bmatrix} \begin{bmatrix} f_0 \\ f_1 \\ \dots \\ f_{n-1} \end{bmatrix}= \begin{bmatrix} y_0 \\ y_1 \\ \dots \\ y_{n-1} \end{bmatrix} 1111wnwnn11wnn2wn(n1)(n2)1wnn1wn(n1)(n1) f0f1fn1 = y0y1yn1

那么我们想要还原系数只需要乘上左边矩阵的逆即可

这个矩阵被称为 “范德蒙德矩阵”,它的逆很特殊,为:

1 n [ 1 1 … 1 1 1 1 / w n … 1 / w n n − 2 1 / w n n − 1 … 1 1 / w n n − 1 … 1 / w n ( n − 1 ) ( n − 2 ) 1 / w n ( n − 1 ) ( n − 1 ) ] \frac{1}{n}\begin{bmatrix} 1 & 1 & \dots & 1&1\\ 1 & 1/w_n & \dots & 1/w_n^{n-2} & 1/w_n^{n-1}\\ \dots \\ 1 & 1/w_n^{n-1}& \dots & 1/w_n^{(n-1)(n-2)} & 1/w_n^{(n-1)(n-1)} \\ \end{bmatrix} n1 11111/wn1/wnn111/wnn21/wn(n1)(n2)11/wnn11/wn(n1)(n1)

即每项分别取倒数,然后再乘上 1 / n 1/n 1/n,那么只需要将上面过程中的所有 w n w_n wn 替换成 1 / w n 1/w_n 1/wn 即可,具体实现我们可以只写一个 FFT 函数,然后传一个 opt 代表是 w n w_n wn 还是 1 / w n 1/w_n 1/wn

蝴蝶变换

递归版常数较大,通常我们会使用循环写法来代替

注意到我们实际上的运算过程是从下至上依次合并的,那么只需要求出最底层的各项 f 0 ′ , f 1 ′ … f n − 1 ′ f'_0,f'_1\dots f'_{n-1} f0,f1fn1 分别对应原来的哪一项

注意到 f i ′ = f r e v ( i ) f_i'=f_{rev(i)} fi=frev(i),其中 r e v ( i ) rev(i) rev(i) 代表 i i i 的二进制高低位倒置后的数这步操作叫做蝴蝶变换

变换后按照分治的过程用循环实现即可

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 4e6+10 ;
const double Pi = acos(-1.0) ;

int n , m , r[N] ;
struct complx
{
	double a , b ;
	complx (double xx=0, double yy=0 ) {
		a = xx , b = yy ; 
	}
	friend complx operator + ( const complx x , const complx y ) {
		return {x.a+y.a,x.b+y.b} ;
	}
	friend complx operator - ( const complx x , const complx y ) {
		return {x.a-y.a,x.b-y.b} ;
	}
	friend complx operator * ( const complx x , const complx y ) {
		return {x.a*y.a-x.b*y.b,x.a*y.b+x.b*y.a} ;
	}
}F[N] , G[N] ;
void FFT( complx *a , int l , int opt ) // [0,2^l)
{
	for(int i = 0 ; i < (1<<l) ; i ++ ) {
		if( i<r[i] ) swap(a[i],a[r[i]]) ;
	}
	for(int i = 1 ; i <= l ; i ++ ) {
		int p = (1<<(i-1)) ;
		complx w1(cos(Pi/p),opt*sin(Pi/p)) ;
		for(int j = 0 ; j < (1<<l) ; j += (1<<i) ) {
			complx w(1,0) ;
			for(int k = 0 ; k < p ; k ++ , w=w*w1 ) {
				complx A = a[j+k] , B = w*a[j+k+p] ;
				a[j+k] = A+B ;
				a[j+k+p] = A-B ;
			}
		}
	}
}

int main()
{
	scanf("%d%d" , &n , &m ) ;
	int x ;
	for(int i = 0 ; i <= n ; i ++ ) {
		scanf("%d" , &x ) ;
		F[i].a = x ;
	}
	for(int i = 0 ; i <= m ; i ++ ) {
		scanf("%d" , &x ) ;
		G[i].a = x ;
	}
	int len = 1 , l = 0 ;
	while( len<n+m+1 ) len<<=1 , l ++ ;
	for(int i = 0 ; i < len ; i ++ ) r[i] = (r[i>>1]>>1)|((i&1)<<(l-1)) ;  
	FFT(F,l,1) ; FFT(G,l,1) ;
	for(int i = 0 ; i < len ; i ++ ) F[i] = F[i]*G[i] ;
	FFT(F,l,-1) ;
	for(int i = 0 ; i <= n+m ; i ++ ) {
		printf("%d " , int(F[i].a/len+0.5) ) ;
	}
	return 0 ;
}

1.2 快速数论变换 (NTT)

FFT 在精度和常数等方面有很大的问题,并不十分常用

而当我们把数域限定为 Z p \Z_p Zp { 0 , 1 , 2 … p − 1 } \{0,1,2\dots p-1\} {0,1,2p1} p p p 为素数)时,就有一个更常用的等效算法:快速数论变换

首先我们需要找到一种能够代替单位根 w n w_n wn 的东西

考虑对于一个非零数 g g g g , g 2 , g 3 … g,g^2,g^3\dots g,g2,g3   m o d   p \bmod {p} modp 意义下一定存在循环,如果循环节为 p − 1 p-1 p1,我们就称 g g g p p p 的一个原根,所有素数均有原根。

已知 w n n = 1 w_n^n=1 wnn=1 g p − 1 = 1 g^{p-1}=1 gp1=1,那么我们就考虑用 g ( p − 1 ) / n g^{(p-1)/n} g(p1)/n 来代替 n n n 次单位根 w n i w_n^i wni,很容易说明这个东西有我们需要的单位根的所有性质。

注意这里 n n n 是我们补齐后的 2 2 2 的整次幂,需要保证 n ∣ p − 1 n|p-1 np1(也就是 p − 1 p-1 p1 的分解中 2 2 2 的指数比较大),这样的模数 p p p 也被称为 NTT 模数,最常见的就是 998244353 = 1 + 2 23 × 119 998244353=1+2^{23}\times119 998244353=1+223×119,其原根 g = 3 g=3 g=3

w n 1 = g ( m o d − 1 ) / n w_n^1=g^{(mod-1)/n} wn1=g(mod1)/n

怎么找原根呢?见 oi-wiki

原根判定定理

m ≥ 3 m\geq 3 m3,则 g g g 是模 m m m 意义下的原根的充要条件是,对于 φ ( m ) \varphi(m) φ(m) 的每个素因子 p p p,都有 g φ ( m ) p ≠ 1 ( m o d m ) g^{\frac{\varphi(m)}{p}}\ne1\pmod m gpφ(m)=1(modm)

这样我们就可以暴力枚举 + check 找到某个模数的原根了

写法和 FFT 几乎没有区别

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 4e6+100 , mod = 998244353 , G = 3 , invG = (mod+1)/G ; 
LL ksm( LL a , LL b )
{
	LL res = 1 , t = a % mod ;
	while( b ) {
		if( b&1 ) res = res * t % mod ;
		b = b >> 1 ;
		t = t * t % mod ;
	}
	return res ;
}

int n , m , r[N] ;
LL F[N] , H[N] ;
void NTT( LL *a , int l , bool fg )
{
	for(int i = 0 ; i < (1<<l) ; i ++ ) {
		if( i < r[i] ) swap(a[i],a[r[i]]) ;
	}
	for(int i = 1 ; i <= l ; i ++ ) {
		int p = (1<<(i-1)) ;
		LL w1 = ksm( fg?invG:G , (mod-1)/(1<<i) ) ;
		for(int j = 0 ; j < (1<<l) ; j += (1<<i) ) {
			LL w = 1 ;
			for(int k = 0 ; k < p ; k ++ , w=w*w1%mod ) {
				LL B = a[j+k+p]*w%mod ;
				a[j+k+p] = ( a[j+k]-B ) % mod ;
				a[j+k] = ( a[j+k] + B ) % mod ; 
			}
		}
	}
}

int main()
{
	scanf("%d%d" , &n , &m ) ;
	for(int i = 0 ; i <= n ; i ++ ) {
		scanf("%lld" , &F[i] ) ;
	}
	for(int i = 0 ; i <= m ; i ++ ) {
		scanf("%lld" , &H[i] ) ;
	}
	int len = 1 , l = 0 ;
	while( len < n+m+1 ) len<<=1 , l ++ ;
	for(int i = 0 ; i < len ; i ++ ) r[i] = (r[i>>1]>>1)|((i&1)<<(l-1)) ;
	NTT(F,l,0) ; NTT(H,l,0) ;
	for(int i = 0 ; i < len ; i ++ ) F[i] = F[i]*H[i]%mod ;
	NTT(F,l,1) ;
	LL iv = ksm(len,mod-2) ;
	for(int i = 0 ; i <= n+m ; i ++ ) {
		printf("%lld " , (F[i]*iv%mod+mod)%mod ) ;
	}
	return 0 ;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值