【luogu P3803】【模板】多项式乘法(FFT)

【模板】多项式乘法(FFT)

题目链接:luogu P3803

题目大意

给你两个多项式,要你求这两个多项式乘起来得到的多项式。(卷积)

思路

系数表示法

就是我们一般来表示一个多项式的方法:
A(x)=a1xk+a2xk1+...+ak
或者可以这样表示:
A(x)=i=1kai×xi

那你很容易看到,用来做这道题用系数表示法来做是 O(n2) 的。

点值表示法

假设我们已经知道了这个多项式,那我们把 n 个不同的 x 带入,会得到 n 个取值 y
由于 n 个点可以确定一个 n 次多项式,那我们就可以根据这 n 个点确定这个多项式。

那你如果选了 n 个点 (x1,y1),...,(xn,yn),那就有:
yi=j=0n1aj×xij

但用它计算还是 O(n2),你选点 O(n),对于每个点计算也是 O(n)

考虑优化

至于系数表示法,它的系数固定,你一改其它都要改,似乎很难弄。

但是第二种点值法似乎没有特别大的限制让它难以优化。
但是怎么优化呢?这就要看点前置知识才可以继续了。

前置知识

向量

普通的量只有大小,但是向量就是有方向又有大小的量。
在几何里面它其实就是一个箭头。

圆的弧度制

等于半径长的圆弧所对的圆心角叫做1弧度的角,叫做弧度,用 rad 表示。
用它做单位来量角的制度就是弧度制。

1=π180rad
180=πrad

复数

a,b 为实数,i2=1,那形如 a+bi 的数就叫做复数。
i 就是其中的复数单位。

复数其实可以在一个二维平面表示出来,x 轴表示实数 ay 轴(当然在原点的时候不是虚数)表示虚数 bi
然后从 (0,0)(a,b) 的向量就表示了复数 a+bi

模长:就是那个点到原点的距离,勾股就可以得到。
幅角:从 x 正半轴以逆时针为正方向到一直向量的转角。

有关复数的运算法则

那由于复数在平面上是向量,那我们其实可以用向量的加减法则来弄。(就是用那个平行四边形定则)

然后乘法就稍微复杂一点。
几何的意义就是,复数相乘,模长也相乘,然后幅角就会相加。
其实我们这里要的是代数意义,我们化一下式子:
(a+bi)(c+di)
=ac+adi+bci+bidi
=ac+i(ad+bc)bdi2=1
=(acbd)+(ad+bc)i

单位根

在复平面上,以原点为圆心,1 为半径作圆,所得的圆叫单位圆。

以圆点为起点,圆的 n 等分点为终点,做 n 个向量,设幅角为正且最小的向量对应的复数为 ωn,称为 n次单位根。

那剩下的那 n1 个就是 ωn2,ωn3,...,ωnn

然后要注意的就是 ωn0ωnn 都是 1,对于的是 x 正半轴的向量。

那怎么计算其它的呢?
我们可以用欧拉公式来求:ωnk=cosk2πn+isink2πn
(大概就是把模长那条线往 x 轴做垂线得到直角三角形,然后用三角函数得到两条直角边的长度,从而得到坐标,也就是复数)

当然显而易见,单位根幅角就是 1n

还有就是如果 zn=1,那 z 就是 n 次单位根。

单位根的一些性质

ω2n2k=ωnk
证明:
ω2n2k=cos2k2π2n+isin2k2π2n
=cosk2πn+isink2πn=ωnk

ωnk+n2=ωnk
证明:
ωnn2=cosn22πn+isinn22πn
=cosπ+isinπ=1
cosπ=cos180=1,sinπ=sin180=0
那就有 ωnk+n2=ωnkωnn2=ωnk(1)=ωnk

OK,现在前置知识结束,开始正文。

快速傅里叶变换(FFT)

