目录
基本概念
定义形如这样的函数为多项式:
其中 为多项式的项数(系数为0的项也是项), 为多项式的最高次幂, 为多项式中 次项的系数。
符号约定&说明
- 一般情况下,函数自变量均用 表示。
- 表示 这个多项式
- 表示自变量 时 的函数值
- 表示多项式 中 一项的系数
- 表示多项式 的项数,也即最高次幂
- 求积分时,常数项默认为
求导&积分
求导:
对于某一项 ,求导为 。
设 ,则 )
inline void getderivative(int n,int *f,int *h)
{
for(int i=0;i<n;++i) h[i]=mul(f[i+1],i+1);
h[n]=0;
}
积分:
设 ,则
inline void getintegration(int n,int *f,int *h)
{
for(int i=1;i<n;++i) h[i]=mul(f[i-1],inv[i]);//inv[i] i的逆元
h[0]=0;
}
点值表示&拉格朗日插值
多项式 不仅可以用每一项系数表示,由拉格朗日插值法,得到也可以用 个二元组 来表示,也就是用 个点来描述一个 项多项式(即为点值表达式):
这种用点描述多项式的形式称为多项式的点值表示,由点值转化为系数式的过程称为插值。
卷积(Karatsuba算法&FTT&NTT)
多项式之间的乘积称为卷积。
设 ,则 。
设 。直接求多项式卷积,复杂度是 的,使用分治的 算法可复杂度为 ,使用 可以将复杂度降到 。
算法原理是利用
不断递归减少乘法次数。
而 的基本思想都是利用单位复根/单位原根并采用分治策略先将 转化为点值表示,在点值上处理为 的点值表示(显然 ),再转化为 ,因为 中涉及大量虚数实数运算,精度和常数都很感人,所以常用单模数 ,但限制模数为质数。
这里先给出 代码(下文省略),以及习惯性模运算优化:
inline void NTT(int *e,int ptr,int len)//ptr=1 正变换 ptr=0 逆变换
{
int i,j,k,ori,G,pd,ix,iy;G= ptr? g:inv[g];//g:原根 inv[g]:g的逆元
for(i=1;i<len;++i) if(i<rv[i]) swap(e[i],e[rv[i]]);
for(i=1;i<len;i<<=1){
ori=fp(G,(mod-1)/(i<<1));
for(j=0;j<len;j+=(i<<1)){
pd=1;
for(k=0;k<i;++k,pd=mul(pd,ori)){
ix=e[j+k];iy=mul(e[i+j+k],pd);
e[j+k]=ad(ix,iy);e[i+j+k]=dc(ix,iy);
}
}
}
if(ptr) return;
for(i=0;i<len;++i) e[i]=mul(e[i],inv[len]);
}
inline int ad(int x,int y) {x+=y;if(x>=mod) x-=mod;return x;}
inline int dc(int x,int y) {x-=y;if(x<0) x+=mod;return x;}
inline int mul(int x,int y) {return 1ll*x*y%mod;}
牛顿迭代
解近似方程根 的一种方法。
设 当前近似解为 ,进一步迭代:
引申:当 中嵌套一个 构成复合函数 ,同样可以牛顿迭代倍增求解近似的 ,满足 。
注: 即对 求 的偏导 , 。
上式可以用泰勒展开证明当 前 项满足条件, 前 项满足条件。
考虑 在 处的泰勒展开式:
这里令 。
因为 存在前 项满足条件,当 时 最小次幂不小于 次方。
所以牛顿迭代求得就是泰勒展开式的前两项:
由因为目标函数 ,故:
基本运算&初等函数
求逆
给定 ,求 满足
倍增(递归分治)求解。
法1:
观察
平方后得:
将 代入:
最终得到:
法2:
考虑牛顿迭代求解:
底层为
其中
化简得到法1中的式子。
代码:
inline void getinverse(int n,int *f,int *g)
{
if(n==1){g[0]=fastpow(f[0],mod-2);return;}
getinv((n+1)>>1,f,g);
int i,j,len=1,L=0;
static int cont[N];
for(;len<n+n;len<<=1) L++;
for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|((i&1)<<(L-1)));
for(i=0;i<n;++i) cont[i]=f[i];
for(i=n;i<len;++i) cont[i]=0;
NTT(cont,1,len);NTT(g,1,len);
for(i=0;i<len;++i) g[i]=mul(g[i],dc(2,mul(cont[i],g[i])));
NTT(g,0,len);
for(i=n;i<len;++i) g[i]=0;
}
开根
给定 ,求 满足
倍增(递归分治)求解。
法1:
观察
平方后得:
巧妙地移项:
化归得:
,故:
法2:
考虑牛顿迭代求解:
底层为
化简得到法1中的式子。
代码:
inline void getsqrt(int n,int *f,int *g)
{
if(n==1) {g[0]=sqrt(f[0]);return;}
getsqrt((n+1)>>1,f,g);
int i,j,L=0,len=1;
for(;len<n+n;len<<=1) L++;
static int inv[N],tp[N];
for(i=0;i<len;++i) inv[i]=0;
getinv(n,g,inv);
for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|((i&1)<<(L-1)));
for(i=0;i<n;++i) tp[i]=f[i];
for(i=n;i<len;++i) tp[i]=0;
NTT(inv,1,len);NTT(g,1,len); NTT(tp,1,len);
for(i=0;i<len;++i) g[i]=mul(ad(mul(g[i],g[i]),tp[i]),mul(iv[2],inv[i]));
NTT(g,0,len);
for(i=n;i<len;++i) g[i]=0;
}
ln
给定 ,求
求 不能用牛顿迭代求解,因为 中 的系数为1,迭代会产生恒等式。
对等式两侧求导:
则
注意求积分 后默认 ,所以 时要特殊处理 (代入任意值计算处理)。
代码:
inline void getln(int n,int *f,int *g)
{
int i,j,len=1,L=0;
for(;len<n+n;len<<=1) L++;
static int der[N],nv[N];
for(i=0;i<len;++i) der[i]=nv[i]=0;
getderivative(n,f,der);
getinverse(n,f,nv);
for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|(i&1)<<(L-1));
NTT(nv,1,len);NTT(der,1,len);
for(i=0;i<len;++i) der[i]=mul(der[i],nv[i]);
NTT(der,0,len);
getintegration(n,der,g);
}
exp
给定 ,求 满足
倍增(递归分治)求解。求exp只能用牛顿迭代了。
考虑取 变形:
底层为 (一般情况下默认 常数项为0)。
代码:
inline void getexp(int n,int *f,int *g)
{
if(n==1) {g[0]=1;return;}
getexp((n+1)>>1,f,g);
int i,j,L=0,len=1;
for(;len<n+n;len<<=1) L++;
static int ln[N];
for(i=0;i<len;++i) ln[i]=0;
getln(n,g,ln);
for(i=0;i<n;++i) ln[i]=dc(f[i],ln[i]);
ln[0]=ad(ln[0],1);
NTT(ln,1,len);NTT(g,1,len);
for(i=0;i<len;++i) g[i]=mul(g[i],ln[i]);
NTT(g,0,len);
for(i=n;i<len;++i) g[i]=0;
}
快速幂
给定 ,求 满足
考虑取 变形,再次进行降幂:
故:
代码:
inline void getpow(int n,int *f,int *g,int K)
{
int i,j,L=0,len=1;
getln(n,f,g);
for(;len<n;len<<=1) L++;
for(i=0;i<len;++i) g[i]=mul(g[i],K);
for(i=0;i<n;++i) f[i]=0;
getexp(n,g,f);
}
三角函数
给定 ,求 满足
由欧拉公式:
所以可以用复数二元组,实部表示 ,虚部表示 。
求一下 分别对应。
貌似可以 直接算,但应用程度不高。
所以是理论可行。代码?唔,不存在的(留坑)。
除法&取模
形式幂级数&拉格朗日反演
给定 ,求 的 次项系数 ,满足
形式幂级数:
对于一个多项式,仅有有限项的系数是非零的。而对于形式幂级数,则去掉这一限制,将项数与系数推广到无穷大:
其中系数 均为环 (可以是实数/复数/整数/模意义下剩余类)中的元素,为方便表示,设 表示 中 的系数。所有的形式幂级数构成形式幂级数环 。
拉格朗日反演:
若 且 ,则
且 为 的复合逆。
特别的,若 ,则:
由 ,得 ,代入上式:
进一步拓展:
(三层嵌套)
证明详见拉格朗日反演- hjuy134
代码:
inline int lagrange(int n,int *F,int *G,int k)
{
static int res[N];
getinverse(n,F,res);
for(i=n-1;i>=1;--i) res[i]=res[i-1];
res[0]=0;
getpow(n,res,G,k);
return mul(G[k-1],fastpow(k,mod-2));
}
时间复杂度总结
内容 | 时间复杂度 | 常数 |
---|---|---|
卷积 | 3 | |
求逆 | 6 | |
开根 | 18 | |
求导 | 1 | |
积分 | 1 | |
ln | 9 | |
exp | 24 | |
快速幂 | 33 |
求值&插值
多点求值
给定 以及点集 ,求 。
倍增(递归分治)求解。
考虑底层:由拉格朗日插值法式,得到
观察到 ,满足
因为 在可取范围 内的答案都是唯一的,所以可以保留到这 个点的
最简拉格朗日插值式化出的多项式( ),而不会影响答案,而这样的一个式子必然仅
存在于 (最高项次幂为 )的剩余类中,故等式成立。
那么考虑分治处理,设:
,
,
显然对于 , ;对于 , 。
于是每次二分递归,将两边连乘积合并即可,先预处理出所有递归中取模后的多项式,再递归处理。
靠谱的做法是小于一定数量时暴力算,因为多点求值的常数太大了(复杂度 )。
代码:
#define mid (((l)+(r))>>1)
inline void prework(int dep,int l,int r,int *x,int *consmul)//预处理连乘积
{
if(l==r){
consmul[dep][l<<1]=mod-x[l];
consmul[dep][l<<1|1]=1;//递归底部:x-xi
return;
}
prework(dep+1,l,mid,x,consmul);
prework(dep+1,mid+1,r,x,consmul);
getmul(consmul[dep+1]+(l<<1),mid-l+1,consmul[dep+1]+((mid+1)<<1),r-mid,consmul[dep]+(l<<1),r-l+1);
}
inline void getval(int dep,int l,int r,int *f,int *y,int *consmul)
{
static int modulo[D][N];//记录取模后多项式
getmod(f,r-l,consmul[dep]+(l<<1),r-l+1,modulo[dep]+l);
if(l==r){y[l]=modulo[dep][l];return;}
getval(dep+1,l,mid,modulo[dep]+l,y,consmul);
getval(dep+1,mid+1,r,modulo[dep]+mid+1,y,consmul);
}
inline void getval(int *f,int *x,int n,int *y)
{
static int consmul[D][N<<1];//记录连乘积
prework(0,1,n,x,dc);
getval(0,1,n,f,y,dc);
}
注:此代码没有提交过,不保证正确性。
快速插值(牛顿插值&拉格朗日优化)
给定二元组集合 ,求
首先介绍一种不快速但很好手算的方法,牛顿插值(增量法):
设
设前 个点插值得到 ,考虑 :
故
代入 ,解出 ,即可求出 。复杂度 。
现在来考虑一种复杂度
的快速插值:
观察拉格朗日插值式:
通过化简优化一下这个 的式子:
设 ,
显然 。
将 巧妙地求导(利用洛必达法则)后发现:
故:
变成了 的多点求值。
那么
就可以快速求出了,而
,直接用分治
求出连乘积(类似多点求值中的预处理)即可。最后
合并答案。
代码:
#define mid (((l)+(r))>>1)
inline void getinterpolation(int dep,int l,int r,int *val,int *consmul,int *f)
{
if(l==r) {f[l]=val[l];return;}
getinterpolation(dep+1,l,mid,val,consmul,f);
getinterpolation(dep+1,mid+1,r,val,consmul,f);
static int g[N],h[N];
getmul(f+l,mid-l,consmul[dep+1]+((mid+1)<<1),r-mid,g,r-l);
getmul(f+mid+1,r-mid-1,consmul(dep+1)+(l<<1),l-mid+1,h,r-l);
//类似线段树与另一子节点合并过程,每一位乘上除自己位以外的所有连乘积
//注意总次数是r-l,不是r-l+1,本区间向乘上另一区间时次数会少一位(少乘了自己)
getadd(g,h,f+l,r-l);//合并
}
inline void getinterpolation(int *x,int *y,int n,int *f)
{
static int consmul[D][N<<1],res[N],val[N];
prework(0,1,n,x,consmul);//预处理连乘积
getderivative(n,consmul[0],res);//处理g'(x)
getval(res,x,n,val);//多点求值得到gi(xi)
for(int i=1;i<=n;++i)
val[i]=mul(y[i],inv[val[i]]);
getinterpolation(0,1,n,val,consmul,f);//最后乘上式子右边一部分
}
注:此代码没有提交过,不保证正确性。
参考资料
- FFT 学习笔记-menci
- 有关多项式处理的各种算法总结-zlttttt
- 多项式求ln,求exp,开方,快速幂 学习总结-_Vertical
- 多项式的运算-KirinBill
- [拉格朗日反演][FFT][NTT][多项式大全]详解-TJY
- Pick’s Blog
- Miskcoo’s Space
- 2015国家集训队论文 鏼 生成函数的运算与组合计数问题
- 2016国家集训队论文 myy 再探快速傅里叶变换
- Foolmike的多项式课件
膜膜膜众神犇。也非常感谢能有这么多这么好的资源。
如果有错误欢迎指出,有疑问欢迎评论。