多项式乘法

FFT主要用于快速求多项式的乘积。多项式的乘积就叫做卷积

F F F G G G来说,显然暴力算法的复杂度是 O ( n m ) O(nm) O(nm),而FFT的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

多项式的性质:用任意 n + 1 n+1 n+1个横坐标不同的点,可以唯一确定一个 n n n次多项式。这个性质叫做多项式的点表示法

证明:设这个多项式 f = a n x n + a n − 1 x n − 1 + . . . + a 1 x + a 0 f=a_nx^n+a_{n-1}x^{n-1}+...+a_1x+a_0 f=anxn+an1xn1+...+a1x+a0,那么我们代入 n + 1 n+1 n+1个点可以获得 n + 1 n+1 n+1个方程,这是 n + 1 n+1 n+1元一次方程,写出系数行列式就会发现是范德蒙德行列式,由于横坐标不同,所以行列式的值不为 0 0 0,于是方程有唯一解

H = F G H=FG H=FG,于是 H H H n + m n+m n+m次多项式,我们选取 n + m + 1 n+m+1 n+m+1个点代入 F , G F,G F,G之后即可确定 H H H。这就是点表示法的好处,时间复杂度从 O ( n m ) O(nm) O(nm)降低到了 O ( n + m ) O(n+m) O(n+m)

于是我们现在要解决的问题是如何快速将一个多项式的系数表示法(也就是我们通常写出来的多项式)与点表示法互相转换

下文的 f ( x ) = a n − 1 x n − 1 + . . . + a 1 x + a 0 f(x)=a_{n-1}x^{n-1}+...+a_1x+a_0 f(x)=an1xn1+...+a1x+a0

系数表示法转点表示法:我们要在取 n + 1 n+1 n+1个特殊点上下功夫,我们取复数域上的单位根

假设 n = 2 c n=2^c n=2c(如果不是的话我们补齐),画出复数域上的单位圆:

image

w n k w_n^k wnk表示将单位圆等分成 n n n份,将 x x x轴逆时针旋转 2 π k n \frac{2πk}{n} n2πk所得到的复数,其中 k k k的取值为 [ 0 , n − 1 ] [0,n-1] [0,n1]。比如上图, w n 0 = 1 w_n^0=1 wn0=1 w n 3 = − i w_n^3=-i wn3=i

w n k w_n^k wnk具有如下的性质:

1. ∀ i ≠ j , w n i ≠ w n j \forall i\neq j,w_n^i\neq w_n^j i=j,wni=wnj

2. w n k = cos ⁡ ( 2 k π n ) + i sin ⁡ ( 2 k π n ) w_n^k=\cos(\frac{2kπ}{n})+i\sin(\frac{2kπ}{n}) wnk=cos(n2)+isin(n2)

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

4. w n k + n 2 = − w n k w_n^{k+\frac{n}{2}}=-w_n^k wnk+2n=wnk

现在,我们将 f f f奇数项和偶数项分开,有 f = g + h f=g+h f=g+h,其中 g ( x ) = a n − 2 x n − 2 + a n − 4 x n − 4 + . . . + a 2 x 2 + a 0 , h = a n − 1 x n − 1 + a n − 3 x n − 3 + . . . + a 1 x g(x)=a_{n-2}x^{n-2}+a_{n-4}x^{n-4}+...+a_2x^2+a_0,h=a _ {n-1} x ^ {n-1} + a _ {n-3}x^{n-3}+...+a_1x g(x)=an2xn2+an4xn4+...+a2x2+a0,h=an1xn1+an3xn3+...+a1x

g g g中每个 x x x x 2 x^2 x2代替可设 φ 1 ( x ) = a n − 2 x n 2 − 1 + a n − 4 x n 2 − 2 + . . . + a 2 x + a 0 φ_1(x)=a_{n-2}x^{\frac{n}{2}-1}+a_{n-4}x^{\frac{n}{2}-2}+...+a_2x+a_0 φ1(x)=an2x2n1+an4x2n2+...+a2x+a0,将 h h h提取一个 x x x并将剩下的式子的 x x x x 2 x^2 x2代替可得 φ 2 ( x ) = a n − 1 x n 2 − 1 + a n − 3 x n 2 − 2 + . . . + a 1 φ_2(x)=a_{n-1}x^{\frac{n}{2}-1}+a_{n-3}x^{\frac{n}{2}-2}+...+a_1 φ2(x)=an1x2n1+an3x2n2+...+a1,于是 f ( x ) = φ 1 ( x 2 ) + x φ 2 ( x 2 ) f(x)=φ_1(x^2)+xφ_2(x^2) f(x)=φ1(x2)+xφ2(x2)

