多项式全家桶
多项式合集
前置知识:多项式的定义,表示方法,FFT,NTT,微积分等。
注意事项
- 多项式的封装很重要,现在一般都是用将指针传入一个函数的方式来进行多项式操作,如:
void Inv(ll *f,ll *g,int n)
,表示对 次多项式 求逆,结果存在 中。 - 多项式数组多了一定要记得清空!!说的直接点,不要觉得你的空间很宝贵,现在一般 的数据范围能够你开 个数组!所以如果两个函数可能会共用一个数组的时候一定不要冒风险,该多开数组就开。
多项式乘法
所有多项式操作的基础,这里提供一篇小学生也能看懂的博客,真的特别详细!从复数到单位根到优化全部讲了一遍,这里仅提供一下代码:
1. FFT
struct node
{
double x,y;
friend node operator +(node a,node b){return {a.x+b.x,a.y+b.y};}
friend node operator -(node a,node b){return {a.x-b.x,a.y-b.y};}
friend node operator *(node a,node b){return {a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
}f[N],g[N];
void FFT(node *f,int type)
{
for(int i = 0;i < lim;i++)if(i < tr[i])swap(f[i],f[tr[i]]);
for(int l = 2;l <= lim;l <<= 1)
{
node wn = {cos(2*Pi/l),sin(2*Pi/l)*type};int now = l>>1;
for(int s = 0;s < lim;s += l)
{
node w = {1,0};int up = s|now;
for(int i = s;i < up;i++,w = w*wn)
{node mul = w*f[i|now];f[i|now] = f[i]-mul;f[i] = f[i]+mul;}
}
}
if(~type)return ;
for(int i = 0;i < lim;i++)
f[i].x = f[i].x/lim+0.5,f[i].y = f[i].y/lim+0.5;
}
int main()
{
n = rd();m = rd();
for(int i = 0;i <= n;i++)f[i].x = rd();
for(int i = 0;i <= m;i++)g[i].x = rd();
while(lim <= n+m)lim <<= 1;
for(int i = 0;i < lim;i++)tr[i] = tr[i>>1]>>1|((i&1)?lim>>1:0);
FFT(f,1);FFT(g,1);
for(int i = 0;i < lim;i++)f[i] = f[i]*g[i];
FFT(f,-1);
for(int i = 0;i <= n+m;i++)printf("%d ",(int)f[i].x);
return 0;
}
2. NTT
的计算要用到小数,而小数不可避免会有精度问题,而且大多数情况下运算都是在模意义下进行的,所以就有了 。
事实上就是将 的单位根换成了原根,其他其实是完全一样的。
一般来说 的模数都是 ,因为 ,而一般情况下 肯定不会达到 。如果模数不为 ,那么可以考虑用三个模数分别做再 合并,详见P4245【模板】任意模数多项式乘法。
这里也只是给出 模板的代码:
ll qp(ll x,int y)
{
ll ans = 1;
for(;y;y >>= 1,x = x*x%mod)
if(y&1)(ans *= x) %= mod;
return ans;
}
int G = 3,invG = qp(3,mod-2),inv2 = qp(2,mod-2);
int jia(int x,int y){return x+y >= mod?x+y-mod:x+y;}
int jian(int x,int y){return x < y?x-y+mod:x-y;}
void NTT(ll *f,int type)
{
for(int i = 0;i < lim;i++)if(i < tr[i])swap(f[i],f[tr[i]]);
for(int l = 2;l <= lim;l <<= 1)
{
ll wn = qp(~type?G:invG,(mod-1)/l);int now = l>>1;
for(int s = 0;s < lim;s += l)
{
ll w = 1;
for(int i = s;i < s+now;i++,w = w*wn%mod)
{int mul = w*f[i+now]%mod;f[i+now] = jian(f[i],mul);f[i] = jia(f[i],mul);}
}
}
}
int main()
{
n = rd();m = rd();
for(int i = 0;i <= n;i++)f[i] = rd();
for(int i = 0;i <= m;i++)g[i] = rd();
while(lim <= n+m)lim <<= 1;
for(int i = 0;i < lim;i++)tr[i] = tr[i>>1]>>1|((i&1)?lim>>1:0);
NTT(f,1);NTT(g,1);
for(int i = 0;i <= lim;i++)f[i] = f[i]*g[i]%mod;
NTT(f,-1);int invn = qp(lim,mod-2);
for(int i = 0;i <= n+m;i++)printf("%lld ",f[i]*invn%mod);
return 0;
}
在正式进入多项式全家桶前,还要介绍一些用于简化操作的函数:
void cpy(ll *al,ll *ar,ll *b)
:表示将al
到ar
的数复制到b
中。
void cpy(ll *al,ll *ar,ll *b){for(int i = 0;al != ar;al++,i++)*(b+i) = *al;}
void clr(ll *al,ll *ar)
:表示将al
到ar
的数全部赋值为 。
void clr(ll *al,ll *ar){for(;al != ar;al++)*al = 0;}
void pri(ll *f,int n)
,表示输出 次多项式 。
void pri(ll *f,int n)
{
for(int i = 0;i <= n;i++)
printf("%lld%c",f[i],i==n?'\n':' ');
}
并且,我们还要对多项试卷积进行封装:
void init(n)
:表示初始化 次多项式的蝴蝶变换数组。
void init(int n)
{
lim = 1;while(lim <= n)lim <<= 1;
for(int i = 0;i < lim;i++)tr[i] = tr[i>>1]>>1|(i&1?lim>>1:0);
}
void times(ll *f,ll *g,ll *a,int n,int m)
:表示求 和 的卷积,结果存在 中。
void times(ll *f,ll *g,ll *a,int n,int m)
{
cpy(f,f+n+1,a);cpy(g,g+m+1,b);init(n+m);
clr(a+n+1,a+lim);clr(b+m+1,b+lim);
NTT(a,1);NTT(b,1);
for(int i = 0;i <= lim;i++)a[i] = a[i]*b[i]%mod;
NTT(a,-1);
}
这里先将 和 复制到了另一个数组中再进行的卷积,并且会卷积前会将不用的东西清空——以后实现的每个函数都应该这样:传进去的参数不应该会发生改变,也不用提前清空不必要的东西。
讲了这么多,终于要开始多项式全家桶了(
多项式求逆
给定一个 次多项式 ,求多项式 使得 。
如果只有一项,显然这一项就是 的逆元,于是可以递归分治求解。
现在假设已经求出了
又因为 ,所以两式相减得:。
所以 。
两边同时平方,由于 在模 意义下等于 ,所以 在模 意义下就是 。
所以
两边同时乘 ,因为 ,所以
所以 ,然后就可以递归做了。
void Inv(ll *f,ll *g,int n)//G = G0(2-FG0)
{
if(!n)return (void)(g[0] = qp(f[0],mod-2));
Inv(f,g,n>>1);init(n << 1);
cpy(f,f+n+1,a);clr(a+n+1,a+lim);clr(g+n+1,g+lim);
NTT(a,1);NTT(g,1);
for(int i = 0;i < lim;i++)
g[i] = g[i]*(2-a[i]*g[i]%mod+mod)%mod;
NTT(g,-1);clr(g+n+1,g+lim);
}
分治
给定序列 ,求序列 。
其中 ,边界为 。
这题也可以用多项式求逆来做:,具体证明看题解吧
下面是分治的做法:
假设我们已经求出了 这一区间的数,现在我们要求 对 的贡献。
根据定义可以发现: 对 的贡献事实上就是 ,只需要做一遍卷积,然后再加到对应位置上即可。
为什么是 而不是 ?因为你要考虑所有 对 的贡献。就比如 乘 就对 有贡献,所以最大应该取到 而不是 。
void CDQ(ll *f,ll *g,int l,int r)//Inverse:F = 1/(1-G)(mod x^n)
{
if(l == r)return ;
int mid = l+r>>1;CDQ(f,g,l,mid);
times(f,g+l,a,r-l,mid-l);
for(int i = mid+1;i <= r;i++)g[i] = jia(g[i],a[i-l]);
CDQ(f,g,mid+1,r);
}
多项式除法
给定一个 次多项式 和一个 次多项式 ,求多项式 和 ,满足以下条件:
- 次数为 , 次数小于
设 是一个 次多项式,另 表示将 的 次项的系数翻转一遍后的多项式,可以发现其实 。
因为 是 次的,所以如果要求出 ,我们完全可以在模 意义下进行求解,而刚好 乘了个 ,所以可以直接丢掉。
那么 ,所以 。
于是可以算出 ,然后 ,就可以求出 了。
void rvs(ll *f,ll *g,int n)//求F_r,存到G里面,F和G有可能是一个东西
{for(int i = 0;i <= n/2;i++){ll t = f[i];g[i] = f[n-i];g[n-i] = t;}}
ll inv_rvsg[N];
void div(ll *f,ll *g,ll *q,ll *r,int n,int m)//F = Q*G+R,Qr = Fr/Gr
{
rvs(f,q,n);rvs(g,g,m);Inv(g,inv_rvsg,n-m);
times(q,inv_rvsg,q,n,n-m);clr(q+n-m+1,q+lim);
rvs(q,q,n-m);rvs(g,g,m);times(g,q,a,m,n-m);
for(int i = 0;i <= m-1;i++)r[i] = jian(f[i],a[i]);
}
多项式开根
给定一个 次多项式 ,求多项式 使得 。保证 。
多项式开根和求逆其实差不多。
如果只有一项,显然这一项就是 ,也是考虑递归分治求解。
现在假设已经求出了 ,因为 ,所以:
为了写代码方便,这里再继续化成:。然后就可以做了。
ll invg[N];
void Sqrt(ll *f,ll *g,int n)//G = (F+G0^2)/2G0
{
if(!n)return (void)(g[0] = 1);
Sqrt(f,g,n>>1);
Inv(g,invg,n);times(invg,f,invg,n,n);
for(int i = 0;i <= n;i++)g[i] = (invg[i]+g[i])*inv2%mod;
clr(g+n+1,g+lim);
}
加强版
与模板唯一不同的是, 不一定是 。
那么我们只需要修改一下边界条件,相当于求一次 的二次剩余。
由于 的原根是 ,那么 一定可以表示为 ,用 求出即可。又因为题目保证了 有二次剩余,所以 的二次剩余就是 。
int BSGS(int a,int b)
{
mp.clear();
ll m = sqrt(mod)+1,now = b;
for(int i = 0;i <= m;i++,now = now*a%mod)mp[now] = i;
ll sum = qp(a,m);now = sum;
for(int i = 1;i <= m;i++,now = now*sum%mod)
if(mp.count(now))return i*m-mp[now];
return -1;
}
int residue(int x,int k)//求x的k次剩余
{
int ans = qp(G,BSGS(G,x)/k);
return min(ans,mod-ans);
}
void Sqrt(ll *f,ll *g,int n)//G = (F+G0^2)/2G0
{
if(!n)return (void)(g[0] = residue(f[0],2));
...
}
微积分
下面的部分就要涉及到微积分了,如果不知道的可以看这:什么是微积分?
一些求导法则:
- ,
- ,
根据第一个求导法则,我们可以写出多项式的求导和积分函数:
void dao(ll *f,ll *g,int n)
{for(int i = 1;i <= n;i++)g[i-1] = f[i]*i%mod;g[n] = 0;}
void jifen(ll *f,ll *g,int n)
{for(int i = n;~i;i--)g[i+1] = f[i]*qp(i+1,mod-2)%mod;g[0] = 0;}
多项式对数函数
给定一个 次多项式 ,求多项式 使得 。保证 。
对 两边同时求导得:
所以
ll invf[N];
void ln(ll *f,ll *g,int n)//G=jifen(F'[i]/F[i])
{
Inv(f,invf,n);dao(f,g,n);
times(g,invf,g,n,n);clr(g+n+1,g+lim);
jifen(g,g,n-1);
}
牛顿迭代
给定一个 次多项式 ,已知有多项式 满足 ,求 。
先来看一个简单的问题:如何对一个很大的数 开方?很显然可以二分,但是二分每次都要算一个很大的数的平方,有没有什么更快的方法?
于是牛顿迭代产生了。
我们本质上是要求 与 的交点。假设我们已经求得了一个近似值 ,牛顿迭代的做法即为过 这个点做函数的切线,取切线与 轴的交点作为新的 。
比如我们要求 (图是盗的):
具体的,我们要求一条过 ,斜率为 的直线与 轴的交点。
很容易得出,切线方程为 ,当 时,
再回到原来的问题:求函数 ,使得 。
假设我们已经求出了 ,那么只需要每次令:
事实上,牛顿迭代每次都会让精度翻倍,即如果 ,那么有 。可以根据多项式求逆和开根感性理解。
多项式指数函数
给定一个 次多项式 ,求多项式 使得 。保证 。
因为 ,所以 。
根据牛顿迭代,我们可以令函数 ,有 (注意一下这里不是复合函数求导)。
由牛顿迭代得:
然后就可以递归去做了。
ll lng[N];
void exp(ll *f,ll *g,int n)//G = G0(1-lnG0+F)
{
if(!n)return (void)(g[0] = 1);
exp(f,g,n>>1);ln(g,lng,n);
for(int i = 0;i <= n;i++)lng[i] = jian(!i+f[i],lng[i]);
times(g,lng,g,n,n);clr(g+n+1,g+lim);
}
再看多项式求逆和开方
在看到多项式求逆和开方的做法时,你可能会有点懵,为啥使用递归分治来做?为啥求了当前一半的答案就可以算出当前整个的答案?下面,我们再从牛顿迭代的角度推一遍。
多项式求逆
给定一个 次多项式 ,求多项式 使得 。
令函数 ,有 。
由牛顿迭代得:
是不是和之前推出来的式子完全一样?现在知道为啥要递归去做了吧!
多项式开方
给定一个 次多项式 ,求多项式 使得 。
令函数 ,有 。
由牛顿迭代得:
多项式快速幂
给定一个 次多项式 和 ,求多项式 使得 。保证 。
显然可以像普通快速幂一样直接做,但是这样子是 的,有没有更快的方法?
考虑对 两边同时取对数得 ,
然后再取指数得:,这样子就可以 做了。
void qpow(ll *f,ll *g,int n,int k)//G = e^{k*lnF}
{
ln(f,lnf,n);
for(int i = 0;i <= n;i++)lnf[i] = lnf[i]*k%mod;
exp(lnf,g,n);
}
加强版
给定一个 次多项式 和 ,求多项式 使得 。不保证 。
如果 ,那么可以考虑先给多项式除以一个 ,然后做快速幂,最后再乘上 。
但是还有一种可能——,这种情况下需要找到最低的系数不为 的那一项。
设这一项为 ,那么让整个多项式除以这一项就能保证第一项为 了。做了快速幂之后再让整个多项式乘 即可。
要注意的是 应该对 取模,而不是对 。还要注意判断 是否比 大,所以在读入模数时应该记录三个值。
ll ff[N];
void exqpow(ll *f,ll *g,int n,char *s)//f = ff*(c*x^d)
{
int len = strlen(s+1);
ll k1 = 0,k2 = 0,k3 = 0;
for(int i = 1;i <= len;i++)
{
k1 = (k1*10+(s[i]^48))%mod;k2 = (k2*10+(s[i]^48))%phimod;
if(i <= 6)k3 = k3*10+(s[i]^48);
}
ll c = 0,d,invc;
for(int i = 0;i <= n;i++)
{
if(!c&&f[i])invc = qp(c = f[i],mod-2),d = i;
if(c)ff[i-d] = f[i]*invc%mod;
}
if(!c)return (void)clr(g,g+n+1);
qpow(ff,g,n,k1);d = min(n+1ll,d*k3);c = qp(c,k2);
for(int i = n;~i;i--)g[i] = i < d?0:g[i-d]*c%mod;
}
多项式三角函数
给定一个 次多项式 ,求多项式 使得 或 。保证 。
首先你需要知道大名鼎鼎的欧拉公式:
取 :;取 :
两式相加再除以 得 ;两式相加再除以 得 ;
还有个问题, 在模意义下是多少?
因为 ,所以 ,求一遍二次剩余即可。
ll I = residue(mod-1,2),invI = mod-I;
ll expg[N],inv_expg[N];
void sin(ll *f,ll *g,int n)//sinx = (e^ix-e^-ix)/2i
{
for(int i = 0;i <= n;i++)g[i] = f[i]*I%mod;
exp(g,expg,n);Inv(expg,inv_expg,n);
int mul = inv2*invI%mod;
for(int i = 0;i <= n;i++)g[i] = (expg[i]-inv_expg[i]+mod)*mul%mod;
}
void cos(ll *f,ll *g,int n)//cosx = (e^ix+e^-ix)/2
{
for(int i = 0;i <= n;i++)g[i] = f[i]*I%mod;
exp(g,expg,n);Inv(expg,inv_expg,n);
for(int i = 0;i <= n;i++)g[i] = (expg[i]+inv_expg[i])*inv2%mod;
}
多项式反三角函数
给定一个 次多项式 ,求多项式 使得 或 。保证 。
有两个式子:
证:
- :
令 :
令 :
证毕,套板子。
ll sqg[N];
void asin(ll *f,ll *g,int n)//asinx = ln(ix+sqrt(1-x^2))/i
{
times(f,f,g,n,n);
for(int i = 0;i <= n;i++)g[i] = jian(!i,g[i]);
Sqrt(g,sqg,n);
for(int i = 0;i <= n;i++)(sqg[i] += f[i]*I) %= mod;
ln(sqg,g,n);
for(int i = 0;i <= n;i++)(g[i] *= invI) %= mod;
}
ll lng1[N],lng2[N];
void atan(ll *f,ll *g,int n)//atanx = (ln(1+ix)-ln(1-ix))/2i
{
for(int i = 0;i <= n;i++)g[i] = (!i+f[i]*I)%mod;
ln(g,lng1,n);
for(int i = 0;i <= n;i++)g[i] = jian(!i,f[i]*I%mod);
ln(g,lng2,n);int mul = inv2*invI%mod;
for(int i = 0;i <= n;i++)g[i] = (lng1[i]-lng2[i]+mod)*mul%mod;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具