多项式计算1

单位根

引入

我们研究复数中特殊的一类数,即在复数范围内 xn=1(nN+) 的根,它们称为单位根,方程为 n 则被称为 n 次单位根,记作 ωn,由代数基本定理可知,n 次单位根共有 n 个,我们逆时针依次编号为 ωn0,,ωnn1,结合复数乘法的几何意义(模长相乘,辐角相加)可以知道单位根的模长皆为 1,单位根 ωnk辐角为 2kπn。所以依据欧拉公式

ωnk=exp(i2kπn)=cos(2kπn)+isin(2kπn)

下面是 8 次单位根的示例

性质

根据其指数表达结合几何意义我们可以发现以下性质:

  • 折半性质:

ω2n2k=ωnk

Proof:ωnk=(ωn1)k=(ω2n2)k=ω2n2k

  • 对称性质:

ω2nk=ω2nk+n

Proof:ω2nn=1,ω2nk+n=ω2nk×ω2nn=ω2nk

  • 求和性质:

i=0n1(ωnk)i=n[k=0]

Proof: 考虑等比数列求和,所以有

i=0n1(ωnk)i=1(ωnk)n1ωnk

分类讨论

  • k=0 时,不能展开为等比数列形式,但因为 ωn0=1 所以答案显然为 n
  • k0 时,1ωnk0(ωnk)n=ωnnk=ω1k=11(ωnk)n=0,所以原式为 0

单位根反演

结合上面的求和性质,我们可以写出下列反演

[nk]=1ni=0n1ωnik

FFT

卷积

给定多项式
f(x)=i=0n1fixi
g(x)=i=0m1gixi
h(x)=f(x)g(x)=i=0n+m2xik=0ifkgik
我们可以知道 h(x) 的系数

hi=k=0ifkgik=j+k=ifjgk

这种形式被称为加法卷积,一般地

hijk=ifjgk

形式被称为 卷积( 是某种运算),本篇只探讨加法卷积。
可以发现加法卷积单次复杂度为 O(n),由于要计算每一项系数的加法卷积,故总复杂度为 O(n2) 不够优秀,考虑从其他方面思考。

点值表示法

上述表达多项式的方法叫做系数表达法,同时多项式可以用 n 个不同的点唯一确定一个 n1 次多项式,这种表达方法叫做点值表达法。

点击查看证明n 个点中的 x 分别带入建立 F(x)=y 的方程,一共 n 个方程。写成系数矩阵和增广矩阵因为各个点都不相同,化简成行阶梯形矩阵可以看到系数矩阵的秩等于增广矩阵的秩,所以该多项式系数存在唯一解。

因为 f(x)g(x)=h(x),所以知道 fgn+m1 个点,则可以求出 hn+m1 个点,从而唯一确定 h。所以我们要实现如何从系数表达法到点值表达法以及如何从点值表达法到系数表达法。

DFT

DFT 即将系数表达法转化点值表达法。对于多项式 F(x)degF=n1,我们可以选择带任意 n 个值从而转化为点值表达法。我们考虑带入自然数,当我们带入 x=±1 时,多项式比较好计算,带入 2 等其他自然数,计算就比较复杂。我们发现 (±1)2=1,很像上文提到的单位根,所以我们考虑带入单位根能否化简计算过程,由于要求 n 个点值,我们选择 n 次单位根,其实 DFT 可以 O(nlogn) 分治求出,为了方便分治,我们将多项式的项数补成一个 2 的整数次幂,下面请看推导过程:
我们先分离奇偶项

n=2kF(x)=i=0n1aixi=i=0n21a2ix2i+i=0n21a2i+1x2i+1=i=0n21a2ix2i+xi=0n21a2i+1x2i

下面我们设

even(i)=2iodd(i)=2i+1

所以有

Feven(x)=i=0n21aeven(i)xiFodd(x)=i=0n21aodd(i)xi

这样可以将 F(x)Feven(x)Fodd(x) 表达

F(x)=Feven(x2)+xFodd(x2)

带入单位根 ωnk

