小学乘法知识(可能连载)

FFT

板子

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2097160;
const int G=3,md=998244353;
int n,m,i,j,k,u,v,p,t,le;
int f[maxn],g[maxn],r[maxn];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void dft(int *f){
for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
for(i=1,le=2;le<=t;i=le,le<<=1){
for(j=0,v=Pow(G,(md-1)/le);j<t;j+=le){
for(k=0,u=1;k<i;++k,u=1LL*u*v%md){
p=1LL*f[i+j+k]*u%md;
Add(f[i+j+k]=f[j+k],md-p);
Add(f[j+k],p);
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(i=0;i<=n;++i) scanf("%d",f+i);
for(i=0;i<=m;++i) scanf("%d",g+i);
for(n+=m,t=1;t<=n;t<<=1);
for(i=1;i<t;++i) r[i]=(r[i>>1]>>1)|((i&1)*(t>>1));
dft(f); dft(g); for(i=0;i<t;++i) f[i]=1LL*f[i]*g[i]%md;
dft(f); reverse(f+1,f+t); p=Pow(t,md-2);
for(i=0;i<=n;++i) printf("%d ",1LL*f[i]*p%md);
return 0;
}

属于需要背的知识。原根表。常见的原根形如 998244353 原根为 3924844033 原根为 5

分治 FFT

例题:P4721 【模板】分治 FFT

m=l+r2。考虑在知道 flfm 时,其对 fm+1fr 会造成怎样的贡献。此时 x[m+1,r];fxi=lmfigxi,此时将 flfmg1grl 卷积可得对应的贡献,分治下去计算其他部分的贡献即可。

使用多项式求逆:考虑 f,g 的生成函数 F(x)=i0fixi,G(x)=i0gixi,则由 i>0,fi=j=0i1fjgij

F(x)=f0x0+i>0xij=0i1fjgij=1+xi0xij=0ifjgi+1j=1+xF(x)G(x)g0x=1+F(x)(G(x)g0)

由于 g0 没有对 f 产生贡献,所以可以令 g0=0,则 F(x)=1+F(x)G(x)=11G(x)。可以多项式求逆解决。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int maxb=262150;
const int md=998244353,_g=3;
int n,i,j,k,u,v,t,p,le,f[maxn],g[maxn];
int r[maxb],F[maxb],G[maxb];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void dft(int *f){
for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
for(i=1,le=2;le<=t;i=le,le<<=1){
v=Pow(_g,(md-1)/le);
for(j=0;j<t;j+=le){
for(k=0,u=1;k<i;++k,u=1LL*u*v%md){
p=1LL*f[i+j+k]*u%md;
Add(f[i+j+k]=f[j+k],md-p);
Add(f[j+k],p);
}
}
}
}
void Solve(int l,int r){
if(l==r) return; int m=(l+r)>>1; Solve(l,m);
for(t=1,k=r-l+m-l-1;t<=k;t<<=1);
memset(F,0,t<<2); memset(G,0,t<<2);
memcpy(F,f+l,(m-l+1)<<2); memcpy(G,g+1,(r-l)<<2);
for(i=1;i<t;++i) ::r[i]=(::r[i>>1]>>1)|((i&1)*(t>>1));
dft(F); dft(G); for(i=0;i<t;++i) F[i]=(1LL*F[i]*G[i])%md;
dft(F); reverse(F+1,F+t); p=Pow(t,md-2);
for(i=m+1;i<=r;++i) Add(f[i],1LL*F[i-l-1]*p%md); Solve(m+1,r);
}
int main(){
scanf("%d",&n); --n; f[0]=1;
for(i=1;i<=n;++i) scanf("%d",g+i); Solve(0,n);
for(i=0;i<=n;++i) printf("%d ",f[i]);
return 0;
}

多项式牛顿迭代


引入:导数

导数表

(a)=0(其中 a 表示和 x 无关的常数,下同)
(xa)=axa1
(ax)=axlna
(logax)=1xlna
(sinx)=cosx
(cosx)=sinx

导数/积分运算法则

乘法法则(f(x)g(x))=f(x)g(x)+f(x)g(x)
除法法则(f(x)g(x))=f(x)g(x)f(x)g(x)g2(x)
链式法则ddxdxdy=ddy
复合函数求导(g(f(x)))=g(f(x))f(x)
分部积分fg=fgdx+fgdx
洛必达法则:如果 limxaf(x)=0,limxag(x)=0,且 f,ga 的某去心邻域内两者均可导且 g(x)0,则有 limxaf(x)g(x)=limxaf(x)g(x)。如果 limxaf(x)=,limxag(x)=,且 f,ga 的某去心邻域内两者均可导且 g(x)0,则有 limxa+f(x)g(x)=limxa+f(x)g(x)

引入:泰勒展开

在逼*某个函数 f(x) 时,可以考虑使用某个多项式,通过对 f(x) 在某个位置 x01n 阶导数求出在同样位置处 1n 阶导数相同的多项式,从而逼* f,由 (i=0naixi)=i=1niaixi1f(x)=i0f(i)(x0)i!(xx0)i。某些时候可以直接取 x0=0,则有 f(x)=i0f(i)(0)i!xi一些例子

引入:牛顿迭代

在求解方程 f(x)=0 的解时,我们考虑使用靠*零点的切线逼*解。具体地,取函数的某个位置 x0,得出在 x0 位置和 f(x) 相切的切线 y=f(x0)(xx0)+f(x0)。此时取该切线的零点 x1=x0f(x0)f(x0),然后再求出 x1 位置和 f(x) 相切的切线 y=f(x1)(xx1)+f(x1);就可以不断迭代下去以逼*该解(显然无法得到解的精确值)。

虽然某些时候不能用牛顿迭代法逼*解,但是这个算法有较为广泛的应用,例如 O(1) 求某个数的*似*方根。同时牛顿迭代法收敛速度(逼*正解速度)快,可以说每次迭代后有效数位均会上升一倍。


在已知多项式 g(x),求满足 g(f(x))0(modxn)f(x)modxn 时,同样可以考虑牛顿迭代法的思路。设已经求出了 modxn2 部分的解 f0(x),(注意需要单独求出常数项)则求 f(x) 时可以考虑将 gf0(x) 处泰勒展开得:

i0g(i)(f(x0))i!(f(x)f0(x))i0(modxn)

然后考虑 f0(x) 的最高次数为 n21,所以 f(x)f0(x) 的最低次数非零项的次数为 n2,则上式 modxn 等效于 g(f0(x))+g(f0(x))(f(x)f0(x))modxn,解得 f(x)=f0(x)g(f0(x))g(f0(x))

多项式牛顿迭代可以用于求多项式经过某些运算后的值。例如求多项式 h(x) 的逆时可以构造 g(f(x))=1f(x)h(x),开方时可以构造 g(f(x))=f2(x)h(x),求 exp 时可以构造 g(f(x))=lnf(x)h(x)

多项式求逆

构造 g(f(x))=1f(x)h(x),在求出上述的 f0(x) 后,由于 g(f(x))=1f2(x),则有 f(x)=f0(x)1f0(x)h(x)1f02(x)=2f0(x)f02(x)h(x)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxb=262150;
const int md=998244353,G=3;
int n,i,j,k,u,v,p,d,t,le;
int f[maxb],g[maxb],h[maxb],r[maxb],x[maxb];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void dft(int *f){
for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
for(i=1,le=2;le<=t;i=le,le<<=1){
for(j=0,v=Pow(G,(md-1)/le);j<t;j+=le){
for(k=0,u=1;k<i;++k,u=1LL*u*v%md){
p=1LL*f[i+j+k]*u%md;
Add(f[i+j+k]=f[j+k],md-p);
Add(f[j+k],p);
}
}
}
}
int main(){
scanf("%d",&n);
for(i=0;i<n;++i) scanf("%d",h+i); n<<=1;
f[0]=Pow(h[0],md-2); Add(x[0]=f[0],f[0]);
for(d=2,t=4;d<=n;d=t,t<<=1){
for(i=0;i<t;++i) r[i]=(r[i>>1]>>1)+(i&1)*d;
memcpy(g,h,d<<2); dft(g); dft(f);
for(i=0;i<t;++i) f[i]=1LL*f[i]*f[i]%md*g[i]%md;
dft(f); reverse(f+1,f+t); p=Pow(t,md-2);
for(i=0;i<d;++i){
Add(f[i]=md-1LL*f[i]*p%md,x[i]);
Add(x[i]=f[i],f[i]); f[i+d]=0;
}
}
for(n>>=1,i=0;i<n;++i) printf("%d ",f[i]);
return 0;
}

多项式 ln

考虑 ln(x)=1x。此时我们需要求 g(x)=lnf(x),两边同时求导有 g(x)=(lnf(x))=f(x)f(x)。注意此时 g(x)=0xf(t)f(t)dt+lnf(0),而 e 为超越数(不能作为某个方程 iaixi=0 的根,其中 ai 均为整数,所以不能在模意义下被表示),所以 lnf(0)f(0) 不为 1 时不能在模意义下被表示。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxb=262150;
const int md=998244353,G=3;
int n,i,j,k,u,v,d,t,p,le;
int f[maxb],g[maxb],h[maxb],x[maxb],r[maxb];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void dft(int *f){
for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
for(i=1,le=2;le<=t;i=le,le<<=1){
for(j=0,v=Pow(G,(md-1)/le);j<t;j+=le){
for(k=0,u=1;k<i;++k,u=1LL*u*v%md){
p=1LL*f[i+j+k]*u%md;
Add(f[i+j+k]=f[j+k],md-p);
Add(f[j+k],p);
}
}
}
}
int main(){
scanf("%d",&n);
for(i=0;i<n;++i) scanf("%d",h+i); n<<=1;
for(f[0]=1,d=x[0]=2,t=4;d<=n;d=t,t<<=1){
for(i=0;i<t;++i) r[i]=(r[i>>1]>>1)+(i&1)*d;
memcpy(g,h,d<<2); dft(f); dft(g);
for(i=0;i<t;++i) f[i]=1LL*f[i]*f[i]%md*g[i]%md;
dft(f); reverse(f+1,f+t); p=Pow(t,md-2);
for(i=0;i<d;++i){
Add(f[i]=md-1LL*f[i]*p%md,x[i]);
Add(x[i]=f[i],f[i]); f[i+d]=0;
}
}
for(n>>=1,i=1;i<n;++i) h[i-1]=1LL*h[i]*i%md; h[n]=0;
memset(f+n,0,(d-n)<<2); for(t=1;t<=(n-1)<<1;t<<=1);
for(i=0;i<t;++i) r[i]=(r[i>>1]>>1)+(i&1)*(t>>1);
dft(f); dft(h); for(i=0;i<t;++i) f[i]=1LL*f[i]*h[i]%md;
dft(f); reverse(f+1,f+t); p=Pow(t,md-2);
for(i=0;i<t;++i) f[i]=1LL*f[i]*p%md;
for(i=n-1;i;--i) f[i]=1LL*f[i-1]*Pow(i,md-2)%md;
for(f[0]=i=0;i<n;++i) printf("%d ",f[i]); return 0;
}

多项式 exp

构造 g(f(x))=lnf(x)h(x),在求出上述的 f0(x) 后,由于 g(f(x))=1f(x),则有 f(x)=f0(x)lnf0(x)h(x)1f0(x)=f0(x)(1lnf0(x)+h(x))

代码

懒得封装,写得比较丑。

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxb=262150;
const int maxt=524300;
const int md=998244353,G=3;
int n,i,j,k,u,v,p,t,le,d0,t0,d1;
int f0[maxb],f1[maxt],g1[maxb],g[maxt],h[maxb],x[maxt],r[maxt];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void dft(int *f){
for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
for(i=1,le=2;le<=t;i=le,le<<=1){
for(j=0,v=Pow(G,(md-1)/le);j<t;j+=le){
for(k=0,u=1;k<i;++k,u=1LL*u*v%md){
p=1LL*f[i+j+k]*u%md;
Add(f[i+j+k]=f[j+k],md-p);
Add(f[j+k],p);
}
}
}
}
int main(){
scanf("%d",&n);
for(i=1;i<n;++i) scanf("%d",h+i); n<<=1;
for(f0[0]=1,d0=2,t0=4;d0<=n;d0=t0,t0<<=1){ //外层倍增求 exp
for(i=1;i<d0;++i) g1[i-1]=1LL*f0[i]*i%md; //求导
for(f1[0]=1,x[0]=d1=2,t=4;d1<=t0;d1=t,t<<=1){ //内层倍增求逆
for(i=1;i<t;++i) r[i]=(r[i>>1]>>1)|((i&1)*d1);
memcpy(g,f0,d1<<2); dft(f1); dft(g);
for(i=0;i<t;++i) f1[i]=1LL*f1[i]*f1[i]%md*g[i]%md;
dft(f1); reverse(f1+1,f1+t); p=Pow(t,md-2);
for(i=0;i<d1;++i){
Add(f1[i]=md-1LL*f1[i]*p%md,x[i]);
Add(x[i]=f1[i],f1[i]); f1[i+d1]=0;
}
}
memset(f1+d0,0,(d1-d0)<<2); for(t=1;t<=t0-2;t<<=1);
for(i=1;i<t;++i) r[i]=(r[i>>1]>>1)|((i&1)*(t>>1));
dft(f1); dft(g1); for(i=0;i<t;++i) f1[i]=1LL*f1[i]*g1[i]%md;
dft(f1); reverse(f1+1,f1+t); p=Pow(t,md-2);
memset(f1+d0,0,(t-d0)<<2); memset(g1,0,t<<2);
for(i=0;i<d0;++i) f1[i]=1LL*f1[i]*p%md; //f'(x)/f(x)
for(i=d0-1;i;--i) f1[i]=1LL*f1[i-1]*Pow(i,md-2)%md; //积分
for(f1[0]=i=1;i<d0;++i) f1[i]=(1LL*f1[i]*(md-1)+h[i])%md;
for(i=1,t=t0;i<t;++i) r[i]=(r[i>>1]>>1)|((i&1)*d0);
dft(f0); dft(f1); for(i=0;i<t;++i) f0[i]=1LL*f0[i]*f1[i]%md;
dft(f0); reverse(f0+1,f0+t); p=Pow(t,md-2);
for(i=0;i<d0;++i) f0[i]=1LL*f0[i]*p%md; //求 exp
memset(f0+d0,0,d0<<2); memset(f1,0,t<<2); memset(g,0,d1<<2);
}
for(n>>=1,i=0;i<n;++i) printf("%d ",f0[i]); return 0;
}

附:O(n2)/O(nlog2n) 求逆/ ln / exp 的做法

可以在多项式次数小的时候进行优化(exp 的一种卡常就是分治 FFT 处理下面的 exp 形式)时间/码长。

求逆:考虑 (i=0nfixi)(i=0ngixi)=1 时由于 i>1,j=0ifjgij=0,所以 gi=j=0i1gjfijf0

ln/exp:令 F(x)=i=0fixi,G(x)=i=0gixi。考虑 F(x)=lnG(x),G(x)=expF(x) 时(此时有 f0=0,g0=1),有:

F(x)=G(x)G(x)F(x)G(x)=G(x)xF(x)G(x)=xG(x)j=0ijfjgij=igigi=1ij=1ijfjgijfi=fig0=gi1ij=0i1jfjgij

多项式带余除法(待补)

多项式多点求值(待补)

拉格朗日插值

在已知某个不超过 n 次的多项式上的 n 个点时,可以使用拉格朗日插值法确定出这个唯一的多项式。设第 i 个点为 (xi,yi)

对于多项式 f(x)=i=0Nfixi,由 f(x)f(a)=i=0Nfi(xiai)=i=1Nfi(xa)(j=0i1xjai1j)f(x)f(a)(modxa)。此时可以列出方程组如下:

{f(x)y1(modxx1)f(x)y2(modxx2)f(x)yn(modxxn)

考虑使用中国剩余定理(可以将所有 xxi 看成两两互质),令 A=i=1n(xxi)ti 为模 xxi 意义下 Axxi 的逆元,由于 Axxi=ji(xxj)=ji((xxi)+(xixj))ji(xixj)(modxxi),则 ti=ji1xixj;答案为 i=1nyijixxjxixj(其对应次数小于 n,所以在模 A 意义下解就是这个式子)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2010;
const int md=998244353;
int n,k,i,j,a,b,c,s,x[maxn],y[maxn];
inline int Pow(int d,int z){
int r=1;
do{
if(z&1) r=1LL*r*d%md;
d=1LL*d*d%md;
}while(z>>=1); return r;
}
int main(){
scanf("%d%d",&n,&k);
for(i=1;i<=n;++i) scanf("%d%d",x+i,y+i);
for(i=1;i<=n;++i,s=(1LL*a*b%md*Pow(c,md-2)+s)%md)
for(a=y[i],j=b=c=1;j<=n;++j) if(j!=i)
b=1LL*b*(k+md-x[j])%md,c=1LL*c*(x[i]+md-x[j])%md;
printf("%d",s); return 0;
}

FMT/FWT(个人的理解)

此处用了一种较为清奇(?)的思路。

(1) 或卷积(求 Ci=jork=iAjBk

考虑多项式乘法的过程:我们会先对序列进行 DFT,然后相乘,最后进行 IDFT。此时可以使用类似的方式,用某种序列表示需要操作的序列。

o(A)i=xori=iAx,则 o(C)i=(jork)ori=iAjBk。而 xori=i 等效于 xi 的子集,所以 (jork)ori 当且仅当 j,k 均是 i 的子集,也就是说 Ci=AiBi。求 o(A/B) 可以使用上述的方法,而从 o(C) 变回 C 时,考虑上述 dp 过程的逆过程:在从 dpi 推到 dpi1 时,如果某个 S 的第 i 位为 1dpi,S=dpi1,S+dpi1,Sxor2i=dpi1,S+dpi,Sxor2i,否则 dpi,S=dpi1,S,直接 dp 即可。

(2) 与卷积(求 Ci=jandk=iAjBk

仍然可以考虑上述的过程,设 a(A)i=xandi=iAx,则同样有 (jandk)andi=i 当且仅当 jandi=kandi=i,所以仍然有 a(C)i=a(A)ia(B)i。求 a(A) 时仍然可以使用上面 dp 的思路,但是转移时有 dpi,S=dpi1,S+[Sand2i=0]dpi1,Sor2i。从 a(C) 推回 C 同理。

(3) 异或卷积(求 Ci=jxork=iAjBk

考虑异或的性质,令 c(x)x 在二进制表示下的位数,可以发现 (c(j)+c(k)c(jxork))mod2=0,同理有 (c(jandi)+c(kandi)c((jxork)andi))mod2=0。令 xp(A)i=c(jandi)mod2=pAj,则 xp(C)i=j,k{0,1}[jxork=p]xj(A)ixk(B)ix0/1 也可以使用上述的 dp 方式计算。不过有更简洁的方式计算:设 x(A)i=x0(A)ix1(A)i,则 x(C)i=x(A)ix(B)i。同或卷积可以直接将求得的 C 翻转即可。

(4) 子集卷积(求 Ci=jork=i,jandk=0AjBk

考虑第二个性质等效于 c(j)+c(k)=c(i)。可以把所有下标按照 c 的不同值分开处理,设 op(A)ic(j)=p,jandi=iAj,则 op(C)i=j+k=poj(A)iok(B)i(暴力计算即可,节省常数)。这里的 o(A) 被称为 A占位幂级数

集合幂级数进阶计算

考虑将上述的子集卷积看成乘法,并以此为基础定义求逆 / ln / exp 等运算。此时可以将子集卷积中对占位多项式的卷积操作改成多项式求逆 / ln / exp,其他操作不变即可。此时集合幂级数的 ln/exp 和多项式的对应 ln/exp 的组合意义有相似之处;例如多项式 exp 对应了将全集划分为有标号元素组成的无标号集合的方案,则集合幂级数 exp 对应了将全集划分成若干子集(满足 子集不同 对应的方案就会不同)的对应方案。可以使用上述的 O(n2) 求逆 / ln / exp 的做法节省常数。

posted @   Fran-Cen  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示