「学习笔记」多项式全家桶

FFT

struct node{
    long double x,y;
    node(){}
    node(long double xx,long double yy){x=xx; y=yy; return ;}
    node operator +(const node &a)const{return node(x+a.x,y+a.y);}
    node operator -(const node &a)const{return node(x-a.x,y-a.y);}
    node operator *(const node &a)const{
        return node(x*a.x-y*a.y,x*a.y+y*a.x);}
    }
    inline node conj(){return node(x,-y);}
};
inline void FFT(node *f,int lim,int opt){
	rep(i,0,lim-1){
        r[i]=r[i>>1]>>1|((i&1)?(lim>>1):0)if(i<r[i]) swap(f[i],f[r[i]]);
    }
    for(int p=2;p<=lim;p<<=1){
	    int len=p>>1; node tt=W[0]=node(1,0); 
        rep(i,1,len-1) W[i]=node(cos(pi/len*i),opt*sin(pi/len*i));
	    for(int k=0;k<lim;k+=p){
            for(int l=k;l<k+len;++l){
                tt=f[l+len]*W[l-k];
                f[len+l]=f[l]-tt,f[l]=f[l]+tt;
            }
        }
    } if(opt==-1) rep(i,0,lim-1) f[i].x/=lim; return ;
}

提升精度的方式是按照上面给出的代码写,而不是整个 buf 每次乘

万径人踪灭

答案应该可以写成:回文子序列数 回文子串个数

后面的随便做一下就行了,比如 hash+ 二分或 Manacher

然后考虑前面的部分:

fi=j<i[sj=si×2j]

那么以 i 为对称半径的答案就是 2fi1

a=1,b=0 那么 1×1=1,0×0=0×1=1×0=0 卷积上去即可

再令 a=0,b=1 做一遍一样的

NTT

inline void NTT(vector<int> &f,int lim,int opt){ f.resize(lim);
    rep(i,0,lim-1){
        r[i]=r[i>>1]>>1|((i&1)?(lim>>1):0)if(i<r[i]) swap(f[i],f[r[i]]);
    }
    for(int p=2;p<=lim;p<<=1){            
        int len=p>>1; W[0]=1; W[1]=ksm(3,(mod-1)/p); 
        if(opt==-1) W[1]=ksm(W[1],mod-2); 
        rep(j,2,len-1) W[j]=mul(W[j-1],W[1]);
        for(int k=0;k<lim;k+=p){
            for(int l=k,tt;l<k+len;++l){
                tt=mul(W[l-k],f[l+len]);
                f[l+len]=del(f[l],tt),ckadd(f[l],tt);
            }
        }
    } 
    if(opt==-1){
        for(int i=0,tmp=ksm(lim,mod-2);i<lim;++i) ckmul(f[i],tmp); 
    }
    return ;
}

写代码的习惯是很重要的,预处理单位根和最后只做一次快速幂还是挺重要的

一个人的高三楼

其实也是 Luogu5488 差分和前缀和

前缀和就是 ΔA=A1

那么对 1 多项式快速幂可以得到一个比较慢的做法

那么考虑实际意义:ΔkAi 就是所有的加和为 i 的位置的求和

那么系数是 (i+k1k1),NTT 即可

差分的初始卷的应该是 1x(简单构造可以得到),二项式定理就可以得到系数

(理解起来貌似是很简单的)

或者也可以用生成函数的封闭形式来得到这题……

HAOI2018 染色

ans=i=0min(m,nS)wi×method(i)

做一些卷积题目就会猜一下这个 method(i) 可能是要卷积出来的东西

那么推一下这个 method(i)

method(i)=(Mi)(NiS)(mi)niS×(iS)!(S!)i

然后就错了,因为最后的乱放可能再次出现有 S 个颜色

那么容斥这个过程,设最终出现了 S 次的颜色种类数是 j 那么得到如下:

method(i)=(Mi)(NiS)(iS)!(S!)ij=iM(1)ji(Miji)(niS(ji)S)((ji)S)!(S!)ji(mj)NjS

这式子貌似不短

