多项式小记
多项式小记
基本介绍
多项式,听起来非常抽象的东西,的确,这个东西是一个省选的知识点,所以它确实不简单。但是随着算法学习的深入,它也显得非常重要。其主要功能,或者说其中包含算法的主要功能主要是更高效率的进行一些多项式的计算,其中的一些思想可以说是极为优美。
FFT
前置芝士
表示多项式 ,例如 可以为 。- 系数表示法:一般来说,我们表示多项式用的都是系数表示法。比如对于上面的
,我们可以用数组 来表示它每一项的系数,简单明了。 - 系数表示法计算乘积:在系数表示发的情况下,如何计算多项式的乘积?非常简单,若
则 ,假设 有 项, 有 项,则计算的时间复杂度就是 的
DFT(IDFT)思想
为了加速计算过程,我们首先从表示方法考虑。有没有其他可以表示多项式的方法?
是有的。考虑一个一次函数,我们知道其图像一定是一个直线,我们知道两点确定一个直线,因此我们可以用两个点来表示一个一次函数。
把这个结论推广(可以去思考更详细的证明),我们可以知道
所以对于
这样子我们会发现,我们再次计算
但是问题是,如何把系数表达式,化为点表达式?
如果随机取点,我们需要取
系数 点,DFT
思考
思考
浅浅推广一些,我们假设
我们会有以下两个式子:
这样做的目的是为了分治,即我们想用尽可求少的点具体的值,来得到尽可能多的点——少算、多点。上面式子即如果知道了
但是我们分成一半之后,会发现
这里我们就考虑到虚数了!
令
对于它,有几个点:
可以理解为把 绕原点旋转 个圆周得到的点 ,也就是把这个圆分成一定份数去某一份,将这个过程翻个倍,不会影响- 若
是 的倍数,
我们代入一下分析:
Case 1:
经过一些化简可以得到:
Case 2:
经过一些化简可以得到:
注:这一部分化简技巧性不大,暴力带入
更好的操作
我们把上面的式子转换一下,变成这样子会好做一点(因为上面的
这意味着什么?这意味着知道后面两个值的情况我们可以
最后,我们再考虑 简单的操作,我们就做到了能够
系数 点 IDFT
最后反推的过程,这里有一个结论就是把上面的
补,考虑证明:
设:
需证明(即类似反演的东西):
右边带入
考虑
注意到第一个
代码实现
跟一些模板一样,FFT 的实现和理论还是有不小的距离的。结果上来说,只记住上面的结论式子就能够应对绝大部分需要 FFT 的多项式题了,但是模板需要全部记忆或者理解。
难点在于各种优化,由简到难(没看懂参考代码):
DFT IDFT
可以封装成一个函数,如上所述,差距只有一个负号。
“分”
按照推理的逻辑,我们会按照
直接上结论,这个过程为“蝴蝶变换”,即满足
to[x]=((i&1)?(n>>1):0)|(to[(x>>1)]>>1);
“治”
回顾:
注意到直接在原数组上修改即可,不需要单独拷贝数组。
for(int k=j;k<j+len;k++){
Cp tmp=now*f[len+k];
f[len+k]=f[k]-tmp;
f[k]=f[k]+tmp;
}
三角函数 + 递推
三角函数常数较大,改为递推的形式。因为确定了
这些优化合起来就是代码实现的 FFT:
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
using namespace std;
const double pi=acos(-1);
const int maxn=5e6+10;
struct Cp{//自定义复数类
Cp (double xx=0,double yy=0){x=xx,y=yy;}
double x,y;
Cp operator + (Cp const &B) const{return Cp(x+B.x,y+B.y);}
Cp operator - (Cp const &B) const{return Cp(x-B.x,y-B.y);}
Cp operator * (Cp const &B) const{return Cp(x*B.x-y*B.y,x*B.y+y*B.x);}
}F[maxn],G[maxn];
int n,m;
int to[maxn];
void FFT(Cp *f, bool flag){//flag=1:DFT
for(int i=0;i<n;i++)if(i<to[i])swap(f[i],f[to[i]]);
for(int i=2;i<=n;i<<=1){
int len=i>>1;
//当前的长度,len表示长度的一半
Cp bas(cos(2*pi/i),sin(2*pi/i));
//固定了长度之后单位自然也固定了
if(!flag)bas.y*=-1;
for(int j=0;j<n;j+=i){//枚举起点
Cp now(1,0);
for(int k=j;k<j+len;k++){//枚举具体位置
//直接在数组上操作,动手写出式子写出来然后画一下可以立刻明白
Cp tmp=now*f[len+k];
f[len+k]=f[k]-tmp;
f[k]=f[k]+tmp;
now=now*bas;
//omega_n^i->omega_n^(i+1)
}
}
}
}
//蝴蝶变化
void initto(int lim){for(int i=0;i<lim;i++)to[i]=((i&1)?(n>>1):0)|(to[i>>1]>>1);}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)scanf("%lf",&F[i].x);
for(int i=0;i<=m;i++)scanf("%lf",&G[i].x);
for(m+=n,n=1;n<=m;n<<=1);//补齐
initto(n);
FFT(F,1),FFT(G,1);
for(int i=0;i<n;i++)F[i]=F[i]*G[i];
FFT(F,0);
for(int i=0;i<=m;i++){
printf("%d ",(int)(F[i].x/n+0.49));//取到最近的整数
}
}
感觉前面的代码理解铺垫应该已经够详细了。
终于把 FFT 更新了,也不知道拖了多久。
NTT
FFT 虽然可以快速计算多项式,但是有一些天然劣势。因为其用的是 double,因此当数字很大的时候精度会不够。而在 OI 当中数字过大的时候一般采用取模的方式解决。然而数学家已经证明了在
原根
原根涉及到很多的数学证明,由于本人水平不足加上时间问题,这里省略大量证明,毕竟作为 OIer 其实有一些证明也不需要太明白。
定义:
定义:若有
定理:只有
定理:若
简述一下如何找
结论:满足
基本的,
分治 FFT / NTT
好像也并没有真正的算法知识,是一个技巧性的东西。看例题:分治 FFT
给定多项式
考虑进行类似 cdq 分治一样的东西,对于一个区间分治之后,计算左边对于右边的贡献。令当前计算的区间为
这一题还有另解,见下。
求逆
引入:界,即一个多项式
对于多项式求逆,即对于
考虑倍增,已知
考虑
清醒点,这是同余意义下,用什么求根公式呢。利用逆的性质,同乘
注意因为界变成了
Poly inv(Poly A){
int sizA=A.siz(),n=1;
for(;n<sizA;n<<=1);
Poly T,R,sav;
T.resiz(n<<1),R.resiz(n<<1),sav.resiz(n<<1),A.resiz(n<<1);
T.a[0]=qpow(A.a[0],mod-2);
for(int len=2;len<=n;len<<=1){
for(int i=0;i<(len>>1);i++)R.a[i]=(T.a[i]<<1)%mod;
sav.cpy(A,len);
NTT(T.a,len<<1,1),px(T,T,len<<1);
NTT(sav.a,len<<1,n),px(T,sav,len<<1);
NTT(T.a,len<<1,0),T.cl(len,len<<1);
for(int i=0;i<len;i++)T.a[i]=(R.a[i]-T.a[i]+mod)%mod;
}
T.resiz(sizA),A.resiz(sizA);
return T;
}
附:对于上面那题,考虑两个多项式的关系可以表示为
化简就有:
求逆直接秒了,而且比分治少一个
求导 + 积分
和正常求导和积分一模一样:
都是可以
Poly dr(Poly A){
Poly res;
res.resiz(A.siz()-1);
for(int i=0;i<res.siz();i++)res.a[i]=A.a[i+1]*(i+1)%mod;
return res;
}
Poly itg(Poly A){
vector<int>pinv;
Poly res;
pinv.resize(A.siz()+1),res.resiz(A.siz()+1);
pinv[1]=1,pinv[0]=0;
for(int i=2;i<pinv.size();i++)pinv[i]=pinv[mod%i]*(mod-mod/i)%mod;
for(int i=1;i<res.siz();i++)res.a[i]=A.a[i-1]*pinv[i]%mod;
res.a[0]=0;
return res;
}
牛顿迭代(复合)
定义复合多项式:
重要定理
若
这个结论就是牛迭的关键,证明大概是在
补:尝试证明。
有泰勒展开:
即
考虑
考虑
然后移项加上
这个公式非常厉害,如上面的多项式求逆的推导,对于
改一下,变成
比较之前的
开根
我们尝试用一下刚才说的牛迭。
对于
有
对于
又要定义复数域,感觉回到了 FFT。不过注意 Cipolla 的复数运算为
Poly sq(Poly A){
int sizA=A.siz(),n=1;
for(;n<sizA;n<<=1);
Poly T,R;
T.resiz(n<<1),R.resiz(n<<1),A.resiz(n<<1);
T.a[0]=Cipolla(A.a[0]);
if(mod-T.a[0]<T.a[0])T.a[0]=mod-T.a[0];
for(int len=2;len<=n;len<<=1){
for(int i=0;i<(len>>1);i++)R.a[i]=(T.a[i]<<1)%mod;
R.cl(len>>1,len);
R=inv(R);
NTT(T.a,len,1),px(T,T,len),NTT(T.a,len,0);
for(int i=0;i<len;i++)T.a[i]=(T.a[i]+A.a[i])%mod;
T.resiz(len<<1),R.resiz(len<<1);
T=mut(T,R);
T.cl(len,len<<1);
}
T.resiz(sizA),A.resiz(sizA);
return T;
}
带余除法
多项式求逆好比计算
这一部分好像和牛顿迭代没有太大的关系,但是这个推导也是比较巧妙的。
先明确一下思路,我们需要想办法通过一些操作把
对于一个
发现其实相当于把系数全部反过来了。
对于要求的式子换元 + 同乘系数:
写成反转的形式有:
看起来没有实质上的改变,但是注意到
通过多项式求逆得到
pair<Poly,Poly>mdiv(Poly A,Poly B){
Poly AT,BT,Q,tmp,R;
R.resiz(B.siz()-1);
int L=A.siz()-B.siz()+1;
AT.resiz(A.siz()),BT.resiz(B.siz());
AT.cpy(A,AT.siz()),BT.cpy(B,BT.siz());
AT.rev(),BT.rev(),AT.resiz(L),BT.resiz(L);
BT=inv(BT),Q=mut(AT,BT);
Q.resiz(L),Q.rev();
tmp=mut(B,Q);
for(int i=0;i<B.siz()-1;i++){
R.a[i]=A.a[i]-tmp.a[i];
if(R.a[i]<0)R.a[i]+=mod;
}
return make_pair(Q,R);
}
求
给定多项式
首先先明确一些东西,有:
证明直接对两边求导得到导函数相等,加上
这个对于多项式同样有用,再加上换元就有:
当然,这个式子对于直接做
这样就可以算出来
注意到这样做的
Poly ln(Poly A){
Poly res,B=A;
B=inv(A),A=dr(A);
res=mut(A,B);
res=itg(res);
res.resiz(A.siz()+1);
return res;
}
求
给定
同样我们只考虑首项为
继续使用牛顿迭代,第一步先转化一下,有
Poly exp(Poly A){
Poly R,T;
int n=1,siz=A.siz();
for(;n<A.siz();n<<=1);
A.resiz(n<<1),T.resiz(n<<1),R.resiz(n<<1),T.a[0]=1;
for(int len=2;len<=n;len<<=1){
R.cpy(T,len>>1),R=ln(R);
for(int i=0;i<len;i++)R.a[i]=(A.a[i]-R.a[i]+mod)%mod;
R.a[0]=(R.a[0]+1)%mod;
T.resiz(len<<1),R.resiz(len<<1);
T=mut(T,R);
T.cl(len,len<<1);
}
T.resiz(siz);
return T;
}
快速幂
给定
比较直观的是就像数字快速幂一样做,但是乘法的代价是
注意到有
这三个部分本人都还没有写常数项是一般情况下的做法,暂时放下,放一个完整的板子。常数不是很优秀,简介程度也说不上好,但是能够应对大部分情况了。
任意模数 NTT
做 NTT 的时候大部分情况遇到的模数就是
解决这种问题比较好些的方法就是三模数 NTT。需要知道的是中国剩余定理 CRT 不知道 Ex 版没关系,普通版本结论知道即可。
一般题目中模数会
但是要直接做 CRT 会爆 longlong 范围,考虑怎么解决这个问题。令三个解分别是
其中逆元都是
可以建立如下关系:
在
全家桶
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=998244353,_G=3,maxn=5e5+10;
int qpow(int x,int y){
int res=1;
for(;y;y>>=1){
if(y&1)res=res*x%mod;
x=x*x%mod;
}
return res;
}
const int invG=qpow(_G,mod-2);
struct cmx{
int x,y,w;
cmx operator * (cmx const & a)const{
cmx res;
res.x=((x*a.x)%mod+(y*a.y)%mod*w%mod)%mod;
res.y=((x*a.y)%mod+(y*a.x)%mod+mod)%mod;
res.w=w;
return res;
}
};
int chk_rem(int x){
int res=qpow(x,(mod-1)/2);
if(res==mod-1)return -1;
return 1;
}
cmx cmxqpow(cmx x,int y){
cmx res=(cmx){1,0,x.w};
for(;y;y>>=1){
if(y&1)res=res*x;
x=x*x;
}
return res;
}
int Cipolla(int X){
if(X==0)return 0;
if(chk_rem(X)==-1)assert(0);//no answer
int a;
cmx ans;
while(1){
a=rand()%mod;
ans.w=(a*a%mod-X+mod)%mod;
if(chk_rem(ans.w)==-1)break;
}
ans.x=a,ans.y=1;
return cmxqpow(ans,(mod+1)/2).x;
}
void NTT(vector<int> &g,int n,int flag){//flag=0:inverse
vector<int>w,f,to;
w.resize(n),f.resize(n),to.resize(n);
for(int i=0;i<n;i++)to[i]=((i&1)?(n>>1):0)|(to[i>>1]>>1);
for(int i=0;i<n;i++)f[i]=g[to[i]];
for(int T=2;T<=n;T<<=1){
int bas=flag?qpow(_G,(mod-1)/T):qpow(invG,(mod-1)/T),len=T>>1;
w[0]=1;
for(int i=1;i<T;i++)w[i]=w[i-1]*bas%mod;
for(int j=0;j<n;j+=T){
for(int k=j;k<j+len;k++){
int tmp=w[k-j]*f[len+k]%mod;
f[len+k]=f[k]-tmp;
f[k]=f[k]+tmp;
if(f[len+k]<0)f[len+k]+=mod;
if(f[k]>=mod)f[k]-=mod;
}
}
}
if(!flag){
int ivn=qpow(n,mod-2);
for(int i=0;i<n;i++)g[i]=f[i]*ivn%mod;
} else for(int i=0;i<n;i++)g[i]=f[i];
}
struct Poly{
vector<int>a;
int siz(){return a.size();}
void resiz(int x){a.resize(x);}
void cpy(Poly V,int len){for(int i=0;i<len;i++)a[i]=V.a[i];}
void cl(int l,int r){for(int i=l;i<r;i++)a[i]=0;}
void rev(){reverse(a.begin(),a.end());}
void pr(){
for(int i=0;i<a.size();i++)cout<<a[i]<<" ";
cout<<"\n";
}
};
void px(Poly &X,Poly Y,int len){
for(int i=0;i<len;i++)X.a[i]=X.a[i]*Y.a[i]%mod;
}
Poly mut(Poly X,Poly Y){
Poly res;
int sizX=X.siz(),sizY=Y.siz(),n=1,tsiz=sizX+sizY;
for(;n<sizX+sizY;n<<=1);
X.resiz(n),Y.resiz(n),res.resiz(n),res.cpy(X,n);
NTT(res.a,n,1),NTT(Y.a,n,1);
px(res,Y,n),NTT(res.a,n,0);
res.resiz(tsiz);
return res;
}
Poly inv(Poly A){
int sizA=A.siz(),n=1;
for(;n<sizA;n<<=1);
Poly T,R,sav;
T.resiz(n<<1),R.resiz(n<<1),sav.resiz(n<<1),A.resiz(n<<1);
T.a[0]=qpow(A.a[0],mod-2);
for(int len=2;len<=n;len<<=1){
for(int i=0;i<(len>>1);i++)R.a[i]=(T.a[i]<<1)%mod;
sav.cpy(A,len);
NTT(T.a,len<<1,1),px(T,T,len<<1);
NTT(sav.a,len<<1,n),px(T,sav,len<<1);
NTT(T.a,len<<1,0),T.cl(len,len<<1);
for(int i=0;i<len;i++)T.a[i]=(R.a[i]-T.a[i]+mod)%mod;
}
T.resiz(sizA),A.resiz(sizA);
return T;
}
Poly sq(Poly A){
int sizA=A.siz(),n=1;
for(;n<sizA;n<<=1);
Poly T,R;
T.resiz(n<<1),R.resiz(n<<1),A.resiz(n<<1);
T.a[0]=Cipolla(A.a[0]);
if(mod-T.a[0]<T.a[0])T.a[0]=mod-T.a[0];
for(int len=2;len<=n;len<<=1){
for(int i=0;i<(len>>1);i++)R.a[i]=(T.a[i]<<1)%mod;
R.cl(len>>1,len);
R=inv(R);
NTT(T.a,len,1),px(T,T,len),NTT(T.a,len,0);
for(int i=0;i<len;i++)T.a[i]=(T.a[i]+A.a[i])%mod;
T.resiz(len<<1),R.resiz(len<<1);
T=mut(T,R);
T.cl(len,len<<1);
}
T.resiz(sizA),A.resiz(sizA);
return T;
}
pair<Poly,Poly>mdiv(Poly A,Poly B){
Poly AT,BT,Q,tmp,R;
R.resiz(B.siz()-1);
int L=A.siz()-B.siz()+1;
AT.resiz(A.siz()),BT.resiz(B.siz());
AT.cpy(A,AT.siz()),BT.cpy(B,BT.siz());
AT.rev(),BT.rev(),AT.resiz(L),BT.resiz(L);
BT=inv(BT),Q=mut(AT,BT);
Q.resiz(L),Q.rev();
tmp=mut(B,Q);
for(int i=0;i<B.siz()-1;i++){
R.a[i]=A.a[i]-tmp.a[i];
if(R.a[i]<0)R.a[i]+=mod;
}
return make_pair(Q,R);
}
Poly dr(Poly A){
Poly res;
res.resiz(A.siz()-1);
for(int i=0;i<res.siz();i++)res.a[i]=A.a[i+1]*(i+1)%mod;
return res;
}
Poly itg(Poly A){
vector<int>pinv;
Poly res;
pinv.resize(A.siz()+1),res.resiz(A.siz()+1);
pinv[1]=1,pinv[0]=0;
for(int i=2;i<pinv.size();i++)pinv[i]=pinv[mod%i]*(mod-mod/i)%mod;
for(int i=1;i<res.siz();i++)res.a[i]=A.a[i-1]*pinv[i]%mod;
res.a[0]=0;
return res;
}
Poly ln(Poly A){
Poly res,B=A;
B=inv(A),A=dr(A);
res=mut(A,B);
res=itg(res);
res.resiz(A.siz()+1);
return res;
}
Poly exp(Poly A){
Poly R,T;
int n=1,siz=A.siz();
for(;n<A.siz();n<<=1);
A.resiz(n<<1),T.resiz(n<<1),R.resiz(n<<1),T.a[0]=1;
for(int len=2;len<=n;len<<=1){
R.cpy(T,len>>1),R=ln(R);
for(int i=0;i<len;i++)R.a[i]=(A.a[i]-R.a[i]+mod)%mod;
R.a[0]=(R.a[0]+1)%mod;
T.resiz(len<<1),R.resiz(len<<1);
T=mut(T,R);
T.cl(len,len<<1);
}
T.resiz(siz);
return T;
}
Poly pqpow(Poly A,int k){
A=ln(A);
for(int i=0;i<A.siz();i++)A.a[i]=A.a[i]*k%mod;
A=exp(A);
return A;
}
int n,m;
Poly f,g;
signed main(){
srand(time(0));
cin.tie(0),cout.tie(0),ios::sync_with_stdio(false);
return 0;
}
跨越大半个 OI 生涯的笔记。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示