浅谈多项式全家桶与实现
- 2026/2/12 upd:重写本文,将每个函数单独开在一个
namespace里,添加了多项式复合逆 - 2021/5/8 upd:修补了一些锅,增加了大量技术,多项式全家桶基本完成
封装模式和代码习惯
本文与(我发布的)其他介绍知识点的博客不同,更侧重于具体实现和封装细节,也是提供一种多项式模板的封装实现方式。有关更多多项式和生成函数的技巧请参考 Re:从零开始的生成函数 by me。
本文模板的封装较为严密和安全,因此需要牺牲轻微的常数以及灵活性,但使用更加方便。请根据自己的需要进行修改和使用。具体代码封装模式为:
- 每个模板开一个独立的
namespace,内含一个主函数形如void solve(int *s, int *input, int arg),其中s是输出数组,input是输入数组,arg是一些额外的参数(如多项式次数等),因具体问题而异。 - 传入的数组指针均可以相同,
solve函数会保证不破坏input数组的内容,并且不访问超出传入次数的数组元素。 namespace内的大写字母ABC等数组表示的是临时数组。
0. 前置知识
[Under Construction]
1. 多项式复合简单函数
一些常见的多项式复合简单函数,可以认为是多项式工业内的“四则运算”。复杂度均为 \(\Theta(n \log n)\)。
顺便一提,多项式复合任意函数已经可以做到 \(\Theta(n \log^2 n)\)。
1.1 NTT
最基础的多项式操作,其它运算都以此为基础,正因此这个模块对常数的影响很大。这里的实现比较基础,没有用到一些优化技巧,若对常数要求较高可以替换为其他实现。
const int N=3e6+5,mod=998244353,g=3;
int ksm(int a,int x){
int tot=1;
while(x){
if(x & 1ll) tot=1ll*tot*a%mod;
a=1ll*a*a%mod;
x>>=1ll;
}
return tot;
}
void gmodn(int &x){ x+=x>>31 & mod; }
void gmod(int &x) { x%=mod; }
namespace NTT{
int A[N],B[N],C[N],rev[N];
int init(int n){
int lim=0;
while((1<<lim)<=n) lim++;
for(int i=0;i<=(1<<lim)-1;i++)
rev[i]=(rev[i>>1] >> 1) | ((i & 1)<<(lim-1));
return lim;
}
void ntt(int *f,int n,int opt){
for(int i=0;i<=n-1;i++)
if(i<rev[i]) swap(f[i],f[rev[i]]);
for(int l=1;l<n;l<<=1ll){
int tmp=ksm(g,(mod-1)/(l*2));
if(opt==-1) tmp=ksm(tmp,mod-2);
for(int i=0;i<=n-1;i+=l<<1ll){
int omegf=1;
for(int j=0;j<l;j++){
int x=f[i+j],y=1ll*omegf*f[i+j+l]%mod;
gmodn(f[i+j]=x+y-mod);
gmodn(f[i+j+l]=x-y);
omegf=1ll*omegf*tmp%mod;
}
}
}
if(opt==-1){
int t=ksm(n,mod-2);
for(int i=0;i<=n-1;i++) f[i]=1ll*f[i]*t%mod;
}
}
void solve(int *s,int* f,int* g,int n,int m){
if(n+m<=150){
for(int i=0;i<=n+m;i++) C[i]=0;
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
gmodn(C[i+j]+=1ll*f[i]*g[j]%mod-mod);
for(int i=0;i<=n+m;i++) s[i]=C[i];
return ;
}
int lim=init(n+m);
for(int i=0;i<(1ll<<lim);i++) A[i]=B[i]=0;
for(int i=0;i<=n;i++) A[i]=f[i];
for(int i=0;i<=m;i++) B[i]=g[i];
ntt(A,(1<<lim),1);
ntt(B,(1<<lim),1);
for(int i=0;i<(1<<lim);i++) C[i]=1ll*A[i]*B[i]%mod;
ntt(C,(1<<lim),-1);
for(int i=0;i<=n+m;i++) s[i]=C[i];
}
}
1.1A 任意模数 NTT
三模 NTT
用三个模数 \(998244353\) / \(1004535809\) / \(469762049\) (原根都是 \(3\))分别做一遍再 CRT 合并起来即可。常数较大。
namespace EXCRT{
int solve(int x1,int x2,int x3,int mod){
int k1=(x2+mod2-x1)%mod2*ksm(mod1,mod2-2,mod2)%mod2;
int x4=x1+k1*mod1;
int k4=(x3+mod3-x4%mod3)%mod3*ksm(mod1*mod2%mod3,mod3-2,mod3)%mod3;
return (x4%mod+k4%mod*mod1%mod*mod2%mod)%mod;
}
}
int st1[N],st2[N],st3[N];
void MTT(int *s,int *a,int *b,int n,int m,int p){
NTT::solve(st1,a,b,n,m,mod1);
NTT::solve(st2,a,b,n,m,mod2);
NTT::solve(st3,a,b,n,m,mod3);
for(int i=0;i<=n+m;i++){
s[i]=EXCRT::solve(st1[i],st2[i],st3[i],p);
}
}
FFT
直接进行 FFT 会爆精度。这里实现的是拆系数 5 次 FFT 的做法,具体推导可见 题解 P4245 【【模板】任意模数NTT】 by command_block。
const double PI=acos(-1);
int mod;
struct cpl{
double A,B;
};
inline cpl operator +(cpl X,cpl Y){
return (cpl){X.A+Y.A,X.B+Y.B};
}
inline cpl operator -(cpl X,cpl Y){
return (cpl){X.A-Y.A,X.B-Y.B};
}
inline cpl operator *(cpl X,cpl Y){
return (cpl){X.A*Y.A-X.B*Y.B,X.A*Y.B+X.B*Y.A};
}
namespace FFT{
cpl A[N],B[N],C[N],S1[N],S2[N],pre[N];
int S[N];
int rev[N];
int init(int n,int m){
int lim=0;
while((1ll<<lim)<=n+m) lim++;
for(int i=0;i<(1<<lim);i++)
rev[i]=(rev[i>>1ll]>>1ll) | ((i & 1ll)<<(lim-1));
for(int i=0;i<=(1<<lim);i++)
pre[i]={cos(2*PI*i/(1<<lim)),sin(2*PI*i/(1<<lim))};
return lim;
}
void fft(cpl *a,int n,int opt){
for(int i=0;i<n;i++)
if(i<rev[i]) swap(a[i],a[rev[i]]);
for(int l=1;l<n;l*=2){
for(int i=0;i<n;i+=l*2){
int u=n/l/2,nw=(opt==1)?0:n;
for(int j=0;j<l;j++){
cpl x=a[i+j],y=pre[nw]*a[i+j+l];
a[i+j]=x+y,a[i+j+l]=x-y;
nw+=opt*u;
}
}
}
if(opt==-1){
for(int i=0;i<n;i++) a[i].A=a[i].A/n,a[i].B=a[i].B/n;
}
}
void solve(int *s,int *f,int *g,int n,int m,int mod){
if(n+m<=200){
for(int i=0;i<=n+m;i++) S[i]=0;
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
S[i+j]=(S[i+j]+1ll*f[i]*g[j]%mod)%mod;
}
}
for(int i=0;i<=n+m;i++) s[i]=S[i];
return ;
}
int M=sqrt(mod);
int lim=init(n,m);
for(int i=0;i<=n;i++) A[i]=(cpl){f[i]%M,f[i]/M},B[i]=(cpl){f[i]%M,-f[i]/M};
for(int i=0;i<=m;i++) C[i]=(cpl){g[i]%M,g[i]/M};
for(int i=n+1;i<(1ll<<lim);i++) A[i]=B[i]=(cpl){0,0};
for(int i=m+1;i<(1ll<<lim);i++) C[i]=(cpl){0,0};
fft(A,(1<<lim),1);
fft(B,(1<<lim),1);
fft(C,(1<<lim),1);
for(int i=0;i<(1<<lim);i++) S1[i]=A[i]*C[i],S2[i]=B[i]*C[i];
fft(S1,(1<<lim),-1);
fft(S2,(1<<lim),-1);
for(int i=0;i<=n+m;i++){
long long a=round((S1[i].A+S2[i].A)/2),b=round((S2[i].A-S1[i].A)/2),c=round(S1[i].B);
s[i]=(a%mod+b%mod*M%mod*M%mod+c%mod*M%mod)%mod;
}
}
}
1.2 多项式求逆
- 给出 \(F(x)\) ,求出 \(G(x)\) 使得 \(F(x)G(x) \equiv 1 \pmod {x^n}\)
记 \(H(t)=F(x)- \dfrac{1}{t}\) ,则有 \(H(G(x)) \equiv 0 \pmod{x^n}\)
用牛顿迭代求 \(H(G(x))\) 的零点。假设已经求出了 \(H(G_0(x)) \equiv 0 \pmod{x^\frac{n}{2}}\),
递归边界为 \(G_0(x) = \dfrac{1}{f_0}\)。
\(O(n \log n)\)
namespace INV{
int A[N],B[N],S[N];
void solve(int *s,int *f,int n){
S[0]=ksm(f[0],mod-2);
S[1]=0;
for(int len=2;len<=(n<<1);len<<=1){
int lim=len<<1;
for(int i=0;i<lim;i++) A[i]=B[i]=0;
for(int i=0;i<=min(len-1,n);i++) A[i]=f[i]; // 避免越界访问
for(int i=0;i<len;i++) B[i]=S[i];
int t=NTT::init(len);
NTT::ntt(A,lim);
NTT::ntt(B,lim);
for(int j=0;j<lim;j++)
S[j]=(2*B[j]%mod+mod-1ll*A[j]*B[j]%mod*B[j]%mod)%mod;
NTT::intt(S,lim);
for(int j=len;j<lim;j++) S[j]=0;
}
for(int i=0;i<=n;i++) s[i]=S[i];
}
}
1.3 多项式对数函数(ln)
- 给出 \(F(x)\) ,求 \(G(x)=\ln(F(x)) \pmod{x^n}\)。保证 \(f_0=1\)。
对两边求导:
求导,求逆,再积分即可。
namespace LN{
int A[N],B[N];
void solve(int *s,int *f,int n){
for(int i=0;i<=n;i++) A[i]=f[i],B[i]=0;
for(int i=1;i<=n;i++) B[i-1]=1ll*f[i]*i%mod;
INV::solve(A,A,n);
NTT::solve(A,A,B,n,n);
for(int i=n-1;i>=0;i--) A[i+1]=1ll*A[i]*ksm(i+1,mod-2)%mod;
A[0]=0;
for(int i=0;i<=n;i++) s[i]=A[i];
}
}
1.4 多项式指数函数(exp)
- 给出 \(F(x)\) ,求出 \(G(x)\) 使得 \(G(x) \equiv e^{F(x)} \pmod {x^{n}}\)。保证 \(f_0=0\)。
记 \(H(t)=F(x)- ln(t)\) ,则有 \(H(G(x)) \equiv 0 \pmod{x^n}\) 。上牛顿迭代,假设已经求出了 \(H(G_0(x)) \equiv 0 \pmod{x^\frac{n}{2}}\)
namespace EXP{
int A[N],B[N],C[N],S[N];
void solve(int *s,int *f,int n){
A[0]=B[0]=C[0]=0; S[0]=1;
for(int len=2;len/2<=n;len<<=1){
int lim=len<<1;
for(int i=0;i<lim;i++) A[i]=B[i]=0;
for(int i=0;i<=min(len-1,n);i++) A[i]=f[i];
for(int i=0;i<len;i++) B[i]=S[i];
LN::solve(C,B,len-1);
for(int i=0;i<len;i++) gmodn(C[i]=A[i]-C[i]);
C[0]=(C[0]+1)%mod;
NTT::init(lim-1);
NTT::ntt(B,lim);
NTT::ntt(C,lim);
for(int j=0;j<lim;j++) S[j]=1ll*B[j]*C[j]%mod;
NTT::intt(S,lim);
for(int j=len;j<lim;j++) S[j]=0;
}
for(int i=0;i<=n;i++) s[i]=S[i];
}
}
1.5 多项式开根
- 给出多项式 \(F(x)\) ,求 \(G(x)\) 使 \(G(x)^2 \equiv F(x) \pmod{x^{n}}\)。
记 \(H(t)=F(x)- t^2\) ,则有 \(H(G(x)) \equiv 0 \pmod{x^n}\)
上牛顿迭代,假设已经求出了 \(H(G_0(x)) \equiv 0 \pmod{x^\frac{n}{2}}\),
注意:边界条件对 \(f_0\) 开根可能得到两个结果,按实际情况选择即可。
namespace SQRT{
int A[N],B[N],C[N],S[N];
void solve(int *s,int *f,int n){
int p=BSGS(3,998244353,f[0]),s0=ksm(3,p/2);
S[0]=min(s0,(mod-s0)%mod);
S[1]=0;
for(int len=2;len/2<=n;len<<=1){
int lim=len<<1;
for(int i=0;i<lim;i++) A[i]=B[i]=C[i]=0;
for(int i=0;i<=min(len-1,n);i++) A[i]=f[i];
for(int i=0;i<len;i++) B[i]=S[i];
INV::solve(C,B,len-1);
NTT::init(lim-1);
NTT::ntt(A,lim,1);
NTT::ntt(C,lim,1);
for(int j=0;j<lim;j++) S[j]=A[j]*C[j]%mod;
NTT::ntt(S,lim,-1);
for(int i=0;i<len;i++) S[i]=(S[i]+B[i])%mod*inv2%mod;
for(int j=len;j<lim;j++) S[j]=0;
}
for(int i=0;i<=n;i++) s[i]=S[i];
}
}
1.6 多项式除法(取模)
- 给出 \(n\) 次多项式 \(F(x)\) 和 \(m\) 次多项式 \(G(x)\) ,求 \(n-m\) 次多项式 \(Q(x)\) 和不超过 \(m-1\) 次多项式 \(R(x)\) 使得 \(F(x)=Q(x)G(x)+R(x)\)。
考虑将系数对称:
考虑先求出 \(Q(x)\) ,模上 \(x^{n-m+1}\):
求出 \(Q(x)\) 后 \(R(x)\) 也很简单了:
namespace DIV{
int A[N],B[N],C[N],D[N];
void solve(int *q,int* r,int *f,int *g,int n,int m){
for(int i=0;i<=n;i++) A[i]=C[i]=f[i];
for(int i=0;i<=m;i++) B[i]=D[i]=g[i];
reverse(A,A+n+1);
reverse(B,B+m+1);
INV::solve(B,B,n-m);
NTT::solve(q,A,B,n,n-m);
for(int i=n-m+1;i<=n+n-m;i++) q[i]=0;
reverse(q,q+n-m+1);
NTT::solve(D,D,q,m,n-m);
for(int i=0;i<=n;i++) r[i]=(C[i]+mod-D[i])%mod;
}
}
1.7 多项式快速幂
- 给出多项式 \(F(x)\) ,求 \(F(x)^k \pmod{x^n}\)
当 \(f_0=1\) 时,可以直接取对数,
这里的 \(k\) 可以对 \(\bmod -1\) 取模。
\(O(n\log n)\)
void Pow(int *s,int *f,int n,int k){
int lim=NTT::init(n,n);
for(int i=n+1;i<(1ll<<lim);i++) A[i]=0;
for(int i=0;i<=n;i++) A[i]=f[i];
Ln(A,A,n);
Mult(A,A,n,k);
Exp(A,A,n);
for(int i=0;i<=n;i++) s[i]=A[i];
}
不保证 \(f_0=1\) 时会有一些 Corner Case。\(f_0 \neq 0\) 时,直接除掉即可;\(f_0 = 0\) 时则找到第一个不为 \(0\) 的系数(记为第 \(t\) 项),然后除掉 \(x^t\) 即可。记 \(p=[x^t]f(x)\),
注意当 \(tk>n\) 时需要特判。
void Pow(int *s,int *f,int n,int k1,int k2,int lenk){
//k1 是对 mod 取模, k2 是对 mod-1 取模,lenk 是 k 的长度
static int A[N];
int lim=NTT::init(n,n);
int t=0;
while(!f[t]) t++;
if(lenk>log(n) && t){
for(int i=0;i<=n;i++) s[i]=0;
return ;
}
int p=ksm(f[t],k2),q=ksm(f[t],mod-2);
n-=t;
for(int i=0;i<=n;i++) A[i]=f[i+t]*q%mod;
for(int i=n+1;i<(1ll<<lim);i++) A[i]=0;
Ln(A,A,n);
Mult(A,A,n,k1);
Exp(A,A,n);
for(int i=0;i<=n+t;i++) s[i]=0;
for(int i=0;i+t*k1<=n+t;i++)
s[i+t*k1]=A[i]*p%mod;
}
1.8 多项式三角函数
@warn 该模板不常用
@todo 更新代码为新模板
- 给出多项式 \(F(x)\) ,求 \(sin(x)\),\(cos(x)\) 与 \(tan(x)\)。保证 \(f_0=0\)。
考虑复数的指数形式(欧拉公式):
void Sin(int *s,int *f,int n){
int I=mod-ksm(g,(mod-1)/4);
static int A[N],B[N];
for(int i=0;i<=n;i++) A[i]=f[i]*I%mod;
poly::Exp(A,A,n);
INV::solve(B,A,n);
int t=ksm(2*I%mod,mod-2);
for(int i=0;i<=n;i++) s[i]=(A[i]+mod-B[i])%mod*t%mod;
}
void Cos(int *s,int *f,int n){
int I=mod-ksm(g,(mod-1)/4);
static int A[N],B[N];
for(int i=0;i<=n;i++) A[i]=f[i]*I%mod;
poly::Exp(A,A,n);
INV::solve(B,A,n);
int t=inv2;
for(int i=0;i<=n;i++) s[i]=(A[i]+B[i])%mod*t%mod;
}
1.9 多项式反三角函数
@warn 该模板不常用
@todo 更新代码为新模板
- 给出多项式 \(F(x)\) ,求 \(\arcsin(x)\),\(\arccos(x)\) 与 \(\arctan(x)\)。保证 \(f_0=0\)。
直接求导:
于是:
void Arcsin(int *s,int *f,int n){
static int A[N],B[N];
for(int i=0;i<=n;i++) A[i]=f[i],B[i]=f[i];
NTT::solve(A,A,A,n,n);
for(int i=0;i<=n;i++) A[i]=(mod-A[i])%mod;
A[0]=(A[0]+1)%mod;
poly::Sqrt(A,A,n);
INV::solve(A,A,n);
poly::Deriv(B,B,n);
NTT::solve(A,A,B,n,n);
poly::Limit(A,A,n);
for(int i=0;i<=n;i++) s[i]=A[i];
}
void Arctan(int *s,int *f,int n){
static int A[N],B[N];
for(int i=0;i<=n;i++) A[i]=f[i],B[i]=f[i];
NTT::solve(A,A,A,n,n);
A[0]=(A[0]+1)%mod;
INV::solve(A,A,n);
poly::Deriv(B,B,n);
NTT::solve(A,A,B,n,n);
poly::Limit(A,A,n);
for(int i=0;i<=n;i++) s[i]=A[i];
}
2. 复杂问题
本章收录一些目前只能做到 \(\Theta(n \log^2 n)\) 的问题。
2.1 多项式多点求值
- 给出多项式 \(F(x)\) 并给定 \(m\) 处位置 \(a_1,a_2,\cdots,a_m\),对每个给出的位置 \(a_i\) 求点值 \(F(a_i)\)。
算法一
若 \(F(x)=Q(x)(x-x_0)+R(x)\),则 \(F(x_0)=R(x_0)\)。所以我们只要对每个 \(x_i\) 求出取模后的余式 \(R(x)\) 即可。
考虑分治,先分治乘处理出 \(\prod\limits_{i=l}^r (x-x_i)\)。若当前获得了 \(F(x)=Q(x) \prod\limits_{i=l}^r (x-x_i) +R(x)\),那么分治下去只要对两边分别取模就可以了,显然次数会减半。
算法二
多项式除法的常数很大,我们希望用其他方式替代。考虑多项式除法的本质:
这里 \(Q(x)\) 是一次的,\(R(x)\) 是零次的,所以只要求出 \(Q(x)\) 的常数项即可得到答案。考虑分治:
试着推一下 \(G_{R_0}^{-1}(x)\) 与 \(G_R^{-1}(x)\) 的关系:
于是:
所以算到底就相当于 \(F_R(x)\) 乘上若干个 \(G_{R}(x)\)。
直接实现复杂度是错的,但注意到我们只需要 \(Q(x)\) 的常数项,即 \([x^{n-1}]Q_R(x)\)。所以乘到某一个区间时,只有最高的 \(r-l+1\) 项有用,因为之后乘的东西不超过 \(r-l+1\) 次。
只维护 \(Q_R(x)\) 的最高 \(r-l+1\) 项,时间复杂度就对了,\(\Theta(n \log^2 n)\)。同时这样每轮只需要做两次多项式乘法,常数很小。
namespace Evaluation{
int tmp[N];
vector <int> P[N],Q[N],Qn[N];
int lenp[N],lenq[N];
void init(int o,int l,int r){
int len=r-l+1,mid=(l+r)>>1ll;
lenp[o]=len;
lenq[o]=len-1;
P[o].resize(2*lenp[o]+2);
Q[o].resize(2*lenq[o]+2);
if(l==r) return ;
init(o<<1,l,mid);
init(o<<1|1,mid+1,r);
}
void solve1(int *a,int o,int l,int r){
if(l==r){
P[o]={1,(mod-a[l])%mod};
return ;
}
int mid=(l+r)>>1;
solve1(a,o<<1,l,mid);
solve1(a,o<<1|1,mid+1,r);
NTT::solve(P[o].data(),P[o<<1].data(),P[o<<1|1].data(),lenp[o<<1],lenp[o<<1|1]);
}
void solve2(int *s,int *a,int o,int l,int r){
if(l==r){
s[l]=Q[o][0];
return ;
}
int mid=(l+r)>>1;
int len1=mid-l+1,len2=r-mid;
NTT::solve(tmp,Q[o].data(),P[o<<1|1].data(),lenq[o],lenp[o<<1|1]);
for(int i=0;i<len1;i++) Q[o<<1][i]=tmp[i+len2];
NTT::solve(tmp,Q[o].data(),P[o<<1].data(),lenq[o],lenp[o<<1]);
for(int i=0;i<len2;i++) Q[o<<1|1][i]=tmp[i+len1];
solve2(s,a,o<<1,l,mid);
solve2(s,a,o<<1|1,mid+1,r);
}
// f: [0,n]; y: [1,m]
void solve(int *s,int *f,int *a,int n,int m){
m=max(n,m); n=max(n,m);
init(1,1,m);
solve1(a,1,1,m);
poly::Rev(f,f,n);
INV::solve(P[1].data(),P[1].data(),lenp[1]);
NTT::solve(tmp,f,P[1].data(),n,lenp[1]);
for(int i=0;i<=m;i++) Q[1][i]=tmp[i];
solve2(s,a,1,1,m);
for(int i=1;i<=m;i++){
s[i]=(f[n]+1ll*s[i]*a[i]%mod)%mod;
}
}
}
2.2 多项式快速插值
- 给出 \(n+1\) 处点值 ,求同时满足这些点值的 \(n\) 次多项式 \(F(x)\)。
直接使用拉格朗日插值:
考虑算这个系数,设 \(G(x)= \prod\limits_{i=1}^n (x-x_i)\),那么这个系数就是 \(c_i =\Big[ \dfrac{G(x)}{x-x_i} \Big](x_i)\)。当然这个记号并不严谨,因为分子分母都是 \(0\),但我们可以用极限的方式来理解,使用洛必达法则:
同样考虑分治:
多项式快速插值问题同样只能做到 \(\Theta(n\log^2n)\)。
@remark 多项式快速插值有常数更小的利用转置原理的做法。
namespace Interpolation{
int tmp[N];
vector <int> P[N],Q[N];
int lenp[N],lenq[N];
int g[N],gv[N];
void init(int o,int l,int r){
int len=r-l+1,mid=(l+r)>>1ll;
lenp[o]=len;
lenq[o]=len-1;
P[o].resize(2*lenp[o]+2);
Q[o].resize(2*lenq[o]+2);
if(l==r) return ;
init(o<<1,l,mid);
init(o<<1|1,mid+1,r);
}
void solve1(int *x,int o,int l,int r){
if(l==r){
P[o]={(mod-x[l])%mod,1};
return ;
}
int mid=(l+r)>>1;
solve1(x,o<<1,l,mid);
solve1(x,o<<1|1,mid+1,r);
NTT::solve(P[o].data(),P[o<<1].data(),P[o<<1|1].data(),lenp[o<<1],lenp[o<<1|1]);
}
void solve2(int *x,int *y,int o,int l,int r){
if(l==r){
Q[o]={1ll*y[l]*ksm(gv[l],mod-2)%mod};
return ;
}
int mid=(l+r)>>1;
solve2(x,y,o<<1,l,mid);
solve2(x,y,o<<1|1,mid+1,r);
int tmp[N];
NTT::solve(Q[o].data(),Q[o<<1].data(),P[o<<1|1].data(),lenq[o<<1],lenp[o<<1|1]);
NTT::solve(tmp,Q[o<<1|1].data(),P[o<<1].data(),lenq[o<<1|1],lenp[o<<1]);
for(int i=0;i<=lenq[o];i++) Q[o][i]=(Q[o][i]+tmp[i])%mod;
}
//x: [1,n]; y: [1,n]
void solve(int *s,int *x,int *y,int n){
init(1,1,n);
solve1(x,1,1,n);
poly::Deriv(g,P[1].data(),lenp[1]);
Evaluation::solve(gv,g,x,lenp[1]-1,n);
solve2(x,y,1,1,n);
for(int i=0;i<=n;i++) s[i]=Q[1][i];
}
}
2.3 多项式复合逆
- 给出多项式 \(F(x)\),求 多项式 \(G(x)\) 使得 \(G(F(x)) \equiv x \pmod{x^n}\)。保证 \(f_0=0\) 且 \(f_1 \neq 0\)。
@TODO 推导
namespace N_COEF_PROBLEM{
// MAX: 6n+O(1)
// Solve S_k = [x^n] f^k
// Require f_0 = 0
int A[N],B[N],C[N],D[N],P[N];
void solve(int *s,int *f,int n){
int sn=n;
int d=1; // deg of y
for(int i=0;i<=n*3+2;i++) A[i]=B[i]=0;
A[0]=B[0]=1; // A = 1
for(int i=0;i<=n;i++) B[i*3+1]=(mod-f[i])%mod; // B = 1-yF
while(n){
int siz=n*(d*2+1)+d*2;
for(int i=0;i<=siz;i++) P[i]=0;
for(int i=0;i<=n;i++){
for(int j=0;j<=d;j++){
int id=i*(d*2+1)+j;
P[id]=1ll*B[id]*ID(i)%mod;
}
}
NTT::solve(C,A,P,siz,siz);
NTT::solve(D,B,P,siz,siz);
int nn=n/2,nd=d*2,nsiz=n*(d*2+1)+d*2;
for(int i=0;i<=nsiz;i++) A[i]=B[i]=0;
for(int i=(n & 1);i<=nn*2+1;i+=2){
for(int j=0;j<=d*2;j++){
int id=i*(d*2+1)+j;
int nid=(i/2)*(nd*2+1)+j;
A[nid]=C[id];
}
}
for(int i=0;i<=nn*2;i+=2){
for(int j=0;j<=d*2;j++){
int id=i*(d*2+1)+j;
int nid=(i/2)*(nd*2+1)+j;
B[nid]=D[id];
}
}
n=nn,d=nd;
}
n=sn;
INV::solve(P,B,n);
NTT::solve(A,A,P,n,n);
for(int i=0;i<=n;i++) s[i]=A[i];
}
}
namespace COMP_INV{
// MAX: 6n+O(1)
int A[N],B[N];
void solve(int *s,int *f,int n){
int kw=ksm(f[1],mod-2);
N_COEF_PROBLEM::solve(A,f,n);
int iw=ksm(A[n],mod-2);
for(int k=0;k<=n;k++) B[n-k]=1ll*A[k]*n%mod*ksm(k,mod-2)%mod*iw%mod;
LN::solve(B,B,n);
int val=(mod-ksm(n,mod-2))%mod; // B^{-1/n}
for(int i=0;i<=n;i++) B[i]=1ll*B[i]*val%mod;
EXP::solve(B,B,n);
s[0]=0;
for(int i=1;i<=n;i++) s[i]=1ll*B[i-1]*kw%mod;
}
}
3. 分块多项式
3.1 快速阶乘算法
- 给出 \(n\),求 \(n! \pmod{p}\)。
令 \(B = \lfloor \sqrt n \rfloor\)。
考虑维护 \(F_d(0), F_d(B), \cdots, F_d(dB)\),转移至 \(2d\) 或 \(d+1\)。
对于转移至 \(2d\):
设 \(H(x) = F_d(x/B)\),那么求 \(F_d(x+d)\) 就是对 \(d+1\) 个连续点值偏移 \(d/B\),可以在 \(\Theta(d \log d)\) 内解决。
对于转移至 \(d+1\):
直接计算即可。总时间复杂度 \(\Theta(\sqrt{n} \log n)\)。
namespace trans{
int A[N],B[N],S[N];
int tmul[N],tinv[N];
void solve(int *s,int *f,int n,long long m){
tmul[0]=tinv[0]=1;
for(int i=1;i<=2*n;i++) tmul[i]=1ll*tmul[i-1]*(m+mod-n+i)%mod;
tinv[2*n]=ksm(tmul[2*n],mod-2);
for(int i=2*n-1;i>=1;i--) tinv[i]=1ll*tinv[i+1]*(m+mod-n+i+1)%mod;
tinv[0]=ksm((m+mod-n)%mod,mod-2);
for(int i=1;i<=2*n;i++) tinv[i]=1ll*tinv[i]*tmul[i-1]%mod;
for(int i=0;i<=2*n;i++){
A[i]=1ll*f[i]*inv[i]%mod*inv[n-i]%mod*ID(n-i)%mod;
B[i]=tinv[i];
}
FFT::solve(S,A,B,n,2*n,mod);
int tmp=1;
for(int i=m;i>=m-n;i--) tmp=1ll*tmp*i%mod;
for(int i=0;i<=n;i++){
s[i]=1ll*S[i+n]*tmp%mod;
tmp=1ll*tmp*(m+i+1)%mod*tinv[i]%mod;
}
}
}
int T,n,B;
int F[N],G[N];
int d;
void shift(){
trans::solve(G,F,d,d+1);
for(int i=0;i<=d;i++) F[i+d+1]=G[i];
trans::solve(G,F,2*d,1ll*d*ksm(B,mod-2)%mod);
for(int i=0;i<=2*d;i++) F[i]=1ll*F[i]*G[i]%mod;
d*=2;
}
void setbit(){
for(int i=0;i<=d;i++) F[i]=1ll*F[i]*(1ll*i*B%mod+d+1)%mod;
F[d+1]=1;
for(int i=1;i<=d+1;i++) F[d+1]=1ll*F[d+1]*(1ll*(d+1)*B%mod+i)%mod;
d++;
}
int solve(int n,int mod){
bool fl=0;
if(n>mod/2) n=mod-1-n,fl=1;
B=sqrt(n);
int lim=log2(B);
init(B);
d=1; F[0]=1,F[1]=B+1;
for(int i=lim-1;i>=0;i--){
shift();
if(B>>i & 1) setbit();
}
int ans=1;
for(int i=0;i<d;i++) ans=1ll*ans*F[i]%mod;
for(int i=B*B+1;i<=n;i++) ans=1ll*ans*i%mod;
if(!fl) return ans;
else return 1ll*ID(n+1)*ksm(ans,mod-2)%mod;
}
3.1A 带系数快速阶乘算法
- 给出 \(n,a,b\),求 \(\prod\limits_{i=1}^{n} (a+bi) \pmod{p}\)。
...
int d;
void shift(){
trans::solve(G,F,d,d+1);
for(int i=0;i<=d;i++) F[i+d+1]=G[i];
trans::solve(G,F,2*d,1ll*d*ksm(B,mod-2)%mod);
for(int i=0;i<=2*d;i++) F[i]=1ll*F[i]*G[i]%mod;
d*=2;
}
void setbit(){
for(int i=0;i<=d;i++) F[i]=1ll*F[i]*(1ll*a*B%mod*i%mod+1ll*a*(d+1)%mod+b)%mod;
F[d+1]=1;
for(int i=1;i<=d+1;i++) F[d+1]=1ll*F[d+1]*(1ll*a*B%mod*(d+1)%mod+1ll*a*i%mod+b)%mod;
d++;
}
int solve(int n,int mod){
B=sqrt(n);
int lim=log2(B);
init(B);
d=1; F[0]=(a+b)%mod,F[1]=(1ll*a*B%mod+F[0])%mod;
for(int i=lim-1;i>=0;i--){
shift();
if(B>>i & 1) setbit();
}
int ans=1;
for(int i=0;i<d;i++) ans=1ll*ans*F[i]%mod;
for(int i=B*B+1;i<=n;i++) ans=1ll*ans*(1ll*a*i%mod+b)%mod;
return ans;
}
3.1B 快速阶乘和
- 给出 \(n\),求 \(\sum\limits_{i=1}^{n} i! \pmod{p}\)。
对于转移至 \(2d\):
对于转移至 \(d+1\):
int B;
int F[N],G[N];
int P[N],Q[N];
int d;
void shift(){
trans::solve(P,F,d,d+1);
trans::solve(Q,G,d,d+1);
for(int i=0;i<=d;i++) F[i+d+1]=P[i];
for(int i=0;i<=d;i++) G[i+d+1]=Q[i];
trans::solve(P,F,2*d,1ll*d*ksm(B,mod-2)%mod);
trans::solve(Q,G,2*d,1ll*d*ksm(B,mod-2)%mod);
for(int i=0;i<=2*d;i++){
G[i]=(G[i]+1ll*F[i]*Q[i]%mod)%mod;
F[i]=1ll*F[i]*P[i]%mod;
}
d*=2;
}
void setbit(){
for(int i=0;i<=d;i++){
F[i]=1ll*F[i]*(1ll*B*i%mod+(d+1))%mod;
gmod(G[i]+=F[i]);
}
F[d+1]=1; G[d+1]=0;
for(int i=1;i<=d+1;i++){
F[d+1]=1ll*F[d+1]*(1ll*B*(d+1)%mod+i)%mod;
gmod(G[d+1]+=F[d+1]);
}
d++;
}
int res0=0,res1=0;
void solve(int n){
B=sqrt(n);
int lim=__lg(B);
init(B);
d=1;
F[0]=1,F[1]=(B+1)%mod;
G[0]=1,G[1]=(B+1)%mod;
for(int i=lim-1;i>=0;i--){
shift();
if(B>>i & 1) setbit();
}
int ans=0,tot=1;
for(int i=0;i<d;i++){
gmod(ans+=1ll*tot*G[i]%mod);
tot=1ll*tot*F[i]%mod;
}
for(int i=B*B+1;i<=n;i++){
tot=1ll*tot*i%mod;
gmod(ans+=tot);
}
res0=tot,res1=ans;
}
4. 杂项
这一块严格来说并不算模板,只是记录一些常见的模型和处理方式。
4.1 分治乘
-
给定 \(n\) 个一次多项式 \(a_i+b_ix\),求 \(\prod\limits_{i=1}^n (a_i+b_ix)\)。
-
给定 \(n\) 个一次多项式 \(a_i+b_ix\),求 \(\sum\limits_{i=1}^n \dfrac{1}{a_i+b_ix}\)。
用类似分治的办法,尽量先乘小的区间,再合并大的区间。若初始多项式次数不一致,可以用 Huffman 算法的思路,优先队列维护当前的多项式,不断取出两个次数最小的多项式相乘。
时间复杂度是 \(T(n)=2T(n/2)+\Theta(n\log n)=\Theta(n\log^2n)\)
具体的实现可以用 vector 存多项式,空间复杂度是 \(\Theta(n\log n)\) 的
对于分式求和,对于每个区间同时维护分子和分母,最后再求逆算答案即可。
4.2 分治 FFT(半在线卷积)
离线分治 FFT
- 已知 \(F(x)\),求 \(G(x)\) 使 \(g_n=\sum\limits_{i=1}^n f_ig_{n-i}\),边界条件为 \(g_0=1\)。
如果知道 \(G(x)\) 的前 \(n\) 项,可以推出第 \(n+1\) 项。
类比 CDQ 分治:现在的区间 \(G\) 分治成左右两个区间(记作 \(G_l\) 与 \(G_r\)),考虑左边对右边的贡献。
再记 \(F_l'\) 与 \(F'\) 为 \(F(x)\) 的前缀多项式(不太严谨),且 \(\deg F_l'=\deg G_l\),\(\deg F'=\deg G\),即加上 \('\) 就代表被平移到了开头。
显然贡献只有 \(F' \times G_l\)。单次 NTT 的时间复杂度为 \(\Theta(n \log n)\),套上分治的总复杂度即为 \(\Theta(n \log^2 n)\)。
在线分治 FFT
- 求 \(F(x)\),\(G(x)\) 使 \(f_n=g_1 \oplus g_2 \oplus \cdots \oplus g_n\),\(g_n=\sum\limits_{i=1}^n f_ig_{n-i}\),边界条件为 \(f_0=g_0=0\),\(f_1=1\)。
现在必须知道 \(f_1,\cdots,f_n\),才能算出 \(f_{n+1}\),没办法再像刚才那样计算。
具体而言,如果左端点不为 \(1\),那 \(F'\) 已经被算出来了,没有问题。但若左端点为 \(1\),区间 \(G\) 没算出来,\(F'=F\) 自然也不知道。
那现在只考虑左端点为 \(1\) 的情况。这时候 \(F_r'\) 不知道,只能将 \(F_l' * G_l\) 贡献到 \(G_r\) 去。因此 \(G_r\) 还缺了 \(F_r' \times G_l\),我们暂且放在一边。幸运的是,这时候 \(G_r\) 中的第一项系数是正确的,\(F_r\) 的第一项和第二项也计算得出来。
因此现在考虑分治到 \(G_r\) 里面,再分治到区间 \(GG\),可以将 \(FF’ \times GG_l\) 贡献到 \(GG_r\)。但是别忘了,\(G_r\) 还缺了 \(F_r' * G_l\),因此还要再补上 \(FF \times GG_l’\)。
每分治到叶子节点就计算一些 \(f_n\),这样就正确了。
时间复杂度仍是 \(\Theta(n \log^2 n)\)。
4.3 下降幂多项式乘法
- 给出下降幂多项式 \(F(x),G(x)\) ,求下降幂多项式 \(H(x) = F(x) G(x)\)。
下降幂多项式可以轻松(指单 log)在系数形式与点值形式之间转换。具体推导可参见 Re:从零开始的生成函数魔法 Section 3.4,这里重复一遍:
设 \(a_i=F(i)\),\(A(x)\) 是 \(a_n\) 的生成函数
总时间复杂度仅为 \(\Theta(n \log n)\)。
void ftt(int *s,int *f,int n,int typ){
static int A[N];
if(typ==1){
A[1]=1;
NTT::solve(s,inv,f,n,n);
for(int i=0;i<=n;i++) s[i]=s[i]*mul[i]%mod;
}
else{
for(int i=0;i<=n;i++) f[i]=f[i]*inv[i]%mod;
for(int i=0;i<=n;i++) A[i]=inv[i]*ID(i)%mod;
NTT::solve(s,A,f,n,n);
}
}
void FTT(int *s,int *f,int *g,int n,int m){
static int A[N],B[N];
ftt(A,f,n+m,1);
ftt(B,g,n+m,1);
for(int i=0;i<=n+m;i++) s[i]=A[i]*B[i]%mod;
ftt(s,s,n+m,-1);
}
4.4 普通多项式与下降幂多项式互化
- 给出下降幂/普通多项式 \(F(x)\) ,求它的普通/下降幂多项式形式 \(G(x)\)。
这个问题是较为困难的,只能以点值为媒介进行转换,使用插值与求值来实现。时间复杂度为 \(\Theta(n \log^2 n)\) 且常数由插值与求值的常数决定。
void FtoP(int *s,int *f,int n){
static int A[N],B[N];
ftt(A,f,n,1);
for(int i=n+1;i>=1;i--) A[i]=A[i-1];
for(int i=1;i<=n+1;i++) B[i]=i-1;
Interpolation::solve(s,B,A,n+1);
}
void PtoF(int *s,int *f,int n){
static int A[N],B[N];
for(int i=1;i<=n+1;i++) B[i]=i-1;
Evaluation::solve(A,f,B,n,n+1);
for(int i=0;i<=n;i++) A[i]=A[i+1];
ftt(s,A,n,-1);
}

浙公网安备 33010602011771号