多项式计算1
单位根
引入
我们研究复数中特殊的一类数,即在复数范围内 的根,它们称为单位根,方程为 则被称为 次单位根,记作 ,由代数基本定理可知, 次单位根共有 个,我们逆时针依次编号为 ,结合复数乘法的几何意义(模长相乘,辐角相加)可以知道单位根的模长皆为 ,单位根 辐角为 。所以依据欧拉公式
下面是 次单位根的示例
性质
根据其指数表达结合几何意义我们可以发现以下性质:
- 折半性质:
- 对称性质:
- 求和性质:
考虑等比数列求和,所以有
分类讨论
- 当 时,不能展开为等比数列形式,但因为 所以答案显然为 ;
- 当 时,,,,所以原式为 ;
单位根反演
结合上面的求和性质,我们可以写出下列反演
卷积
给定多项式
我们可以知道 的系数
这种形式被称为加法卷积,一般地
形式被称为 卷积( 是某种运算),本篇只探讨加法卷积。
可以发现加法卷积单次复杂度为 ,由于要计算每一项系数的加法卷积,故总复杂度为 不够优秀,考虑从其他方面思考。
点值表示法
上述表达多项式的方法叫做系数表达法,同时多项式可以用 个不同的点唯一确定一个 次多项式,这种表达方法叫做点值表达法。
点击查看证明
将 个点中的 分别带入建立 的方程,一共 个方程。写成系数矩阵和增广矩阵因为各个点都不相同,化简成行阶梯形矩阵可以看到系数矩阵的秩等于增广矩阵的秩,所以该多项式系数存在唯一解。因为 ,所以知道 和 的 个点,则可以求出 的 个点,从而唯一确定 。所以我们要实现如何从系数表达法到点值表达法以及如何从点值表达法到系数表达法。
即将系数表达法转化点值表达法。对于多项式 其 ,我们可以选择带任意 个值从而转化为点值表达法。我们考虑带入自然数,当我们带入 时,多项式比较好计算,带入 等其他自然数,计算就比较复杂。我们发现 ,很像上文提到的单位根,所以我们考虑带入单位根能否化简计算过程,由于要求 个点值,我们选择 次单位根,其实 可以 分治求出,为了方便分治,我们将多项式的项数补成一个 的整数次幂,下面请看推导过程:
我们先分离奇偶项
下面我们设
所以有
这样可以将 用 与 表达
带入单位根
所以已知 和 可以求 和 ,所以 求 次单位根点值可以分治成 和 求 次单位根点值。
有了 的经验后,不妨用线性代数的视角看待 ,我们设单位根矩阵
我们设系数向量
我们设点值向量
显然 。
实质上就是已知 与 求 , 实质上就是已知 与 求 ,考虑构造 的逆 。
可以证明
点击查看证明
由矩阵乘法的定义可知 由单位根的求和性质显而易见 即为 的逆矩阵。位逆序置换
依据上述可以实现递归版,但是递归版常数较大,经常会被卡常,考虑如何实现非递归实现,下面给出较为通用的实现方法:我们处理出每个 递归计算至最后的位置 。下面以八次多项式为例说明
我们可以发现 其实是 的逆序。可以用类似 的思想 递推,递推式为 ,其中 分别表示左移和右移。
求得 后,我们将第 项的系数交换至 。然后自底向上 ,然后将 存放至原来的系数上,每次合并两部分答案。可以发现 相较于 只是将 变为 ,最后再除以 ,所以我们可以一个函数实现 。
与 进行多项式乘法需要
第一步求出 点值表达法并相乘:
第二步用 的点值表达法 求出 :
故时间复杂度为 。
模板题
直接用 即可,注意 不是 的项数而是其度数。
点击查看代码
#include<bits/stdc++.h>
#define MOD (1000000007)
#define ll long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define lowbit(x) (x&-x)
#define Swap(x,y) (x^=y,y^=x,x^=y)
using namespace std;
void read(ll &x)
{
register char ch=0;register bool f=0;x=0;
while(ch<'0'||ch>'9'){f|=!(ch^'-');ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
x=f?-x:x;
}
void write(ll x,bool bk)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(!x)
{
if(!bk) putchar('0');
return;
}
write(x/10,1);
putchar((x%10)^48);
}
void print(ll x,char ch)
{
write(x,0);
if(ch) putchar(ch);
}
ll rev[2097157];
const double PI=acos(-1);
struct Complex{
double x,y;
Complex(){}Complex(double nx,double ny):x(nx),y(ny){}
Complex operator+(Complex t){return (Complex){x+t.x,y+t.y};}
Complex operator-(Complex t){return (Complex){x-t.x,y-t.y};}
Complex operator*(Complex t){return (Complex){(x*t.x-y*t.y),x*t.y+y*t.x};}
Complex operator*(double lamdba){return (Complex){x*lamdba,y*lamdba};}
Complex operator/(double lamdba){return (Complex){x/lamdba,y/lamdba};}
}A[2097157],B[2097157];
void FFT(ll n,Complex* Arr,ll flg)
{
for(ll i=0;i<n;i++) if(i<rev[i]) swap(Arr[i],Arr[rev[i]]);
for(ll i=2,step=1;i<=n;i<<=1,step<<=1)
{
Complex w(cos(PI/step),flg*sin(PI/step));
for(ll j=0;j<n;j+=i)
{
Complex r(1,0);
for(ll k=0;k<step;k++,r=r*w)
{
Complex ev(Arr[j+k]),ov(r*Arr[j+k+step]);
Arr[j+k]=ev+ov,Arr[j+k+step]=ev-ov;
}
}
}
if(!~flg) for(ll i=0;i<n;i++) Arr[i]=Arr[i]/n;
}
void Polymul(ll n,ll m,Complex* A,Complex* B,Complex* C)
{
ll len=1,t=0;
static Complex tpA[2097157],tpB[2097157];
for(;len<n+m-1;len<<=1,++t);
for(ll i=0;i<len;i++)
{
//printf("tpA[%lld]=%lf\ntpB[%lld]=%lf\n",i,A[i].x,i,B[i].x);
tpA[i]=A[i],tpB[i]=B[i];
rev[i]=((rev[i>>1]>>1)|((i&1)<<t>>1));
}
FFT(len,tpA,1),FFT(len,tpB,1);
for(ll i=0;i<len;i++)
{
C[i]=tpA[i]*tpB[i];
// printf("tpA[%lld]=(%lf)+(%lf)i\n",i,tpA[i].x,tpA[i].y);
// printf("tpB[%lld]=(%lf)+(%lf)i\n",i,tpB[i].x,tpB[i].y);
}
FFT(len,C,-1);
}
ll n,m;
int main()
{
read(n),read(m);
++n,++m;
for(ll i=0;i<n;i++)
{
ll tp;
read(tp);
A[i].x=tp;
}
for(ll i=0;i<m;i++)
{
ll tp;
read(tp);
B[i].x=tp;
}
Polymul(n,m,A,B,A);
for(ll i=0;i<n+m-1;i++)
print((ll)round(A[i].x),' ');
}
可以看出,上面 涉及了许多浮点数运算,所以精度要求高,系数的值域如果更大则无法接受其精度。而在实际计数时,我们经常需要对一个数取模,所以下面介绍一种在模 ,意义下的替代方案。
我们发现 需要浮点数计算的根本原因是 分治时依赖单位根,所以我们想找到单位根在模 意义下的替代品,事实说明原根是单位根的替代品,而依赖原根的变换称为 。
阶
对于数 , 对于 的阶定义为满足 的最小正整数解,记为 ,在 时,阶为 或被认为不存在。 的上界从欧拉定理可知为 。
原根
当 时, 被称为是 的原根。
性质
实现
用原根替换单位根即可,注意取模。
点击查看代码
#include<bits/stdc++.h>
#define MOD (998244353)
#define ll long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define lowbit(x) (x&-x)
#define Swap(x,y) (x^=y,y^=x,x^=y)
using namespace std;
void read(ll &x)
{
register char ch=0;register bool f=0;x=0;
while(ch<'0'||ch>'9'){f|=!(ch^'-');ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
x=f?-x:x;
}
void write(ll x,bool bk)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(!x)
{
if(!bk) putchar('0');
return;
}
write(x/10,1);
putchar((x%10)^48);
}
void print(ll x,char ch)
{
write(x,0);
if(ch) putchar(ch);
}
ll rev[2097157];
const ll g=3;
ll qpow(ll x,ll y)
{
ll res=1;
while(y)
{
if(y&1) res=res*x%MOD;
x=x*x%MOD,y>>=1;
}
return res;
}
// const double PI=acos(-1);
// struct Complex{
// double x,y;
// Complex(){}Complex(double nx,double ny):x(nx),y(ny){}
// Complex operator+(Complex t){return (Complex){x+t.x,y+t.y};}
// Complex operator-(Complex t){return (Complex){x-t.x,y-t.y};}
// Complex operator*(Complex t){return (Complex){(x*t.x-y*t.y),x*t.y+y*t.x};}
// Complex operator*(double lamdba){return (Complex){x*lamdba,y*lamdba};}
// Complex operator/(double lamdba){return (Complex){x/lamdba,y/lamdba};}
// }A[2097157],B[2097157];
ll A[2097157],B[2097157];
void NTT(ll n,ll* Arr,ll flg)
{
for(ll i=0;i<n;i++) if(i<rev[i]) swap(Arr[i],Arr[rev[i]]);
for(ll i=2,step=1;i<=n;i<<=1,step<<=1)
{
ll w=qpow(g,(MOD-1)/i);
if(!~flg) w=qpow(w,MOD-2);
for(ll j=0;j<n;j+=i)
{
ll r=1;
for(ll k=0;k<step;k++,r=r*w%MOD)
{
ll ev=Arr[j+k],ov=r*Arr[j+k+step]%MOD;
Arr[j+k]=(ev+ov)%MOD,Arr[j+k+step]=((ev-ov)%MOD+MOD)%MOD;
}
}
}
if(!~flg)
{
ll inv=qpow(n,MOD-2);
for(ll i=0;i<n;i++) Arr[i]=Arr[i]*inv%MOD;
}
}
void Polymul(ll n,ll m,ll* A,ll* B,ll* C)
{
ll len=1,t=0;
static ll tpA[2097157],tpB[2097157];
for(;len<n+m-1;len<<=1,++t);
for(ll i=0;i<len;i++)
{
//printf("tpA[%lld]=%lf\ntpB[%lld]=%lf\n",i,A[i].x,i,B[i].x);
tpA[i]=A[i],tpB[i]=B[i];
rev[i]=((rev[i>>1]>>1)|((i&1)<<t>>1));
}
NTT(len,tpA,1),NTT(len,tpB,1);
for(ll i=0;i<len;i++)
{
C[i]=tpA[i]*tpB[i];
// printf("tpA[%lld]=(%lf)+(%lf)i\n",i,tpA[i].x,tpA[i].y);
// printf("tpB[%lld]=(%lf)+(%lf)i\n",i,tpB[i].x,tpB[i].y);
}
NTT(len,C,-1);
}
ll n,m;
int main()
{
// freopen("data.in","r",stdin);
// freopen("data.out","w",stdout);
read(n),read(m);
++n,++m;
for(ll i=0;i<n;i++) read(A[i]);
for(ll i=0;i<m;i++) read(B[i]);
Polymul(n,m,A,B,A);
for(ll i=0;i<n+m-1;i++)
print(A[i],' ');
}
作者:littlepinkpig
出处:https://www.cnblogs.com/littlepinkpig/articles/16609815.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
你可以在这里自定义其他内容
作者:littlepinkpig
出处:https://www.cnblogs.com/littlepinkpig/articles/16609815.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现