Bostan-Mori 算法
EI 哥哥科普到 OI 界的科技……最近出现了基于这个的多项式复合/复合逆复杂度的突破,所以今天去看了一下。
这是一个用于解决多项式有理分式的单项系数问题 的算法。该算法在解决常系数齐次线性递推问题时,代码明显短很多,常数相较原方法极其优越。
首先我们考虑用一次卷积将线性递推数列的生成函数写成有理分式的形式 (其中 是该线性递推特征多项式,卷一下初项截断得到 )。
然后考虑如何解决 。我们将其写成 ,而 有 ,所以其满足奇数次项都为零。
所以我们将 奇偶次项分开,设 ,那么原式就是 。
发现左边只会贡献到偶数次项,右边只会贡献到奇数次项。所以我们只需要往一边递归,然后将 变成 继续做就行了。每一层进行常数次长度为线性递推式长度 的卷积,复杂度 。
如果直接实现上述做法,每一层有两次卷积,一共 6 次长度为 的 DFT。事实上我们可以做到更优的常数。
我们考虑直接维护 的点值形式 。每次考虑从 推出 与 ,然后再对这两个多项式次数折半。
不妨假设 是 2 的整幂。
首先我们要把 的点值形式从长度至少为 拓展到长度至少为 的 。这一部分当然可以直接做一次长度为 的 IDFT,然后再做一次长度为 的 DFT,不过我们可以更好。
注意到 ,所以 IDFT 一次然后再做一次长度为 的 DFT 即可。
倍长两个点值数组后,考虑表示出 。注意到对于数组 ,。于是就得到了 的值。
那么我们点乘就可以得到 与 。考虑如何提取奇/偶次项系数。做变换 可以消去 中的奇/偶次项。我们已经会求 的 DFT 数组了。所以我们可以直接求出 。然后考虑将次数折半。对于只有偶次项的多项式,直接取前一半点值就行了,对于只有奇次项的多项式,取完前一半后还要除以 。
综上所述,我们一次迭代只需要进行 4 次长度恰好为 的 DFT,而 DFT 本身常数较小。所以说现在常系数齐次线性递推也可以承受 2e5 的数据范围了!
不仅如此,这个算法还可以用于解决特殊形式的有理二元分式求值。
对于 ,我们如果要求 ,可以对 这一维施加 Bostan-Mori 算法,这样迭代 次后 的次数不超过 , 的次数不超过 ,所以直接进行二元多项式卷积,复杂度就是 的了。
而对于多项式复合/复合逆问题,其转置问题恰好是二元多项式有理分式求值,由于我忘了转置原理所以说没有继续研究了,可以看 alpha1022 博客。
常系数齐次线性递推代码:
#include <cstdio> #include <algorithm> #include <numeric> #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin)),p1==p2?EOF:*p1++) char buf[1<<22],*p1=buf,*p2=buf; using namespace std; int read(){ char c=getchar();int x=0;bool f=0; while(c<48||c>57) f|=(c=='-'),c=getchar(); do x=x*10+(c^48),c=getchar(); while(c>=48&&c<=57); if(f) return -x; return x; } const int P=998244353; typedef long long ll; typedef unsigned long long ull; const int N=1<<20; int cw[N|1],rev[N],ccw[N|1]; int bit,len,ilen; int n,k; int qp(int a,int b=P-2){ int res=1; while(b){ if(b&1) res=(ll)res*a%P; a=(ll)a*a%P;b>>=1; } return res; } void init(int x){ bit=-1;len=1; while(len<=x) ++bit,len<<=1; int w=qp(3,(P-1)>>(bit+1)); cw[0]=cw[len]=1; for(int i=1;i<len;++i){ rev[i]=(rev[i>>1]>>1)|((i&1)<<bit); cw[i]=(ll)cw[i-1]*w%P; } ilen=qp(len); } void DFT(int *F){ static ull f[N]; for(int i=0;i<len;++i) f[i]=F[rev[i]]; for(int i=1,tt=len>>1;i<len;i<<=1,tt>>=1) for(int j=0;j<len;j+=(i<<1)) for(int k=j,t=0;k<(j|i);++k,t+=tt){ ull x=f[k],y=f[k|i]*cw[t]%P; f[k]+=y;f[k|i]=x+(P-y); } for(int i=0;i<len;++i) F[i]=f[i]%P; } int A[N],B[N],T[N]; void multi(int *F){ for(int i=0;i<len;++i) T[i]=F[i]; DFT(T);reverse(T+1,T+len); for(int i=0;i<len;++i) T[i]=(ll)T[i]*ccw[i]%P*ilen%P; DFT(T);for(int i=len-1;~i;--i) F[i<<1]=F[i],F[i<<1|1]=T[i]; } int main(){ n=read();k=read(); A[0]=1; for(int i=1;i<=k;++i){ A[i]=(-read())%P; if(A[i]<0) A[i]+=P; } for(int i=0;i<k;++i){ B[i]=read()%P; if(B[i]<0) B[i]+=P; } init(k<<1); DFT(A);DFT(B); for(int i=0;i<len;++i) B[i]=(ll)B[i]*A[i]%P; for(int i=0;i<=len;++i) ccw[i]=cw[i]; DFT(B);reverse(B+1,B+len); for(int i=0;i<(len>>1);++i) A[i]=A[i<<1],B[i]=(ll)B[i]*ilen%P; for(int i=(len>>1);i<len;++i) A[i]=B[i]=0; init(k); for(int i=k;i<len;++i) B[i]=0; DFT(B); while(n){ multi(A);multi(B); for(int i=0;i<(len<<1);++i) B[i]=(ll)B[i]*A[i^len]%P; for(int i=0;i<len;++i){ A[i]=(ll)A[i]*A[i|len]%P; if(n&1) (B[i]-=B[i|len])<0&&(B[i]+=P); else (B[i]+=B[i|len])>=P&&(B[i]-=P); if(B[i]&1) B[i]+=P; B[i]>>=1; A[i|len]=B[i|len]=0; } if(n&1){ for(int i=0;i<len;++i) B[i]=(ll)B[i]*ccw[(len<<1)-i]%P; } n>>=1; } printf("%d\n",int(accumulate(B,B+len,0ll)%P*qp(accumulate(A,A+len,0ll)%P)%P)); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】