现在,设 k ∈ [ 0 , n 2 − 1 ] k∈[0,\frac{n}{2}-1] k[0,2n1],则 f ( w n k ) = φ 1 ( w n 2 k ) + w n k φ 2 ( w n 2 k ) = φ 1 ( w n 2 k ) + w n k φ 2 ( w n 2 k ) , f ( w n k + n 2 ) = φ 1 ( w n 2 k ) − w n k φ 2 ( w n 2 k ) = φ 1 ( w n 2 k ) − w n k φ 2 ( w n 2 k ) f(w_n^k)=φ_1(w_n^{2k})+w_n^kφ_2(w_n^{2k})=φ_1(w_{\frac{n}{2}}^{k})+w_n^kφ_2(w_{\frac{n}{2}}^{k}),f(w_n^{k+\frac{n}{2}})=φ_1(w_n^{2k})-w_n^kφ_2(w_n^{2k})=φ_1(w_{\frac{n}{2}}^{k})-w_n^kφ_2(w_{\frac{n}{2}}^{k}) f(wnk)=φ1(wn2k)+wnkφ2(wn2k)=φ1(w2nk)+wnkφ2(w2nk),f(wnk+2n)=φ1(wn2k)wnkφ2(wn2k)=φ1(w2nk)wnkφ2(w2nk)

也就是说我们要求出将 w n 0 , w n 1 , . . . , w n n − 1 w_n^0,w_n^1,...,w_n^{n-1} wn0,wn1,...,wnn1代入 f f f的值,只需要递归求解 φ 1 , φ 2 φ_1,φ_2 φ1,φ2即可;一共会划分 O ( log ⁡ n ) O(\log n) O(logn)层,每层的总复杂度为 O ( n ) O(n) O(n),所以时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

点表示法转换为系数表示法:设我们现在已经知道了 ( w n 0 , f ( w n 0 ) ) , . . . , ( w n n − 1 , f ( w n n − 1 ) ) (w_n^0,f(w_n^0)),...,(w_n^{n-1},f(w_n^{n-1})) (wn0,f(wn0)),...,(wnn1,f(wnn1)),我们要求 f ( x ) f(x) f(x),则有 a k = ∑ i = 0 n − 1 f ( w n i ) ( w n − k ) i n a_k=\frac{\sum_{i=0}^{n-1}f(w_n^{i})(w_n^{-k})^i}{n} ak=ni=0n1f(wni)(wnk)i