我们设 A(x) 的系数为 (a0,a1,...,an1)n 次多项式)
那就有 A(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5++an2xn2+an1xn1

我们可以把它奇偶分开,A(x)=(a0+a2x2+...+an2xn2)+(a1+a3x3+...+an1xn1)
那我们可以设 A1(x)=a0+a2x+...+an2xn21,A2(x)=a1+a3x+...+an1xn21

那我们容易看出 A(x)=A1(x2)+xA2(x2)

ωnk 带入,A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)

ωnk+n2 带入,A(ωnk)=A1(ωn2k+n)+ωnk+n2A2(ωn2k+n),然后把这个式子化一下:
=A1(ωn2kωnn)ωnkA2(ωn2kωnn)
=A1(ωn2k)ωnkA2(ωn2k)

你会发现这两个东西不同的只有一个常数项。
那我们可以枚举得到第一个答案,然后通过第一个答案得到第二个。
那你会发现你只用枚举前面的一半,那问题规模就减半。

那分出的两个问题可以继续分半解决,那我们可以用递归来搞。
那这个其实就是类似分治的感觉,是 O(nlogn) 的。

快速傅里叶逆变换(IFFT)

前面我们在弄的时候都是用点值表示法。

但一般我们(题目)给的多半都是系数表示法。
从系数表示法得到点值表示法我们已经学会了,但是怎么转回来呢?

这个时候就要用到 IFFT 了。
(y0,y1,...,yn1)(a0,a1,...,an1) 的傅里叶变换(就是点值表示)
那另外有一个 (c0,c1,...,cn1) 满足:
ck=i=0n1yi(ωnk)i
(其实就是把 (y0,y1,...,yn1) 当做多项式,求它在 ωn0,ωn1,...,ωn(n1) 的点值表示)

然后来推推公式:
ck=i=0n1yi(ωnk)i
=i=0n1(j=0n1aj(ωni)j)(ωnk)i
=i=0n1(j=0n1aj(ωnj)i)(ωnk)i
=i=0n1(j=0n1aj(ωnj)i(ωnk)i)
=i=0n1j=0n1aj(ωnjk)i
=j=0n1aji=0n1(ωnjk)i

我们设 S(x)=i=0n1xi
带入 ωnkS(ωnk)=i=0n1(ωnk)i

然后分类讨论:

k=0 时,ωnk=ωn0=1
那式子就变成了 S(ωnk)=i=0n11i=n

k0 时,我们考虑怎么求。
观察到没项都乘了一个值,我们考虑用这么一种方法:
ωnkωnkS(ωnk)=i=1n(ωnk)i
然后两个相减:ωnkS(ωnk)S(ωnk)=(ωnk)n(wnk)0
(ωnk1)S(ωnk)=(ωnk)n1
(ωnk1)S(ωnk)=(ωnn)k1
(ωnk1)S(ωnk)=1k1
S(ωnk)=11ωnk1=0

OK,分析完了 S(x),我们回到之前卡克的地方:
ck=j=0n1aji=0n1(ωnjk)i
=j=0n1ajS(ωnjk)
那只会有一个 j=k,使得 jk=0,贡献是 n,其它的时候,贡献都是 0
那就是 ck=akn
那你就会发现,ak=ckn

那我们就可以通过求出 (c0,c1,...,cn1),然后再根据这个一弄就可以得到 ak 了。
(而且你会发现求 (c0,c1,...,cn1) 和求 FFT 的很像,只是由 ωni 变成了 ωni,那我们只要再搞一个 op,记录它单位的改变即可以了)

算法总结

它的主要思想就是把系数表示法转成点值表示法,然后用点值表示法的分支方法来快速得到答案,然后再将答案转回系数表示法。

实现

妹想到把,还有。

别的地方都没有问题,我们来讲讲递归的做法。

递归

(我一定不会告诉你这个是过不了的)

递归其实就是根据我们前面的奇偶分开做法,把一个序列弄成两个,然后递归处理之后合并。

递归到只剩一个常数项的时候,就可以直接返回。

