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

感觉题解区不是写的太高深,就是写的太高深。所以给初中、小学和幼儿园的萌新准备一篇简单易懂的良心题解~

前置知识

一、多项式的系数表示法和点值表示法。A(x)=i=0n1aixi

系数:(a0,a1,a2...an2,an1)

点值:(x0,y0),(x1,y1)...(xn1,yn1)。易证,n 个点确定一个多项式。

二、向量的加减乘。

三、圆的弧度制:1=π180rad,180=πrad

四、复数:设 a,b 为实数,i2=1,形如 a+ib 的数叫复数。x 为复平面的实数轴,y 为虚数轴,从 (0,0)(a,b) 的向量表示复数 a+ib,其中模长为 a2+b2,幅角为以 x 轴正半轴到已知向量的转角的有向角。

五、单位根。(下文默认 n2 的正整数次幂)

在复平面上以 (0,0) 为圆心,1 为半径做圆。n 等分后再做 n 个向量。则幅角为正且最小的向量对应复数 ωn,称为 n 次单位根。根据向量的乘法,其余 n1 个复数为 ωn2,ωn3...ωnn。其中 ωnk=cosk2πn+sink2πn

接下来证明两个之后会用到的公式。

  1. 证明 ω2n2k=ωnk

ω2n2k=cos2k2π2n+sin2k2π2n=cosk2πn+sink2πn=ωnk

  1. 证明 wnk+n2=ωnk

ωnk+n2=ωnkωnn2=ωnk(cosn22πn+sinn22πn)=ωnk(cosπ+sinπ)=ωnk

特殊的,wn0=wnn=1

正文

这里的 FFT 是要解决两个多项式相乘的问题。

如果用系数表示法直接暴力相乘,复杂度 O(n2),很难进一步优化,所以考虑点值表示法。

点值表示法的做法为,对于两个多项式分别找出足够的点,将其对应的函数值相乘,再通过某种方式转回系数表示法。

然而,如果我们随便选 n 个点并求出其对应的值,单次复杂度线性,执行 nO(n2),还是不行。

因此我们所选的 n 个点必须要有很好的性质,从而减少计算量。

(ωn,ωn2,ωn3...ωnn) 刚好有着很强的性质,可以作为所选的 n 个值,通过分治可将复杂度降至 O(nlogn)

接下来会简单介绍做法。

定义多项式 A(x)=(a0,a1,a2...an1)

按下标奇偶分类后得:

A(x)=(a0+a2x2+a4x4+...+an2xn2)+(a1x+a3x3+a5x5+...+an1xn1)

定义:

A1(x)=a0+a2x+a4x2+a6x3+...+an2xn21

A2(x)=a1+a3x+a5x2+a7x3+...+an1xn21

那么可得:

A(x)=A1(x2)+xA2(x2)

带入 ωnk(k<n2) 得:

A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)

带入 ωnk+n2 得:

A(ωnk+n2)=A1(ωn2k+n)+ωnk+n2A2(ωn2k+n)=A1(ωn2kωnn)ωnkA2(ωn2kωnn)=A1(ωn2k)ωnkA2(ωn2k)

可以发现,当 k 取遍 [0,n2)[n2,n) 时,A1,A2 转移项下标都取遍 [n2,n)。第二个式子与第一个式子只差一个符号,所以递归完后可以同时算,复杂度 O(nlogn)

这步操作也被称为蝴蝶操作,即将 A1(ωni)A(ωni)A(ωni+n2) 连边,A2(ωni)A(ωni)A(ωni+n2) 连边,形成的图形像蝴蝶的双翅。(其实这里的连边是上标之间的连边,A2 接在 A1 右边)

可能会有人问:自变量为什么必须是 ωnk,而不能是 k 呢?

原因很简单:当 k 取遍 [0,n2)[n2,n) 时,两段对应的区间完全不同,导致搜索量无法减半,复杂度 O(n2)

这里想明白 FFT 基本就算学明白了。

现在我们已经得到新多项式的 n 个点了,考虑怎么转化回系数,即 IFFT。

因为 xi=ωni,yi=t=0n1atxit。所以 yi=t=0n1at(ωni)t

定义 ck=i=0n1yi(wnk)i

那么,

ck=i=0n1[(j=0n1aj(ωni)j)(wnk)i]=i=0n1(j=0n1aj(ωni)j(ωnk)i)=j=0n1aji=0n1(wnj)i(wnk)i=j=0n1aji=0n1(wnjk)i

定义 s(x)=i=0n1xi

两边同乘 x 得:xs(x)=i=1nxi

两者相减得:(x1)s(x)=xn1

所以 s(x)=xn1x1

带入 ωnk(kn) 得:s(x)=(wnk)n1wnk1=11wnk1=0

jki=0n1(wnjk)i=0

j=ki=0n1(wnjk)i=n

所以 ck=nakak=ckn

FFT 算法到这里就结束了。

具体实现时,可对多项式 A(x),B(x) 分别跑一遍 FFT,对应函数值相乘做一遍 IFFT 还原系数。

然后我们发现当递归到最底层时,序列各个元素编号二进制反转后是连续的。

所以可以先把最底层的弄出来,自底向上合并,常数很小。

code:

#include<bits/stdc++.h>
using namespace std;
typedef double db;
const int N=1e7+5;
const db pi=acos(-1.0);
int n,m,r[N],lim=1;
struct node{db x,y;node(db x=0.0,db y=0.0):x(x),y(y){}};
node operator+(node a,node b){return node(a.x+b.x,a.y+b.y);}
node operator-(node a,node b){return node(a.x-b.x,a.y-b.y);}
node operator*(node a,node b){return node(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);}
void FFT(node *a,int op){
  for(int i=0;i<lim;++i)if(i<r[i])swap(a[i],a[r[i]]);
  for(int t=1;t<lim;t<<=1){
    node B=node(cos(pi/t),op*sin(pi/t)),mi,x,y;
    for(int len=t<<1,j=0;j<lim;j+=len){
      mi=node(1,0);
      for(int k=0;k<t;++k,mi=mi*B){
        x=a[j+k];y=mi*a[j+t+k];
        a[j+k]=x+y;a[j+t+k]=x-y;
      }
    }
  }
}
node a[N],b[N];
int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=0;i<=n;++i)cin>>a[i].x;
  for(int i=0;i<=m;++i)cin>>b[i].x;
  int l=0;
  while(lim<=n+m)lim<<=1,++l;
  for(int i=0;i<lim;++i)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
  FFT(a,1);FFT(b,1);
  for(int i=0;i<lim;++i)a[i]=a[i]*b[i];
  FFT(a,-1);
  for(int i=0;i<=n+m;++i)cout<<(int)(a[i].x/lim+0.5)<<" ";
  cout<<endl;
  return 0;
}

本文作者:HQJ2007

本文链接:https://www.cnblogs.com/HQJ2007/p/17561323.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   HQJ2007  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起