那么化简完了 NTT 即可得到答案

把所有的组合数打开得到

method(i)=N!M!i!j=iM(1)ji(ji)!(Mj)njS(Mj)!(NjS)(S!)j

减法卷积可以翻转完了直接做,但是如果有梦想也可以推成加法卷积

j=0NN!M!(mj)NjS(S!)j(NjS)!(Mj)!i=0jwii!×(1)ji(ji)!

Θ(nlogn+n) 即可

分治FFT

还是 cdq 的那一套

每次对于当前区间处理当前左边和 g 的卷积,把答案累加到右侧

按照左中右的顺序进行处理

有些细节:开始的时候把序列长度补成 2x 的形式,给右边添加的时候的起点需要思考

这样就会做快速求第二类斯特林数的一列的形式

inline void NTT(int *f,int lim,int fl){
    for(int i=0;i<lim;++i) if(i<R[i]) swap(f[i],f[R[i]]);
    for(int p=2;p<=lim;p<<=1){
        int len=p>>1,tmp=ksm(3,(mod-1)/p); if(fl==-1) tmp=ksm(tmp,mod-2);
        for(int k=0;k<lim;k+=p){
            int buf=1; for(int l=k;l<k+len;++l){
                int tt=mul(buf,f[l+len]); 
                f[l+len]=del(f[l],tt); f[l]=add(f[l],tt);
                buf=mul(buf,tmp);
            }
        }
    } 
    if(fl==-1) for(int i=0,tp=ksm(lim,mod-2);i<lim;++i) f[i]=mul(f[i],tp); 
    return ;
}
inline void solve(int l,int r){
    if(l+1==r) return ; int mid=l+((r-l)>>1); solve(l,mid); 
    int len=r-l; 
    for(int i=0;i<len;++i) R[i]=R[i>>1]>>1|((i&1)?(len>>1):0);
    for(int i=0;i<len;++i) G[i]=g[i]; 
    for(int i=l;i<mid;++i) F[i-l]=f[i]; 
    for(int i=mid;i<r;++i) F[i-l]=0;
    NTT(F,len,1); NTT(G,len,1); 
    for(int i=0;i<len;++i) F[i]=mul(F[i],G[i]); 
    NTT(F,len,-1);
    for(int i=mid;i<r;++i) f[i]=add(f[i],F[i-l]);
    return solve(mid,r);
}

任意模数NTT

考虑 FFT 的精度如果在系数是 109 的情况会爆炸,那么考虑优化这个直接 FFT 再取模的做法

把当前多项式的每个 Ai 拆成 A0[i]×k+A1[i] 这里 A0[i]=A[i]/k,A1[i]=A[i]%kB 也拆一下系数

DFT 求点值的时候 考虑把两个并成一个,因为每个数一开始的虚部均为 0

构造 Pi=Ai+iBi,Qi=AiiBi,并观察 P,QDFT 定义式子 P[k],Q[nk] 发现是

P[k]=i=0n1(A[i]+ib[i])(cos2πikn+isin2πikn)

Q[nk]=i=0n1(A[i]ib[i])(cos2πiknisin2πikn)

由于 ωnjk=cos2πiknisin2πikn 即三角函数里面变号,所以就解释得通了

对着展开就能发现是共轭复数,所以得到其一,翻转取共轭就得到了另一个

那么解方程组可以的到 DFT 的结果(复数科技妙)

得到 DFT 的结果之后,考虑怎么 IDFT 四个多项式得到最终答案

接着构造 Pi=A0[i]B0[i]+iA1[i]B0[i],Qi=iA1[i]B1[i]+A0[i]B1[i]

因为这些个多项式系数都是实数,那么虚不会变成实,实不会变成虚,所以直接 IDFT 回去得到答案

精度不足就可以做三组 FFT 来完成

很厉害的东西