证明:
∑ i = 0 n − 1 f ( w n i ) ( w n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( w n i ) j ) ( w n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( w n j ) i ) ( w n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( w n j − k ) i = ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( w n j − k ) i = n a k \sum_{i=0}^{n-1}f(w_n^{i})(w_n^{-k})^i\\=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(w_n^{i})^j)(w_n^{-k})^i\\=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(w_n^{j})^i)(w_n^{-k})^i\\=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(w_n^{j-k})^i\\=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}(w_n^{j-k})^i\\=na_k i=0n1f(wni)(wnk)i=i=0n1(j=0n1aj(wni)j)(wnk)i=i=0n1(j=0n1aj(wnj)i)(wnk)i=i=0n1j=0n1aj(wnjk)i=j=0n1aji=0n1(wnjk)i=nak
,最后一步成立是因为
∑ i = 0 n − 1 ( w n j − k ) i = { n , j = k 1 − ( w n j − k ) n 1 − w n j − k = 1 − ( w n n ) j − k 1 − w n j − k = 1 − 1 1 − w n j − k = 0 , j ≠ k \sum_{i=0}^{n-1}(w_n^{j-k})^i=\begin{cases} n,j=k \\ \frac{1-(w_n^{j-k})^n}{1-w_n^{j-k}}=\frac{1-(w_n^n)^{j-k}}{1-w_n^{j-k}}=\frac{1-1}{1-w_n^{j-k}}=0,j\neq k \end{cases} i=0n1(wnjk)i={n,j=k1wnjk1(wnjk)n=1wnjk1(wnn)jk=1wnjk11=0,j=k

于是我们设 ρ ( x ) = ∑ i = 0 n − 1 f ( w n i ) x i \rho(x)=\sum_{i=0}^{n-1}f(w_n^{i})x^{i} ρ(x)=i=0n1f(wni)xi,则我们求出 ρ ( w n 0 ) , ρ ( w n − 1 ) , . . . , ρ ( w n − ( n − 1 ) ) \rho(w_n^{0}),\rho(w_n^{-1}),...,\rho(w_n^{-(n-1)}) ρ(wn0),ρ(wn1),...,ρ(wn(n1))即可,这就是上文讲的傅里叶正变换(系数表示法转化为点表示法)

最后讲一下实现。实践证明,如果用递归来实现的话,常数是非常大的,所以我们一般利用迭代来实现

假设现在 n = 8 n=8 n=8,则每一层的划分如下

a 0   a 1   a 2   a 3   a 4   a 5   a 6   a 7 a 0   a 2   a 4   a 6   ∣   a 1   a 3   a 5   a 7 a 0   a 4 ∣ a 2   a 6 ∣ a 1   a 5 ∣ a 3   a 7 a 0 ∣ a 4 ∣ a 2 ∣ a 6 ∣ a 1 ∣ a 5 ∣ a 3 ∣ a 7 a_0\space a_1\space a_2\space a_3\space a_4\space a_5\space a_6\space a_7\\a_0\space a_2\space a_4\space a_6\space |\space a_1\space a_3\space a_5\space a_7\\a_0\space a_4|a_2\space a_6 |a_1\space a_5|a_3\space a_7\\a_0|a_4|a_2|a_6|a_1|a_5|a_3|a_7 a0 a1 a2 a3 a4 a5 a6 a7a0 a2 a4 a6  a1 a3 a5 a7a0 a4a2 a6a1 a5a3 a7a0a4a2a6a1a5a3a7

我们现在需要快速获得最后一行的顺序。观察,第一行的二进制位与最后一行对应的二进制位刚好为翻转关系。比如第一行的 a 4 a_4 a4对应最后一行的 a 1 a_1 a1,两者的二进制位分别是 100 100 100 001 001 001

证明:考虑一个数如何走到最后一层。从第一层到第二层,如果其末尾是 1 1 1,那么就会被放到右边,否则就会被放到左边;从第二层到第三层,如果其右移一位后末尾是 1 1 1那么就会被放到右边,否则就会被放到左边;依次类推,每次走完一层之后都会右移一位。于是一个数的每个二进制位决定了其每一步是向左走还是向右走。而每往右走一步,当前下标就会加上当前组数的个数的一半,向左走则不变。比如 a 5 a_5 a5,先向右走,而当前组(指第一层)有 8 8 8个数,于是下标加上 4 4 4,再向左走,下标不变,再向右走,而当前组(第三层第三组的{ a 1 , a 5 a_1,a_5 a1,a5})有两个数,于是下标加上 1 1 1,最终的下标就是 0 + 4 + 1 = 5 0+4+1=5 0+4+1=5。由上面的过程可知,每次下标加的数就是将当前二进制位放到翻转的位置。于是结论成立

代码见下

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10;
const double PI=acos(-1);
int n,m,len;
complex<double> y[3][N<<2];
int rev[N<<2];
void change(int op)//交换 
{
	for(int i=0;i<len;i++)
	if(i<rev[i]) swap(y[op][i],y[op][rev[i]]);//防止交换两次 
}
void fft(int op,int on)//像BFS一样从下往上一层一层地合并 
{
	change(op);
	for(int h=2;h<=len;h<<=1)//h为当前这一层的区间长度 
	{
		complex<double> wn(cos(2*PI/h),sin(on*2*PI/h));//将单位圆分成h份 
		for(int j=0;j<len;j+=h)//合并起始点 
		{
			complex<double> w(1,0);
			for(int k=j;k<j+h/2;k++)
			{
				complex<double> u=y[op][k],t=w*y[op][k+h/2];
				y[op][k]=u+t,y[op][k+h/2]=u-t;
				//蝴蝶变换,见OI-wiki 
				w*=wn;
			}
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n+1;i++)//千万注意这里要加一 
	{
		int a;
		scanf("%d",&a);
		complex<double> u(a,0);//complex是C++自带的一个类 
		y[0][i-1]=u;
	}
	for(int i=1;i<=m+1;i++)
	{
		int a;
		scanf("%d",&a);
		complex<double> u(a,0);
		y[1][i-1]=u;
	}
	len=n+m+1;
	for(int i=0;;i++)//补齐 
	if(len==(1<<i)) break;
	else if(len<(1<<i))
	{
		len=1<<i;
		break;
	}
	for(int i=0;i<len;i++)//这一步操作见OI-wiki的位逆序置换O(n)实现 
	{
		rev[i]=rev[i>>1]>>1;
		if(i&1) rev[i]|=len>>1;
	}
	fft(0,1),fft(1,1);
	for(int i=0;i<len;i++)
	y[2][i]=y[0][i]*y[1][i];
	fft(2,-1);
	for(int i=0;i<=n+m;i++)
	printf("%d ",(int)(real(y[2][i])/len+0.5));//这里都要这么写,不然有精度误差 
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值