然后我们先来一个小小的优化。
它有一个很牛逼的名字——蝴蝶效应。
(其实就是记忆化)
因为向量的乘法耗比较多时间,我们可以先把要乘的乘出来放一个变量里,然后要用的时候直接就是这个变量。
(这样如果这个乘法值要用多次的话就可以节省时间,原本要乘很多次,现在只用乘一次)

然而就算你加了这个优化,也是过不了的。
因为你递归,加上常数比较大,在 106 就爆了。

那我们就要找一个更优的方法来弄,那就是迭代实现。

迭代实现

我们考虑观察一下原序列和翻转之后的序列。

我们观察一下它是怎么变的:
0,1,2,3,4,5,6,7
0,2,4,6,1,3,5,7
0,4,2,6,1,5,3,7

似乎没有什么想法,转成二进制:
000,100,010,110,001,101,011,111
再把一开始的也转成二进制:
000,001,010,011,100,101,110,111
你会发现它就是把它们的二进制翻转了。

那你可以用一个 O(n) 的方法得到对应关系——DP。
fi=(fi/2/2)|((i&1)n1)
大概就是把之前翻转好的往左边移一个,流出位置给原序列中右边新出现的一位放。

接着就是怎么通过这个翻转对应得到我们迭代的序列。
现有一个显然的东西,就是 ffi=i

那也就是说,它们之间是两两对应的。
那就需要把每对都只翻转一次,就可以了。
那简单,我们只要找一个在一对里面只会发生一次的事,就比如 fi>i。(当然你小于也可以,只要让一对只有一个会发生就可以了)

然后我们来讲讲迭代要怎么搞。
其实就想到与从下段直接网上合并。(因为你已经确定了最下的序列)
就枚举合并的大小,然后枚举区间,然后就合并。

小声哔哔

终于写完了,好累啊。
awa

代码

#include<cstdio> #include<cmath> #include<iostream> using namespace std; struct complex { double x, y; complex (double xx = 0, double yy = 0) { x = xx; y = yy; } }a[5000005], b[5000005]; int n, m, limit, l_size; int an[5000005]; double Pi = acos(-1.0); complex operator +(complex x, complex y) {//定义向量的加减乘 return complex(x.x + y.x, x.y + y.y); } complex operator -(complex x, complex y) { return complex(x.x - y.x, x.y - y.y); } complex operator *(complex x, complex y) { return complex(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x); } //op=1则系数变点值,为-1则点值边系数 //至于为啥看看求的两个公式就知道了 void FFT(complex *now, int op) { for (int i = 0; i < limit; i++) if (i < an[i]) swap(now[i], now[an[i]]);//求出要迭代的序列 for (int mid = 1; mid < limit; mid <<= 1) {//枚举合并序列的大小 complex Wn(cos(Pi / mid), op * sin(Pi / mid));//单位根 for (int R = mid << 1, j = 0; j < limit; j += R) {//R是区间右端点,j表示左端点 complex w(1, 0);//幂 for (int k = 0; k < mid; k++, w = w * Wn) {//枚举区间的每个位置 complex x = now[j + k], y = w * now[j + mid + k];//先求出来减少时间 now[j + k] = x + y;//得到一个求出另外一个的 now[j + mid + k] = x - y; } } } } int main() { scanf("%d %d", &n, &m); for (int i = 0; i <= n; i++) scanf("%lf", &a[i].x); for (int i = 0; i <= m; i++) scanf("%lf", &b[i].x); limit = 1; while (limit <= n + m) { limit <<= 1; l_size++; } for (int i = 0; i < limit; i++) an[i] = (an[i >> 1] >> 1) | ((i & 1) << (l_size - 1)); FFT(a, 1); FFT(b, 1); for (int i = 0; i <= limit; i++) a[i] = a[i] * b[i]; //将点值表示法转换为系数表示法再输出 FFT(a, -1); for (int i = 0; i <= n + m; i++) printf("%d ", (int)(a[i].x / limit + 0.5)); return 0; }

__EOF__

本文作者あおいSakura
本文链接https://www.cnblogs.com/Sakura-TJH/p/luogu_P3803_FFT.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   あおいSakura  阅读(153)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示