inline void MTT(){
    for(int i=0;i<=n;++i) A0[i].x=a[i]>>15,A0[i].y=a[i]&32767;
    for(int i=0;i<=m;++i) B0[i].x=b[i]>>15,B0[i].y=b[i]&32767;
    int lim=1; while(lim<=(n+m)) lim<<=1; 
    for(int i=0;i<lim;++i) r[i]=r[i>>1]>>1|((i&1)?(lim>>1):0);
    FFT(A0,lim,1); FFT(B0,lim,1);
    for(int i=0;i<lim;++i) A1[i]=A0[i?lim-i:i].conj(),B1[i]=B0[i?lim-i:i].conj();
    node p,q;
    for(int i=0;i<lim;++i){
        p=A0[i],q=A1[i];
        A0[i]=(p+q)*node(0.5,0),A1[i]=(q-p)*node(0,0.5);
    }
    for(int i=0;i<lim;++i){
        p=B0[i],q=B1[i];
        B0[i]=(p+q)*node(0.5,0),B1[i]=(q-p)*node(0,0.5);
    }
    /*
    你可能对于是 (q-p)*node(0,0.5) 产生疑惑,复数的三角表示的运算规则,辐角相加
    原来就有一个90度,再转90度就到x轴负半轴了,所以要乘 -1 转回来
    */
    for(int i=0;i<lim;++i){
        P[i]=A0[i]*B0[i]*node(1,0)+A1[i]*B0[i]*node(0,1);
        Q[i]=A1[i]*B1[i]*node(0,1)+A0[i]*B1[i]*node(1,0);
    }
    FFT(P,lim,-1); FFT(Q,lim,-1);
    for(int i=0;i<=n+m;++i){
        P[i].x=P[i].x/lim+0.5;
        P[i].y=P[i].y/lim+0.5;
        Q[i].x=Q[i].x/lim+0.5;
        Q[i].y=Q[i].y/lim+0.5;
        int r1=(int)(P[i].x)%mod*(1ll<<30)%mod;
        int r2=((int)(P[i].y)%mod+(int)(Q[i].x)%mod)%mod*(1ll<<15)%mod;
        int r3=(int)(Q[i].y)%mod;
        print(add(r1,add(r2,r3));
    }
    puts("");
    return ;    
}

多项式求逆

已知 H(x)F(x)1modxn2,求

G(x)F(x)=1modxn

必有

G(x)F(x)1modxn2G(x)H(x))F(x)0modxn2(G(x)H(x))20modxnG2(x)2H(x)G(x)+H2(x)0modxnG2(x)F(x)2F(x)G(x)H(x)+H2(x)0modxn

这时候因为 G(x)F(x)0modxn 所以就得到了答案式子:

G(x)2H(x)F(x)H(x)2modxn

递归即可,边界为 G(0)1F(0)mod998244353,复杂度 Θ(nlogn)

注意递归时是 n2 再合并能得到 modxn 意义下的结果,下取整就错了

inline void solve(int Len){
    if(Len==1) return G[0]=ksm(F[0],mod-2),void(); solve((Len+1)>>1);
    int lim=1; while(lim<=(Len<<1)) lim<<=1;  
    for(int i=0;i<Len;++i) H[i]=F[i]; 
    for(int i=Len;i<lim;++i) H[i]=0;
    P.MTT(H,G,Len,lim); P.MTT(H,G,Len,lim);
    for(int i=0;i<Len;++i) G[i]=del(mul(G[i],2),H[i]);
    for(int i=Len;i<lim;++i) G[i]=0; 
    return ;
}

城市规划

dp[i] 表示当前 i 个点构成的无项联通图的数目,dp[1]=1

转移考虑 i 个点每个点都可以向下一个点连边,所以是 dp[i+1]=dp[i]×(2i1)

但是 (5,7),(4,7) 的连边方式就可以保证 (4,5)联通,并不需要 14 的哪个点和 5

n 个点的不要求联通的无向联通图数目为 g(n)=2n(n1)21

f(n) 为联通的无向图,那么就有如下的式子:

g(n)=i=1n(n1i1)g(ni)f(i)

具体就是枚举第一个点所在的联通块里面有多少个点

F(x)=f(x)(x1)!,G(x)=g(x)(x1)!,H(x)=g(x)x!