F(ωnk)=Feven((ωnk)2)+ωnkFodd((ωnk)2)=Feven(ωn2k)+ωnkFodd(ωn2k)=Feven(ωn/2k)+ωnkFodd(ωn/2k)F(ωnk+n/2)=Feven((ωnk+n/2)2)+ωnk+n/2Fodd((ωnk+n/2)2)=Feven(ωn2k+n)ωnkFodd(ωn2k+n)=Feven(ωn2k)ωnkFodd(ωn2k)=Feven(ωn/2k)ωnkFodd(ωn/2k)

所以已知 Feven(ωn/2k)Fodd(ωn/2k) 可以求 F(ωnk)F(ωnk+n/2),所以 F(x)n 次单位根点值可以分治成 FevenFoddn2 次单位根点值。

IDFT

有了 DFT 的经验后,不妨用线性代数的视角看待 DFT&IDFT,我们设单位根矩阵

A=((ωn0)0(ωn0)1(ωn0)n1(ωn1)0(ωn1)1(ωn1)n1(ωnn1)0(ωnn1)1(ωnn1)n1)

我们设系数向量

t=(a0a1an1)

我们设点值向量

y=(F(ωn0)F(ωn1)F(ωnn1))

显然 y=At
DFT 实质上就是已知 AtyIDFT 实质上就是已知 yAt,考虑构造 A 的逆 A1
可以证明

nA1=((ωn0)0(ωn0)1(ωn0)n1(ωn1)0(ωn1)1(ωn1)n1(ωn(n1))0(ωn(n1))1(ωn(n1))n1)=B

点击查看证明 由矩阵乘法的定义可知 (AB)ij=k=1nAikBkj=k=0n1(ωni)k(ωnk)j=k=0n1ωnk(ij) 由单位根的求和性质显而易见 1nB 即为 A 的逆矩阵。

位逆序置换

依据上述可以实现递归版,但是递归版常数较大,经常会被卡常,考虑如何实现非递归实现,下面给出较为通用的实现方法:我们处理出每个 i 递归计算至最后的位置 rev(i)。下面以八次多项式为例说明

pos 0 1 2 3 4 5 6 7
Begin 0 1 2 3 4 5 6 7
bin 000 001 010 011 100 101 110 111
End 0 4 1 5 2 6 3 7
bin 000 100 010 110 001 101 011 111

我们可以发现 rev(i) 其实是 i 的逆序。可以用类似 DP 的思想 O(n) 递推,递推式为 rev(i)=(rev(i>>1)>>1)((i1)<<(log2n1)),其中 <<,>> 分别表示左移和右移。
求得 rev(i) 后,我们将第 i 项的系数交换至 rev(i)。然后自底向上 DFT,然后将 DFT) 存放至原来的系数上,每次合并两部分答案。可以发现 IDFT 相较于 DFT 只是将 ωnk 变为 ωnk,最后再除以 n,所以我们可以一个函数实现 DFT&IDFT
AB进行多项式乘法需要
第一步求出 A,B 点值表达法并相乘:D=DFT(A)DFT(B)
第二步用 C 的点值表达法 D 求出 CC=IDFT(D)
故时间复杂度为 T(n)=2T(n2)+O(n)=O(nlogn)

模板题

直接用 FFT 即可,注意 n,m 不是 A,B 的项数而是其度数。

Code

点击查看代码
#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),' ');
}

NTT

NTT

可以看出,上面 FFT 涉及了许多浮点数运算,所以精度要求高,系数的值域如果更大则无法接受其精度。而在实际计数时,我们经常需要对一个数取模,所以下面介绍一种在模 p,意义下的替代方案。
我们发现 FFT 需要浮点数计算的根本原因是 FFT 分治时依赖单位根,所以我们想找到单位根在模 p 意义下的替代品,事实说明原根是单位根的替代品,而依赖原根的变换称为 NTT

对于数 aa 对于 p 的阶定义为满足 ax1(modp) 的最小正整数解,记为 δp(a),在 gcd(a,p)1 时,阶为 或被认为不存在。δp(a) 的上界从欧拉定理可知为 φ(p)

原根

δp(g)=φ(p) 时,g 被称为是 p 的原根。

性质

实现

用原根替换单位根即可,注意取模。

Code

点击查看代码
#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 国际」许可协议进行许可。

posted @   little_pinkpig  阅读(25)  评论(1编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up light_mode palette
选择主题
more_horiz
keyboard_arrow_up light_mode palette
选择主题