多项式学习笔记
1. 快速傅里叶变换(FFT)
1.1. 定义
傅里叶变换(法语:Transformation de Fourier,英语:Fourier transform,缩写:FT)是一种线性变换,通常定义为一种积分变换。其基本思想是一个函数可以用(可数或不可数,可数的情况对应于傅里叶级数)无穷多个周期函数的线性组合来逼近,从而这些组合系数在保有原函数的几乎全部信息的同时,还直接地反映了该函数的“频域特征”。 ——维基百科
呃,这个定义在 OI 中用不到。OI 中的 FFT 其实是这样定义的:
设 是某一满足有限性条件的序列,它的离散傅里叶变换(DFT)为:
——OI-Wiki
实际上,FFT 可以理解为对于形如 的多项式,快速求 在 次单位根上的 个点值 。
1.2. 快速傅里叶变换(FFT)
不妨令 。如果 ,就可以补0凑成 。设 , ,根据单位根的周期性,存在
当 时,满足
这就可以用分治的方法求 。假设已经递归地求出所有 和 (相当于把奇偶次项分开,再对分出来的两个 次多项式 FFT),就可以用上面的两个式子求出所有的 。
代码如下:
typedef complex<double> Comp;
void FFT(Comp *f,int n,int rev){
if(n==1){
return;
}else{
for(int i=0;i<n;i++){
tmp[i]=f[i];
}
for(int i=0;i<n;i++){//分离奇偶次项
if(i%2){
f[i/2+n/2]=tmp[i];
}else{
f[i/2]=tmp[i];
}
}
Comp *g=f+n/2;
FFT(f,n/2,rev);FFT(g,n/2,rev);
Comp cur(1,0),step(cos(2*M_PI/n),sin(2*M_PI*rev/n));
for(int i=0;i<n/2;i++){
tmp[i]=f[i]+cur*g[i];
tmp[i+n/2]=f[i]-cur*g[i];
cur*=step;
}
for(int i=0;i<n;i++){
f[i]=tmp[i];
}
}
}
但是递归常数太大了。可以考虑从下到上“合并”多项式。一个多项式的系数组成的数列在 FFT 中位置关系的变换大概如下:
0 1 2 3 4 5 6 7 =>
0 2 4 6 1 3 5 7 =>
0 4 2 6 1 5 3 7
注意到如果把序号写成二进制,变换后的序号就是变换前的序号翻转过来。因此可以写出如下代码:
void initrev(int lim){//预处理二进制翻转
for(int i=0;i<lim;i++){
rev[i]=(rev[i>>1]>>1)|((i&1)*(lim>>1));
}
}
void FFT(Comp f[],int lim,int opt){
for(int i=0;i<lim;i++){
if(rev[i]<i){
swap(f[rev[i]],f[i]);
}
}
for(int m=2;m<=lim;m<<=1){
int k=m>>1;
Comp step(cos(M_PI/k),sin(M_PI*opt/k));
for(int i=0;i<lim;i+=m){
Comp cur(1,0);
for(int j=0;j<k;j++,cur=cur*step){
Comp tmp=f[i+j+k]*cur;
f[i+j+k]=f[i+j]-tmp;
f[i+j]=f[i+j]+tmp;
}
}
}
if(opt==-1){
for(int i=0;i<lim;i++){
f[i]=f[i].real()/lim;
}
}
}
这优化被称为“蝶形变换”。
1.3. 快速傅里叶逆变换
以后再写
2. 快速数论变换(NTT)
FFT 利用了单位根的性质,在模意义下有一种叫“原根”的数也具有类似的性质。
2.1. 阶和原根
对于常数 ,定义最小的满足 的 为 在 意义下的阶。
若 在 意义下的阶为 ,则称 为 的原根。
设 是 的原根,则 。即 的幂运算在 意义下的周期为 。故当 时, 可以与单位根 类比,不妨记 。
与单位根类似,这里的 也满足 。以下是笔者自己瞎胡的伪证:
解方程 可知 。当 时存在 ,由于 ,可推出 不是 的原根,与假设矛盾,故 。
2.2. NTT
考虑对在模 意义下的 次多项式进行与 FFT 类似的变换。
与 FFT 类似 ,NNT 可以求 在 意义下的 个点值 。仍然令 ,, , ,设 为质数且 满足 (即 ,这是为了保证 一直存在),则有
分离奇偶项后分治的想法仍然有效。这里直接给出蝶形变换优化的代码:
void initrev(int lim){
for(int i=0;i<lim;i++){
rev[i]=(rev[i>>1]>>1)|((i&1)*(lim>>1));
}
}
void NTT(int f[],int lim,int opt){//当 opt=-1时为逆变换
for(int i=0;i<lim;i++){
if(rev[i]<i){
swap(f[rev[i]],f[i]);
}
}
for(int m=2;m<=lim;m<<=1){
int k=m>>1;
int step=ksm(opt==1?G:INV_G,(P-1)/m);
for(int i=0;i<lim;i+=m){
int cur=1;
for(int j=0;j<k;j++,cur=cur*step%P){
int tmp=f[i+j+k]*cur%P;
f[i+j+k]=(f[i+j]-tmp+P)%P;
f[i+j]=(f[i+j]+tmp)%P;
}
}
}
if(opt==-1){
int inv=ksm(lim,P-2);
for(int i=0;i<lim;i++){
f[i]=f[i]*inv%P;
}
}
}
逆变换与 FFT 类似,这里不作赘述。
顺便提一下, 是质数且 ,它的原根是 ,因此在可以用 NTT 的题目里经常要求模 。
2.3. 任意模数 NTT
NTT 对 有比较严格的限制......
......
以后再写
3. 多项式初等函数
3.1. 多项式卷积
给定一个 次多项式 ,和一个 次多项式 。请求出 和 的卷积。
多项式卷积其实就是多项式乘法。多项式 为 和 的卷积时, 。例如,当 , 时 。
用 FFT 在单位根处插值后,把点值乘起来就是答案多项式的点值表示,再逆变换把点值表示转换成系数表示就行了。
记多项式 的 次项系数为 。可知 ,即 。
我的室友最近喜欢上了一个可爱的小女生。马上就要到她的生日了,他决定买一对情侣手环,一个留给自己,一个送给她。每个手环上各有 个装饰物,并且每个装饰物都有一定的亮度。
但是在她生日的前一天,我的室友突然发现他好像拿错了一个手环,而且已经没时间去更换它了!他只能使用一种特殊的方法,将其中一个手环中所有装饰物的亮度增加一个相同的非负整数 。并且由于这个手环是一个圆,可以以任意的角度旋转它,但是由于上面装饰物的方向是固定的,所以手环不能翻转。需要在经过亮度改造和旋转之后,使得两个手环的差异值最小。
在将两个手环旋转且装饰物对齐了之后,从对齐的某个位置开始逆时针方向对装饰物编号 ,其中 为每个手环的装饰物个数, 第 个手环的 号位置装饰物亮度为 ,第 个手环的 号位置装饰物亮度为 ,两个手环之间的差异值为(参见输入输出样例和样例解释):
麻烦你帮他计算一下,进行调整(亮度改造和旋转),使得两个手环之间的差异值最小,这个最小值是多少呢?
记旋转前两个手环在位置 处的装饰物亮度为 和 ,为了方便不妨假设 。设旋转第一个手环,旋转后 号装饰物对应 号装饰物,第一个手环整体加亮 ,不难得出差异值为 。发现拆开后的第一部分是常数,第二部分是关于 的二次函数,这两部分都容易处理。第三部分是关于 的式子,如果用 做代换可以得到 ,发现这长得特别像上文提到的卷积系数的形式。于是可以考虑进一步假设 或 时 ,令 ,进一步变形得到 ,这就和卷积系数一模一样了。因此,把 和 看作多项式 和 的系数,再算出它们卷积后的多项式所有的系数,就得到了所有可能的 。取 值即可。
事实上,利用 FFT 进行的快速多项式乘法经常用来快速求值可化为卷积系数形式的式子。
3.2. 多项式乘法逆
给定一个多项式 ,请求出一个多项式 , 满足 。系数对 取模。
本文作者:ztx-,使用署名-非商业性使用 4.0 国际进行许可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)