写成生成函数的式子发现就变成了卷积,多项式求逆即可

  • 吃一堑涨一智:多项式求逆中的数组别用混

多项式 ln

G(x)=ln(F(x))modxn

两边同时对复合函数求导得到

G(x)=ln(F(x))F(x)modxn

G(x)=F(x)F(x)modxn

多项式求逆完了积分回去即可

注意在进行多项式 ln 的过程中需要保证 F[0]=1 否则求导积分的过程会出问题,具体出什么问题我也不知道

inline void Dao(int *G,int *F,int len){
    for(int i=0;i<len-1;++i) G[i]=mul(F[i+1],i+1);
    return ;  
}
inline void Ji(int *G,int *F,int len){
    for(int i=0;i<len;++i) G[i+1]=mul(F[i],ksm(i+1,mod-2));
    return ;
}
inline void Ln(){
    P.Dao(G,F,n); P.Inv(F,H,n);
    int lim=1; while(lim<(n<<1)) lim<<=1; 
    P.NTT(G,lim,1); P.NTT(H,lim,1); 
    for(int i=0;i<lim;++i) G[i]=mul(G[i],H[i]); P.NTT(G,lim,-1);
    P.Ji(ans,G,n);
    return ;
}

多项式 exp

先推式子:F(x)=eA(x)modxn

lnF(x)A(x)=0modxn

那么我们需要求出来的就是 G(F(x))=lnF(x)A(x)=0

这时候有个科技叫牛顿迭代,就是快速求函数零点的一个方法,本质上利用了导函数是对应值点处函数的斜率这一性质

问题是 F(G(x))=0G(x),牛顿迭代的方法是利用上次求出来的精度较小的 G0(x) 得到 G(x)=G0(x)F(G0(x))F(G0(x))

注意!这里的分母是 F(G0(x)) 而不是复合函数求导!!

这个式子比较好的地方是精度翻倍,也就是说对 modxn2 意义下做一次,现在精度就是 modxn 意义下的了,证明可以用逆多项式同余意义下也是 n2 次乘起来就翻倍了理解

正经得到证明需要泰勒展开一下

套回原来的式子,大概推导过程如下:

由于牛顿迭代的的精度翻倍,那么可以类似多项式求逆来进行递归,设已经得到了 F0(x)eA(x)modxn2,现尝试推 F(x)eA(x)modxn

带入上文中提及的 G(F(x))=lnF(x)A(x)=0

带入牛顿迭代表达式:

F(x)=F0(x)G(F0(x))G(F0(x))=F0(x)lnF0(x)A(x)1F0(x)

注意这里只对 G(x) 求导,所以并不需要复合函数求导法则

inline void exp(int *a,int *b,int n){
    if(n==1) return b[0]=1,void(); exp(a,b,(n+1)>>1); 
    int lim=1; while(lim<=(n<<1)) lim<<=1; Ln(b,Tln,n); 
    for(int i=0;i<n;++i) Tln[i]=del(a[i],Tln[i]); 
    Tln[0]=add(Tln[0],1); 
    for(int i=n;i<lim;++i) Tln[i]=0;
    NTT(Tln,lim,1); NTT(b,lim,1); 
    for(int i=0;i<lim;++i) b[i]=mul(b[i],Tln[i]); 
    NTT(b,lim,-1); 
    for(int i=n;i<lim;++i) b[i]=0; return ;
}

ln,expEGF 的表示下的一些模型中是为互逆运算:

  • 在错排问题中,没有置换环的 EGFi2(i1)!xii!=ln(1x)x

    那么错排问题的 EGF 就是 exp(ln(1x)x)

  • 无向连通图的 EGFF(x),无向图(不保证连通) 的 EGFG(x) 那么有 G(x)=exp(F(x)),F(x)=lnG(x)

    这个也可以同理到森林 G(x) 和树 F(x) 上,也是 F(x)=lnG(x) 的关系

    如果限制深度的话可以尝试递推:Fk(x)=xeFk1(x),Gk(x)=exp[xGk1(x)]

付公主的背包

关键题:乘法取对数变成加法

考虑构造出来生成函数封闭形式就是这个式子了

1ei=1ln(1xv)

F(x)=1xV,G(x)=lnF(x) 那么:

G(x)=F(x)F(x)G(x)=Vi0xV1ViG(x)=i0xV+Vi1+iG(x)=i1xVii

调和级数把多项式系数暴力得到之后写一份 exp 和 多项式求逆即可

多项式开根

H(x)2F(x)modxn2G(x)H(x)modxn2

剩下的推导和多项式求逆的过程一致,最后得到

G(x)=H2(x)+F(x)2H(x)

实现类似多项式求逆的递归即可

inline void sqr(int *A,int *B,int n){
    if(n==1) return B[0]=1,void(); sqr(A,B,(n+1)>>1); 
    int lim=1; while(lim<(n<<1)) lim<<=1; 
    for(int i=0;i<lim;++i) Tp[i]=0; 
    for(int i=0;i<n;++i) T2[i]=A[i]; 
    for(int i=n;i<lim;++i) T2[i]=0; 
    Inv(B,Tp,n); NTT(Tp,lim,1); NTT(B,lim,1); NTT(T2,lim,1); 
    for(int i=0;i<lim;++i) B[i]=mul(i2,add(B[i],mul(Tp[i],T2[i]))); 
    NTT(B,lim,-1); 
    for(int i=n;i<lim;++i) B[i]=0; 
    return ;
}

多项式快速幂

F(x)=Gk(x)modxn,两边同时 ln

klnG(x)=lnF(x)modxn

再来一把 exp 则答案就有了:

eklnG(x)=F(x)modxn

这个做法依赖于多项式 ln,所以需要保证 F[0]=0

对于 F[0]0 的情况,平移即可,注意特判快速幂指数 k 过大导致的全是 0 的情况

多项式除法

FR(x)=xnF(1x),其实也就是系数翻转(没错,R 就是 Reverse

F(x)=G(x)Q(x)+R(x)

F(1x)=G(1x)Q(1x)+R(1x)

配上 xn 可以得到:

xnF(1x)=xmG(1x)xnmQ(1x)+xn(m1)xm1R(1x)

换式子:

FR(x)=GR(x)QR(x)+xn(m1)RR(x)

FR(x)GR(x)QR(x)modxn(m1)

那么求逆得到 QR(x) 之后减法做出来 R(x) 就行了

写得时候记得清空一些数组

取模和除法是并行的

常系数齐次线性递推

建议阅读 https://www.luogu.com.cn/blog/BJpers2/solution-p4723

做法就是将用 xn 模特征多项式(定义为递推数列取负翻转后再添 1 作为 [xk]P

使用倍增求 xn 来加速这个过程

Code Display
inline poly Mod(vector<int> F,vector<int> G){
    int n=F.size(),m=G.size(); if(n<m) return F;
    poly Fr=F,Gr=G,R;
    reverse(Fr.begin(),Fr.end());
    reverse(Gr.begin(),Gr.end()); Gr.resize(n-m+1);
    Gr=Inv(Gr,n-m+1);
    poly Q=Mul(Gr,Fr); Q.resize(n-m+1);
    reverse(Q.begin(),Q.end());
    Q=Mul(Q,G);
    rep(i,0,m-2) R.pb(del(F[i],Q[i]));
    return R;
}
int f[N],a[N],n,k;
signed main(){
    n=read(); k=read();
    rep(i,1,k) f[i]=(read()%mod+mod)%mod; 
    rep(i,0,k-1) a[i]=(read()%mod+mod)%mod;
    vector<int> p={1},x={0,1},pmod;
    rep(i,0,k-1) pmod.pb(del(0,f[k-i])); pmod.pb(1);
    for(;n;n>>=1,x=Mod(Mul(x,x),pmod)) if(n&1){
        p=Mod(Mul(p,x),pmod);
    }
    p.resize(k);
    int ans=0;
    rep(i,0,k-1) ckadd(ans,mul(a[i],p[i]));
    print(ans);
    return 0;
}
posted @   没学完四大礼包不改名  阅读(129)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示