多项式基础操作全家桶

多项式入门教程

基础概念

形如 F(x)=a1+a2x+a3x2+...+anxnF(x)=a1+a2x+a3x2+...+anxn 的东西叫多项式。

然后,这个 nn 可以是无穷大的。

其中上面的 aiai 称为多项式的第 ii 项系数。

多项式乘法

即各项相乘,假设有两个多项式 F(x)=ni=0aixiF(x)=ni=0aixiG(x)=ni=0bixiG(x)=ni=0bixi

那么:

F(x)×G(x)=n+mi=0(x+y=iaxby)xiF(x)×G(x)=n+mi=0(x+y=iaxby)xi

然后我们发现这个式子里有一个部分很有意思,我们专门拿出来:

设两多项式的乘积的第 ii 项系数为 cici 那么有:

ci=x+y=iaxbyci=x+y=iaxby

我们称这个为加法卷积。如果把那个加号换成别的就变成其他的什么卷积了。

那么我们如何计算多项式乘法?你发现可以容易的在 O(n2)O(n2) 内计算。但是这是不优的,我们接下来介绍一种 O(nlogn)O(nlogn) 内计算多项式乘法的算法,先了解一些前置知识。

复数

什么叫复数?形如 a+bia+bi 的叫复数。其中 i2=1i2=1aa 称为一个复数的实部,bb 称为一个复数的虚部。|a+bi||a+bi| 表示其模长。其共轭复数为 abiabi,对于复数 zz ,其共轭复数记作 ¯z¯¯¯z

然后其四则运算为:

(a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)×(c+di)=ac+adi+bcibd=(acbd)+(ad+bc)i(a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)×(c+di)=ac+adi+bcibd=(acbd)+(ad+bc)i

然后模长 |z|=|a+bi|=z¯z=a2+b2|z|=|a+bi|=z¯¯¯z=a2+b2

a+bi0a+bi0 时存在逆元 1a+bi=abi|a+bi|1a+bi=abi|a+bi|

复数几何意义

我们把 xx 坐标表示实部, yy 坐标表示虚部的平面称为复平面。可以发现是数轴的扩展。

可以把复数看做一个在复平面上的向量 (a,b)(a,b),记其幅角为 θθ ,模长为 rr 那么有 a=rcosθa=rcosθb=rsinθb=rsinθ,那么其实复数还可表示为 r(cosθ+isinθ)r(cosθ+isinθ)

然后你会惊奇的发现复数相乘相当于模长相乘,幅角相加。

r1(cosθ1+isinθ1)×r2(cosθ2+isinθ2)=r1r2(cosθ1cosθ2+icosθ1sinθ2+isinθ1cosθ2sinθ1sinθ2)=r1r2(cos(θ1+θ2)+isin(θ1+θ2))r1(cosθ1+isinθ1)×r2(cosθ2+isinθ2)=r1r2(cosθ1cosθ2+icosθ1sinθ2+isinθ1cosθ2sinθ1sinθ2)=r1r2(cos(θ1+θ2)+isin(θ1+θ2))

单位根

我们称满足 zn=1zn=1 的复数 zznn 次单位根。

首先由于模长相乘,幅角相加的乘法,我们知道 nn 次单位根模长为 11nθ=2kπnθ=2kπ。因此不同的 nn 次单位根总共有 nn 个,它们将单位圆 nn 等分。

一般我们用 winwin 表示第 i+1i+1nn 次单位根,即 θin=2iπnθin=2iπn

一些性质

性质1

wkn=wk+nnwkn=wk+nn

性质2

wakan=wknwakan=wkn

角度所占比相同,模长都为 11 ,因此固然相同。

性质3

win×wjn=wi+jnwin×wjn=wi+jn

单位根可以看做 1i1i 圆周。

性质4

wk+n2n=wknwk+n2n=wkn

旋转了一个半圆,因此相反。

性质5

n1i=0wain=n[a=0]n1i=0wain=n[a=0]

证明:

a=0a=0 时,等价于 nn 个第 11nn 次单位根相加,就是 nn11 相加,于是得到 nn

a0a0

先了解一下等比数列求和公式

对于 a,aq,aq2,...,aqna,aq,aq2,...,aqnSn=a+aq+aq2+...+aqnSn=a+aq+aq2+...+aqn

其等价于 af(q),f(x)=1+x+x2+...+xnaf(q),f(x)=1+x+x2+...+xn

我们可以知道 f(x)=xf(x)+1xn+1f(x)=xf(x)+1xn+1f(x)=1xn+11xf(x)=1xn+11x

那么 Sn=af(q)=a1qn+11qSn=af(q)=a1qn+11q

好,那么把 w0nw0n 代入 aawanwan 代入 qq 得到

=w0n1(wan)n1wan=1w0n1wan=0=w0n1(wan)n1wan=1w0n1wan=0

Q.E.D.

离散傅里叶变换

简称 DFT (Discrete Fourier Transform)。

其逆变换称为 IDFT

所以实际上
我们知道, nn 个方程可以确定 nn 个未知数。也就是说对于一个 nn 次多项式,我们只需要 n+1n+1 个点值就可以求出多项式的系数。点值是什么?将 xx 的取值代入多项式得到的值。

然后经过简单验证可以发现,F,GF,Gxx 处的点值的乘积,等于 F×GF×Gxx 处的点值。

因为我们发现多项式在点值上相乘更快,所以我们考虑一种系数表示转点值表示再转回系数表示的做法。

而这个东西的作用和点值表示有一些相同的性质,以及一个更为优秀的性质。因为傅里叶变换的本质是把单位根代入多项式。

回到正题,傅里叶变换。

假设对于一个多项式 ff 我们知道其在 00n1n1 处的系数。

那么定义其傅里叶变换 F[f]F[f] 满足:(两式的单位根的次方的正负号可以交换)

F[f](k)=n1i=0f[i]wiknF1[f](k)=1nn1i=0f[i]wiknF[f](k)=n1i=0f[i]wiknF1[f](k)=1nn1i=0f[i]wikn

先证明一下这两个互为逆运算,即证明:

F1[F[f]](k)=f[k]F[F1[f]](k)=f[k]F1[F[f]](k)=f[k]F[F1[f]](k)=f[k]

证明:

怎么证明?全部展开!

F1[F[f]](k)=1nn1i=0F[f](i)wikn=1nn1i=0wiknn1j=0f[j]wjin=1nn1j=0f[j]n1i=0wi(jk)n=f[k]F1[F[f]](k)=1nn1i=0F[f](i)wikn=1nn1i=0wiknn1j=0f[j]wjin=1nn1j=0f[j]n1i=0wi(jk)n=f[k]

同理另一个得证。

Q.E.D.

然后实际上,你发现:

F[f](k)=f(wkn)F1[f](k)=1nf(wkn)F[f](k)=f(wkn)F1[f](k)=1nf(wkn)

卷积定理

f×g[x]=i+j=xf[i]g[j]i,j[0,n1]f×g[x]=i+j=xf[i]g[j]i,j[0,n1]

然后有:

F[f×g](x)=(F[f]×F[g])(x)F[f×g](x)=(F[f]×F[g])(x)

证明:

F[f×g](x)=n1i=0(f×g)[i]wix=n1i=0a+b=if[a]g[b]wixn=n1i=0a+b=if[a]waxng[b]wbxn=(n1i=0f[i]wixn)(n1i=0g[i]wixn)=(F[f]×F[g])(x)F[f×g](x)=n1i=0(f×g)[i]wix=n1i=0a+b=if[a]g[b]wixn=n1i=0a+b=if[a]waxng[b]wbxn=(n1i=0f[i]wixn)(n1i=0g[i]wixn)=(F[f]×F[g])(x)

也就是傅里叶变换的先后是无关的,所以可以用傅里叶变换的乘积进行逆傅里叶变换得到乘积。

快速傅里叶变换

简称 FFT (Fast Fourier Transform),用来快速计算傅里叶变换。

为方便处理,我们把 nn 取到大于等于自身的最小的 22 的幂次。(不用担心会计算错误,因为不存在的高位系数都是 00)。

m=n2m=n2

设:

F0[f](k)=m1i=0f[2i]w2ikn=m1i=0f[2i]wikmF1[f](k)=m1i=0f[2i+1]w(2i+1)kn=wknm1i=0f[2i+1]wikmF0[f](k)=m1i=0f[2i]w2ikn=m1i=0f[2i]wikmF1[f](k)=m1i=0f[2i+1]w(2i+1)kn=wknm1i=0f[2i+1]wikm

然后有 F[f](k)=F0[f](k)+F1[f](k)F[f](k)=F0[f](k)+F1[f](k)

观察一下可以发现,F0[f](k+m)=F0[f](k)F0[f](k+m)=F0[f](k)F1[f](k+m)=wmnF1[f](k)F1[f](k+m)=wmnF1[f](k)wk+mn=wknwk+mn=wkn 。(三个结论暴力拆解易证)

所以 F0F0F1F1 都只计算前 mm 项,这个分治处理,边界是 m1=1m1=1,此时两式都是原多项式。

然后复杂度 T(n)=2T(n2)+O(n)T(n)=2T(n2)+O(n) 于是总复杂度 O(nlogn)O(nlogn)

然后再观察一下 FFF1F1:

F[f](k)=n1i=0f[i]wiknF1[f](k)=1nn1i=0f[i]wiknF[f](k)=n1i=0f[i]wiknF1[f](k)=1nn1i=0f[i]wikn

如果令 wn=w1nwn=w1n

F1[f](k)=1nn1i=0f[i]wiknF1[f](k)=1nn1i=0f[i]wikn

发现和 FF 长得其实巨像无比,所以我们只需要将 wnwn 作为单位根进行傅里叶变换就可以实现逆傅里叶变换。

代码实现

由于考虑到代码常数的问题,我们就不考虑实现递归版了。因为这个理解和实现起来都简单。这个递归版的实现可以看 这里

为了保证一个能够过题的常数,我们考虑以下的一些优化。

别的没什么好说的,我们重点说一下 FFT 的二进制翻转优化。

先给出结论,递归到最底层时,位置为 ii 处理的是在原序列位置为 rev(i)rev(i) 的地方,其中 rev(i)rev(i) 表示 ii 的二进制翻转。

我们直接把序列放到这个位置便是对的,因为我们只在乎递归边界的值的大小,中间的位置我们不需要在意。

那么为什么递归的最底层满足位置的二进制翻转?

证明:

考虑归纳法证明。

n=1n=1,有 rev(0)=0rev(0)=0 成立。

n=2n=2,有 rev(0)=0,rev(1)=1rev(0)=0,rev(1)=1 成立。

n=4n=4, 有 rev(0)=0,rev(1)=2,rev(2)=1,rev(3)=3rev(0)=0,rev(1)=2,rev(2)=1,rev(3)=3 成立。

n=8n=8,有:

0 1 2 3 4 5 6 710 2 4 6|1 3 5 720 4|2 6|1 5|3 730|4|2|6|1|5|3|74

成立。

假设结论对于 n=2kn=2k 成立,证明对于 n=2k+1n=2k+1 也成立。

0 1 2 ... 2k+110 2 ... 2k+121 3 ... 2k+110 1 2 ... 2k+110 2 ... 2k+121 3 ... 2k+11

左边的位置全部除去二进制下最低位的 00 得到

0 1 ... 2k10 1 ... 2k1

我们知道,对于 n=2kn=2k 结论成立,于是左边重新加回最低位 00 ,发现此操作前后二进制翻转不变,因此左半边结论成立。

右半边除去最低位 11 得到相同的式子。

由于对于 n=2kn=2k 成立,右边加回最低位 11 ,发现操作前后二进制翻转相差 2k2k,减去左半边下标 2k2k 正好对应右半边的二进制翻转。于是右半边结论成立。

综上结论成立。Q.E.D.

那么我们直接把这位的多项式的值换到二进制翻转的位置计算即可。至于怎么线性求二进制翻转?看 这里

可能你有点疑惑,为什么递归最底层的值不需要操作可以直接用?你代入 F[f](0)F[f](0) 你发现 w0=1w0=1 ,直接就是原本的多项式。

#include<bits/stdc++.h>
#define ll long long
#define db double 
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define Better_IO true
namespace IO{
    #if Better_IO
        char buf[(1<<20)+3],*p1(buf),*p2(buf);
        const int lim=1<<20;
        inline char gc(){
            if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
            return p1==p2?EOF:*p1++;
        }
        #define pc putchar
    #else
        #define gc getchar
        #define pc putchar
    #endif
    inline bool blank(char c){
        return c=='\t' or c=='\n' or c=='\r' or c==' ' or c==EOF;
    }
    inline void gs(char *s){
        char ch=gc();
        while(blank(ch) ) ch=gc();
        while(!blank(ch) ) {*s++=ch;ch=gc();}
        *s=0;
    }
    inline void ps(char *s){
        while(*s!=0) {pc(*s++);}
    }
    template<class T>
    inline void read(T &s){
        s=0;char ch=gc();bool f=0;
        while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
        while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
        if(ch=='.'){
            db p=0.1;ch=gc();
            while('0'<=ch&&ch<='9') {s=s+p*(ch^48);ch=gc();p*=0.1;}
        }
        s=f?-s:s;
    }
    template<class T,class ...A>
    inline void read(T &s,A &...a){
        read(s);read(a...);
    }
};
using IO::gs;
using IO::ps;
using IO::read;
using IO::gc;
const int N=(1<<20)+3;
const db pi=acos(-1);
struct Complex{
    db a,b;
    Complex(){};
    Complex(db _a,db _b){
        a=_a;b=_b;
    }
    inline friend Complex operator + (Complex x,Complex y){
        return {x.a+y.a,x.b+y.b};
    }
    inline friend Complex operator - (Complex x,Complex y){
        return {x.a-y.a,x.b-y.b};
    }
    inline friend Complex operator * (Complex x,Complex y){
        return {x.a*y.a-x.b*y.b,x.a*y.b+x.b*y.a};
    }
};
int n,m;
Complex F[N<<1],G[N<<1],w[N];
int rev[N<<1];
inline void getrev(){
    for(int i=0;i<n;++i){
        rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0);
    }
}
inline void FFT(Complex *f,bool I){
    for(int i=0;i<n;++i){
        if(rev[i]<i)
            std::swap(f[i],f[rev[i] ]);
    }
    for(int p=2;p<=n;p<<=1){
        int len=p>>1;
        w[0]=Complex(1,0);w[1]=Complex(cos(2*pi/p),sin(2*pi/p) );
        if(I) w[1].b*=-1;
        for(int i=2;i<len;++i){
            w[i]=w[i-1]*w[1];
        }
        for(int k=0;k<n;k+=p){
            for(int l=k;l<k+len;++l){
                Complex tmp=f[l+len]*w[l-k];
                f[l+len]=f[l]-tmp;
                f[l]=f[l]+tmp;
            }
        }
    }
}
int main(){
    file(a);
    read(n,m);
    for(int i=0;i<=n;++i){
        read(F[i].a);
    }
    for(int i=0;i<=m;++i){
        read(G[i].a);
    }
    for(m+=n,n=1;n<=m;n<<=1);
    getrev();
    FFT(F,0);FFT(G,0);
    for(int i=0;i<n;++i) F[i]=F[i]*G[i];
    FFT(F,1);
    for(int i=0;i<=m;++i){
        printf("%d ",(int)(F[i].a/n+0.5) );
    }
    return 0;
}

快速数论变换

简称 NTT (Number-theoretic transform)。

很多时候计数问题都是在取模意义下的,而 FFT 使用浮点数,不单单是巨慢无比,更可怕的是基本上用不了了,怎么办?于是考虑选择一个模意义下的玩意来代替单位根。最终大佬们找到了 原根。然后就形成了 NTT

原根与阶

定义:aa 在模 pp 意义下的阶为一个最小的整数 kk 使得 ak1(modp)ak1(modp)。其中 gcd(a,p)=1gcd(a,p)=1。记作 δp(a)δp(a)

本质上是幂次的最小循环节。

性质1

根据欧拉定理,我们发现一个数 aa 的阶会小于等于 φ(a)φ(a)

性质2

an1(modm)an1(modm) , 则 δm(a)|nδm(a)|n

证明:

n=δm(a)q+r(0r<δm(a))n=δm(a)q+r(0r<δm(a))

r>0r>0arar(aδm(a))q1(modm)arar(aδm(a))q1(modm)

然后就是有一个比 δm(a)δm(a) 更小的满足阶的性质的东西 rr ,于是矛盾。

于是 r=0r=0 ,即 δm(a)|nδm(a)|n ,得证,Q.E.D.

性质3

对于 a,b,ma,b,m ,且 gcd(a,m)=gcd(b,m)=1gcd(a,m)=gcd(b,m)=1 则:

δm(ab)=δm(a)δm(b)δm(ab)=δm(a)δm(b) 的充分必要条件是 gcd(δm(a),δm(b))=1gcd(δm(a),δm(b))=1

证明:

必要性证明:

由阶的定义可得

(ab)lcm(δm(a),δm(b))(aδm(a))k1(bδm(b))k21(modm)(ab)lcm(δm(a),δm(b))(aδm(a))k1(bδm(b))k21(modm)

性质2 可知

δm(ab)|lcm(δm(a),δm(b))δm(ab)|lcm(δm(a),δm(b))

δm(ab)=δm(a)δm(b)δm(ab)=δm(a)δm(b) 得:

δm(a)δm(b)|lcm(δm(a),δm(b))δm(a)δm(b)|lcm(δm(a),δm(b))

由公式 ab=lcm(a,b)gcd(a,b)ab=lcm(a,b)gcd(a,b) 可知:

gcd(δm(a),δm(b))=1gcd(δm(a),δm(b))=1

Q.E.D.

充分性证明:

1(ab)δm(ab)δm(b)aδm(ab)(modm)1(ab)δm(ab)δm(b)aδm(ab)(modm)

可得 δ(a)|δ(ab)δm(b)δ(a)|δ(ab)δm(b) 再结合 gcd(δm(a),δm(b))=1gcd(δm(a),δm(b))=1 得到 δm(a)|δm(ab)δm(a)|δm(ab)

同理可得: δm(b)|δm(ab)δm(b)|δm(ab)

因此 δm(a)δm(b)|δm(ab)δm(a)δm(b)|δm(ab)

然后类似地可以得到 δm(ab)|δm(a)δm(b)δm(ab)|δm(a)δm(b)

性质4

对于 gcd(a,m)=1gcd(a,m)=1,有:

δm(ak)=δm(a)gcd(δ(a),k)δm(ak)=δm(a)gcd(δ(a),k)

证明:

由:

(ak)δm(ak)1(modm)(ak)δm(ak)1(modm)

可得 δm(a)|kδm(ak)δm(a)|kδm(ak) ,则 δm(a)gcd(δm(a),k)|δm(ak)δm(a)gcd(δm(a),k)|δm(ak)

再由:

(ak)δm(a)gcd(δm(a),k)=(aδm(a))kgcd(δm(a),k)1(modm)(ak)δm(a)gcd(δm(a),k)=(aδm(a))kgcd(δm(a),k)1(modm)

于是有 δm(ak)|δm(a)gcd(δm(a),k)δm(ak)|δm(a)gcd(δm(a),k)

然后把刚才这个式子与上面得到的式子结合,可得:

δm(ak)=δm(a)gcd(δm(a),k)δm(ak)=δm(a)gcd(δm(a),k)

Q.E.D.

原根

定义:一个数 aa 在模 pp 意义下满足 δp(a)=φ(p)δp(a)=φ(p) ,则称之为 pp 的原根。

证明 性质1 前,需要证明两个定理:

拉格朗日定理

对于素数 pp,对于模 pp 意义下的整系数多项式 f(x)f(x):

f(x)=a0+a1x1+...+anxn(p|an)f(x)=a0+a1x1+...+anxn(p/|an)

其同余方程 f(x)0(modp)f(x)0(modp) 在模 pp 意义下至多有 nn 个不同解。

证明:

n=0n=0 时,有 p|a0p/|a0 ,因此 f(x)0(modp)f(x)0(modp) 是无解的,定理对于 n=0n=0 成立。

假设定理对于 degf<ndegf<n 成立,考虑证明定理对于 degf=ndegf=n 成立。

反证法,假设存在 n+1n+1 个符合条件的 x0,x1,...,xnx0,x1,...,xn

现在设 g(x)g(x) 满足 f(x)f(x0)=(xx0)g(x)f(x)f(x0)=(xx0)g(x) ,因此可知 g(x)g(x) 在模 pp 意义下最多是 n1n1 次多项式,又因为对于 i[0,n]i[0,n] 都满足 f(xi)0(modp)f(xi)0(modp) ,于是有:

对于 i[1,n]i[1,n] 有,

(xix0)g(xi)=f(xi)f(x0)0(modp)(xix0)g(xi)=f(xi)f(x0)0(modp)

又因为 xix0(modp)xi≢x0(modp) ,得到 g(x)0(modp)g(x)0(modp) ,此时 g(x)0(modp)g(x)0(modp) 这个同余方程有 nn 个根,与归纳法的假设冲突,矛盾。

因此定理对 degf=ndegf=n 也成立,得证。

Q.E.D.

定理1

对于 a,b,pa,b,p 满足 gcd(a,p)=gcd(b,p)=1gcd(a,p)=gcd(b,p)=1 ,存在 cZcZ 使得 δp(c)=lcm(δp(a),δp(b))δp(c)=lcm(δp(a),δp(b))

证明:

对于 δp(a),δp(b)δp(a),δp(b) 质因数分解,得到:

δp(a)=si=0pniiδp(b)=si=0pmiiδp(a)=si=0pniiδp(b)=si=0pmii

k1k1 为所有 niminimipnipni 的乘积,k2k2 为所有 ni>mini>mipmiipmii 的乘积。

然后取 x,yx,y 使得 δp(a)=k1x,δP(b)=k2yδp(a)=k1x,δP(b)=k2y。可以发现 gcd(x,y)=1gcd(x,y)=1lcm(δp(a),δp(b))=xylcm(δp(a),δp(b))=xy

然后由 阶的性质4δp(ak1)=δp(a)gcd(δp(a),k1)=x,δp(bk2)=yδp(ak1)=δp(a)gcd(δp(a),k1)=x,δp(bk2)=y

再由 阶的性质3 可得:(条件:gcd(δp(a),δp(b))=1gcd(δp(a),δp(b))=1gcd(a,p)=gcd(b,p)=1gcd(a,p)=gcd(b,p)=1

δp(ak1bb2)=xy=lcm(δP(a),δp(b))δp(ak1bb2)=xy=lcm(δP(a),δp(b))

可得 c=ak1bk2c=ak1bk2

Q.E.D.

性质1

对于任意奇素数(非 22 素数)pp ,其存在原根。

证明:

由上面的 定理1 ,可以发现存在一个 gg 使得 δp(g)=lcm(δp(1),...,δp(p1))δp(g)=lcm(δp(1),...,δp(p1))

于是,对于任意 i[1,p1]i[1,p1]δp(i)|δp(g)δp(i)|δp(g)iδp(g)1(modp)iδp(g)1(modp)。也就是说,这些 ii 全部都是同余方程 xδp(g)1(modp)xδp(g)1(modp) 的解。

拉格朗日定理 可知,δp(g)p1δp(g)p1 ,同时由费马小定理,有 δp(g)p1δp(g)p1 ,可得 δp(g)=p1=φ(p)δp(g)=p1=φ(p)

所以 gg 是模 pp 意义下的原根。

Q.E.D.

性质2

对于奇素数 ppaNaNpapa 有原根。

证明:

考虑先证明 p2p2 的原根存在。

pp 的一个原根为 gg ,其在模 p2p2 的阶为 dd ,则有 d|φ(p2)d|φ(p2)欧拉定理+阶的性质2),再由 gd1(modp)gd1(modp) 于是有 gd1(modp2)gd1(modp2),因此 φ(p)|dφ(p)|d

由于 φ(p)=(p1)|dφ(p)=(p1)|dd|p(p1)=φ(p2)d|p(p1)=φ(p2)pp 为奇素数,可知 d=p1d=p1d=p(p1)d=p(p1)

d=p1d=p1 ,则 ggp2p2 的原根。当 d=p(p1)d=p(p1) 时,考虑证明 (g+p)(g+p)p2p2 的原根。

首先 (g+p)(g+p)pp 的原根。同上可知 g+pg+pp2p2 的阶为 (p1)(p1)p(p1)p(p1) 。由 gp11(modp2)gp11(modp2) 结合二项式定理可得:

(g+p)p11pgp2(modp2)(g+p)p11pgp2(modp2)

可知 (g+p)p11(modp2)(g+p)p1≢1(modp2) ,则 (g+p)p(p1)1(modp2)(g+p)p(p1)1(modp2)

然后可以归纳证明:

假设 ggpapa 的原根 ,ddgg 对于模 pa+1pa+1 的阶,考虑证明 ggpa+1pa+1 。于是类似的可以得到 d=pa1(p1)d=pa1(p1)d=pa(p1)d=pa(p1)

欧拉定理: gpa2(p1)1(modpa1)gpa2(p1)1(modpa1) ,即 gpa2(p1)=1+kpa1gpa2(p1)=1+kpa1

由于 ggpapa 的原根,d>pa2(p1)d>pa2(p1),有 gpa2(p1)1(modpa)gpa2(p1)≢1(modpa)

于是又有 pa|kpa1pa/|kpa1gcd(k,p)=1gcd(k,p)=1

然后二项式定理展开,有:

gpa1(p1)=(1+kpa1)p=1+kpa1(modpa+1)gpa1(p1)=(1+kpa1)p=1+kpa≢1(modpa+1)

于是 d=pa(p1)d=pa(p1) , pa+1pa+1 存在原根 gg

Q.E.D.

性质3

对于奇素数 ppaNaN2pa2pa 有原根。

证明:

假设对于 papa 存在原根 gg。首先我们知道对于 2pa2pa ,它的原根必然是奇数的。而对于 papa 的原根 gg ,以及 (g+pa)(g+pa) ,其中至少有一个是奇数,我们记这个奇数为 bb

bb 在模 2pa2pa 的意义下的阶为 dd ,于是有 d|φ(2pa)=φ(pa)d|φ(2pa)=φ(pa)阶的性质2)且 φ(pa)|dφ(pa)|d阶的性质2),即 d=φ(pa)=φ(2pa)d=φ(pa)=φ(2pa)

所以 bb 为模 2pa2pa 的原根。

Q.E.D.

性质4

对于 p2,4,pc,2pcp2,4,pc,2pc ,其不存在原根。

证明:

分三种情况来讨论:

1.对于 p=2a,a3p=2a,a3

2.对于 pp2a2a 与一个奇素数的乘积且 a2a2

3.对于 pp2a2a 与多个奇素数的乘积且 a1a1

我们先处理第 2,32,3 种情况。这两种情况其实可以概括成:

对于 p=rsp=rs ,有 r,s>2r,s>2gcd(r,s)=1gcd(r,s)=1

gcd(r,s)=1gcd(r,s)=1 可知 φ(p)=φ(r)φ(s)φ(p)=φ(r)φ(s) ,然后由 r,s>2r,s>2 得到 4|φ(p)4|φ(p) (因为此时两者的欧拉函数都为偶数)。

d=12φ(p)d=12φ(p) ,那么 φ(r),φ(s)φ(r),φ(s) 都是 dd 的因子。于是对于任意 gcd(a,p)=1gcd(a,p)=1,有 ad1(modr)ad1(modr)ad1(mods)ad1(mods) ,则有 ad1(modp)ad1(modp) 。发现 d<φ(p)d<φ(p),于是没有原根,

而第 11 种情况:

由于互质的限制,我们只需考虑奇数。

因此对于任意奇数 b=2k+1b=2k+1 ,有:

b2a2=(2k+1)2a11+(2a21)2k+(2a22)(2k)21+2a1(k+(2a21)k2)1(mod2a)b2a2=(2k+1)2a11+(2a21)2k+(2a22)(2k)21+2a1(k+(2a21)k2)1(mod2a)

解释一下:

其中第二步,由于从第 44 项开始,都具有 2a1(2a22)2a1(2a22) 的组合,具有 2a2a 的因子,于是可以删去。

其中第三步 k+(2a21)k2k+(2a21)k2 是偶数。(因为两项奇偶性相同)

于是 a2a11(mod2a)a2a1≢1(mod2a),没有原根。

Q.E.D.

性质5:原根存在定理

形如 1,2,4,pc,2pc1,2,4,pc,2pcpp 为奇质数)的数才存在原根。

证明:

对于 p=1,2,4p=1,2,4 的情况,分别有原根 1,1,31,1,3

对于 pcpc 我们在 性质2 中已经证明,对于 2pc2pc 我们在 性质3 中得到证明。

对于 p2,4,pc,2pcp2,4,pc,2pc 的,我们在 性质4 中证明。

综上,该定理成立。

Q.E.D.

性质6:原根的数量级

可以证明,如果 mm 有原根,那么其最小原根 gm0.25gm0.25 ,证明略(我不道啊)。

所以找最小原根是可以暴力找的。怎么找呢?

性质7:原根判定定理

对于 m3,gcd(a,m)=1m3,gcd(a,m)=1 ,则 aa 是模 mm 意义下的原根的充要条件是:对于 φ(m)φ(m) 的每个质因数 pp,都有 aφ(m)p1(modm)aφ(m)p≢1(modm)

证明:

考虑反证,假设存在一个非模 mm 的原根 aa 使得存在一个质因数 pp 满足 aφ(m)p1(modm)aφ(m)p≢1(modm)

于是存在 t<φ(m)t<φ(m) 使得 at1(modm)at1(modm)

裴蜀定理 可知,存在 x,yx,y 使得 ty=φ(m)x+gcd(φ(m),t)ty=φ(m)x+gcd(φ(m),t),于是有 aty1aφ(m)x+gcd(φ(m),t)agcd(t,φ(m))(modm)aty1aφ(m)x+gcd(φ(m),t)agcd(t,φ(m))(modm) ,中间的转换根据欧拉定理。

gcd(t,φ(m))|φ(m)gcd(t,φ(m))|φ(m) ,且 gcd(t,φ(m))t<φ(m)gcd(t,φ(m))t<φ(m)

然后可知存在 φ(m)φ(m) 的质因数 pp 使得 gcd(t,φ(m))|φ(m)pgcd(t,φ(m))|φ(m)p 于是有 aφ(m)p1(modm)aφ(m)p1(modm) ,矛盾。

Q.E.D.

求出原根

有了上面的性质的支撑,我们就可以计算原根了。我们直接暴力枚举然后用原根判定定理检验,其他原根可由这个原根推出。具体来说,gi,i[0,φ(p)]gi,i[0,φ(p)] 在模 pp 意义下互不相同,我们只需要检验其阶是否为 φ(p)φ(p) 即可(凭借 阶的性质4 可以转化为 iiφ(p)φ(p) 是否互质,其揭露了原根循环群的本质)。

复杂度 O(p0.25φ(p)logφ(p)+p)O(p0.25φ(p)logφ(p)+p)

给出参考代码:

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define Better_IO true
namespace IO{
	#if Better_IO==true
		char buf[(1<<20)+3],*p1(buf),*p2(buf);
		const int lim=1<<20;
		inline char gc(){
			if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
			return p1==p2?EOF:*p1++;
		}
		#define pc putchar
	#else
		#define gc getchar
		#define pc putchar
	#endif
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		s="";char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=2e6+3;
int n,d;
inline int inc(int x,int y){
	return (x+=y)>=n?x-n:x;
}
inline int dec(int x,int y){
	return (x-=y)<0?x+n:x;
}
inline int mul(int x,int y){
	return 1ll*x*y%n;
}
inline int qpow(int a,int b){
	int ans=1;
	while(b){
		if(b&1) ans=mul(ans,a);
		a=mul(a,a);
		b>>=1;
	}
	return ans;
}
std::vector<int>pri;
db phi;
inline void get_phi(int x){
	phi=x;
	for(auto y:pri){
		phi*=(y-1);phi/=y;
	}
}
inline bool check(int x){
	for(auto y:pri){
		if(qpow(x,phi/y)==1)
			return 0;
	}
	return 1;
}
int gcd(int x,int y){
	if(y==0) return x;
	return gcd(y,x%y);
}
inline void answer(int x){
	static int ans[N],top;
	top=0;
	for(int i=0;i<phi;++i){
		if(gcd(i,phi)==1){
			ans[++top]=qpow(x,i);
		}
	}
	std::sort(ans+1,ans+1+top);
	printf("%d\n",top);
	for(int i=d;i<=top;i+=d){
		printf("%d ",ans[i]);
	}
}
int main(){
	file(a);
	int T;read(T);
	while(T--){
		read(n,d);
		if(n==2){
			printf("1\n");
			if(d==1) printf("1\n");
			else printf("\n");
			continue;
		}
		int x=n;
		pri.clear();
		for(int i=2;i*i<=x;++i){
			if(x%i==0){
				while(x%i==0) x/=i;
				pri.push_back(i);
			}
		}
		if(x!=1){
			pri.push_back(x);
		}
		get_phi(n);
		x=phi;
		pri.clear();
		for(int i=2;i*i<=x;++i){
			if(x%i==0){
				while(x%i==0) x/=i;
				pri.push_back(i);
			}
		}
		if(x!=1){
			pri.push_back(x);
		}
		bool f=0;
		for(int i=1;i<=n;++i){
			if(check(i) and gcd(i,n)==1){
				answer(i);f=1;
				break;
			}
		}
		if(!f) printf("0\n");
		pc('\n');
	}
	return 0;
}

再论快速数论变换

回到 NTT 本身,考虑使用原根来代替单位根。

首先,我们看看在 FFT 中使用了那些单位根的性质:

1.ωkn=(ω1n)kωkn=(ω1n)k

因此只需求出一个单位根 ω1nω1n

2.ωin,i[0,n)ωin,i[0,n) 互不相同

保证可以得到 nn 个不同点值。

3.ωkn=ωkmodnnωkn=ωkmodnn

有了这条就然后需要推出:

ωk+n2n=ωknn1i=0wain=n[a=0]ωk+n2n=ωknn1i=0wain=n[a=0]

展现出一个循环,或者说圆。

4.ωkana=ωknωkana=ωkn

等分性质。

我们看看怎么使得原根满足这几个性质。

其实是考虑一个模意义下阶为 nn 的一个元素 gg

发现对于模素数 pp,其原根 gg 的阶为 p1p1

于是有 δp(gk)=δm(g)gcd(k,δp(g))=p1gcd(k,p1)δp(gk)=δm(g)gcd(k,δp(g))=p1gcd(k,p1)

我们发现,当 n|(p1)n|(p1) 时, δp(gp1n)=nδp(gp1n)=n ,此时 gp1ngp1n 满足条件 1,2,3

至于为什么满足 3 的推理:

(gn2)2=gn1(modp)(gn2)2=gn1(modp)

gi,i[1,n]gi,i[1,n] 互不相同,得到 gn21(modp)gn21(modp)

gk+n2gk(modp)gk+n2gk(modp) 。、

Q.E.D.

至于另一个推理则同理单位根的证明了。

4 也是易证的: (gp1kn)k=gp1n(gp1kn)k=gp1n

于是 n|(p1)n|(p1) 时,gn2gn2 是可以代替单位根存在的。

我们发现 p=998244353p=998244353 时, p1=223×7×17p1=223×7×17。而我们的 nn22 的幂次,于是总是整除的。我们可以把这种模数称为 NTT模数

接下来直接替换即可。顺带一提, 998244353998244353 的一个原根为 33

至于 IDFT 是由其逆元来弄的,把原根换成其逆元即可。

代码实现
#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define Better_IO true
namespace IO{
	#if Better_IO==true
		char buf[(1<<20)+3],*p1(buf),*p2(buf);
		const int lim=1<<20;
		inline char gc(){
			if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
			return p1==p2?EOF:*p1++;
		}
		#define pc putchar
	#else
		#define gc getchar
		#define pc putchar
	#endif
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		s="";char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=(1<<20)+3;
const int mods=998244353;
inline int inc(int x,int y){
	return (x+=y)>=mods?x-mods:x;
}
inline int dec(int x,int y){
	return (x-=y)<0?x+mods:x;
}
inline int mul(int x,int y){
	return 1ll*x*y%mods;
}
inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=mul(res,a);
		a=mul(a,a);
		b>>=1;
	}
	return res;
}
int n,m;
int g=3,invg=qpow(g,mods-2);
int F[N<<1],G[N<<1],w[N];
int rev[N<<1];
inline void get_rev(){
	for(int i=1;i<n;++i){
		rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0);
	}
}
inline void NTT(int *f,bool I){
	for(int i=0;i<n;++i){
		if(i<rev[i]) std::swap(f[i],f[rev[i] ]);
	}
	for(int p=2;p<=n;p<<=1){
		int len=p>>1;
		w[0]=1;w[1]=qpow(I?invg:g,(mods-1)/p);
		for(int i=2;i<len;++i){
			w[i]=mul(w[i-1],w[1]);
		}
		for(int k=0;k<n;k+=p){
			for(int l=k;l<k+len;++l){
				int tmp=mul(f[l+len],w[l-k]);
				f[l+len]=dec(f[l],tmp);
				f[l]=inc(f[l],tmp);
			}
		}
	}
}
int main(){
	file(a);
	read(n,m);
	for(int i=0;i<=n;++i){
		read(F[i]);
	}
	for(int i=0;i<=m;++i){
		read(G[i]);
	}
	for(m+=n,n=1;n<=m;n<<=1);
	get_rev();
	NTT(F,0);NTT(G,0);
	for(int i=0;i<n;++i) F[i]=mul(F[i],G[i]);
	NTT(F,1);
	int invn=qpow(n,mods-2);
	for(int i=0;i<=m;++i){
		printf("%d ",mul(F[i],invn) );
	}
	return 0;
}

多项式插值

拉格朗日插值

对于 nn 次多项式 f(x)f(x) ,我们知道其 n+1n+1 个点值 (xi,yi)(xi,yi),于是有:

f(x)=n+1i=1yijixxjxixjf(x)=n+1i=1yijixxjxixj

由性质 f(x)f(a)(mod(xa))f(x)f(a)(mod(xa)) 结合中国剩余定理易得。

计算的复杂度为 O(n2)O(n2)

如果点值连续,可通过小转换得到:

f(x)=n+1i=1yin+1j=1(xj)(xi)(1)n+1i(i1)!(n+1i)!f(x)=n+1i=1yin+1j=1(xj)(xi)(1)n+1i(i1)!(n+1i)!

通过预处理可以 O(n)O(n) 计算。

多项式快速插值

快速沃尔什变换

快速沃尔什变换, 即 FWT(Fast Walsh Transform).

其意义是位运算卷积,也就是将我们熟知的加法卷积的加法换为位运算。即:

ck=ij=kaibjck=ij=kaibj

其中我们用 来代指某种位运算。

我们期望的做法同样是将多项式转为点值进行运算后在转回来。

考虑构造一组 FWT[f(x)]FWT[f(x)] 的变化,形如:

FWT[f](i)=n1j=0c(i,j)f(j)FWT[f](i)=n1j=0c(i,j)f(j)

其中 c(i,j)c(i,j) 表示式子的系数。

我们知道, A×B=CA×B=C ,则 FWT[A]×FWT[B]=FWT[C]FWT[A]×FWT[B]=FWT[C]

则:

FWT[A](i)FWT[B](i)=FWT[C](i)n1j=0c(i,j)A(j)n1j=0c(i,j)B(j)=n1j=0c(i,j)C(j)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1j=0c(i,j)C(j)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1j=0c(i,j)ab=jA(a)B(b)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1a=0n1b=0A(a)B(b)c(i,ab)FWT[A](i)FWT[B](i)=FWT[C](i)n1j=0c(i,j)A(j)n1j=0c(i,j)B(j)=n1j=0c(i,j)C(j)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1j=0c(i,j)C(j)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1j=0c(i,j)ab=jA(a)B(b)n1j=0n1k=0c(i,j)c(i,k)A[j]B[k]=n1a=0n1b=0A(a)B(b)c(i,ab)

得到 c(i,j)c(i,k)=c(i,jk)c(i,j)c(i,k)=c(i,jk) 。然后不难发现 c(i,x)c(i,x) 可以把 xx 按照当前二进制操作拆分然后相乘。

我们考虑如下的做法:

FWT[A](i)=n1j=0c(i,j)A(j)=n21j=0c(i,j)A(j)+n1j=n2c(i,j)A(j)FWT[A](i)=n1j=0c(i,j)A(j)=n21j=0c(i,j)A(j)+n1j=n2c(i,j)A(j)

发现左边没有 nn 的最高位 00 ,而右边有。

xx 表示 xx 在二进制上去除最高位,xixi 表示 xx 二进制上从高到底的第 ii 位。

得到:

c(i0,0)n21j=0c(i,j)A(j)+c(i0,1)n1j=n2c(i,j)A(j)c(i0,0)n21j=0c(i,j)A(j)+c(i0,1)n1j=n2c(i,j)A(j)

发现一直这么拆是 O(logn)O(logn) ,合并两式复杂度为 O(n)O(n) ,总复杂度 O(nlogn)O(nlogn)

发现 cc 是个矩阵,我们对其求逆然后再 FWT ,就可以做到 IFWT

然后我们要用的 cc 矩阵只有 2×22×2

接下来为 33 种位运算构造矩阵。构造方法是把式子全列出来,然后手动枚举找可行情况,过于繁琐,于是直接给出构造和细节。

Or

[1110]

[1011]

一般采用第二种,因为其同时具有子集卷积的性质。也是记忆技巧。

[1011]c(i,j)=[i&j=j]

其逆矩阵为:

[1011]

And

[0111]

[1101]

采用第二种,其逆矩阵为:

[1101]

记忆技巧,为 Or 矩阵的转置。

具有超集卷积的性质。

Xor

[1111]

[1111]

仍然采用第二种。

记忆技巧,c(i,j)=(1)i&j

其逆矩阵为:

[12121212]

【模板】快速沃尔什变换 (FWT)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int mod=998244353;
inline int inc(int x,int y){
	return (x+=y)>=mod?x-mod:x;
}
inline int dec(int x,int y){
	return (x-=y)<0?x+mod:x;
}
inline int mul(int x,int y){
	return 1ll*x*y%mod;
}
inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=mul(res,a);
		a=mul(a,a);
		b>>=1;
	}
	return res;
}
const int N=(1<<17)+3;
int _n,n;
int a[N],b[N],ans1[N],ans2[N],ans3[N];
int oa[N],ob[N];
const int inv2=qpow(2,mod-2);
const int 
	Cor[2][2]={{1,0},{1,1} },ICor[2][2]={{1,0},{mod-1,1} },
	Cand[2][2]={{1,1},{0,1} },ICand[2][2]={{1,mod-1},{0,1} },
	Cxor[2][2]={{1,1},{1,mod-1} },ICxor[2][2]={{inv2,inv2},{inv2,dec(mod,inv2)} };
inline void FWT(int *f,const int c[2][2]){
	for(int p=2;p<=n;p<<=1){
		int len=(p>>1);
		for(int k=0;k<n;k+=p){
			for(int l=k;l<k+len;++l){
				int tl=f[l],tr=f[l+len];
				f[l]=inc(mul(c[0][0],tl),mul(c[0][1],tr) );
				f[l+len]=inc(mul(c[1][0],tl),mul(c[1][1],tr) );
			}
		}
	}
}
int main(){
	file(a);
	read(_n);n=(1<<_n);
	for(int i=0;i<n;++i){
		read(a[i]);
		oa[i]=a[i];
	}
	for(int i=0;i<n;++i){
		read(b[i]);
		ob[i]=b[i];
	}
	FWT(a,Cor);FWT(b,Cor);
	for(int i=0;i<n;++i) ans1[i]=mul(a[i],b[i]);
	FWT(ans1,ICor);
	for(int i=0;i<n;++i){
		a[i]=oa[i];
		b[i]=ob[i];
	}
	FWT(a,Cand);FWT(b,Cand);
	for(int i=0;i<n;++i) ans2[i]=mul(a[i],b[i]);
	FWT(ans2,ICand);
	for(int i=0;i<n;++i){
		a[i]=oa[i];
		b[i]=ob[i];
	}
	FWT(a,Cxor);FWT(b,Cxor);
	for(int i=0;i<n;++i) ans3[i]=mul(a[i],b[i]);
	FWT(ans3,ICxor);
	for(int i=0;i<n;++i){
		printf("%d ",ans1[i]);
	}pc('\n');
	for(int i=0;i<n;++i){
		printf("%d ",ans2[i]);
	}pc('\n');
	for(int i=0;i<n;++i){
		printf("%d ",ans3[i]);
	}pc('\n');
	return 0;
}

FWT本质的探索

把数看成很多 01 维,那么 Or 运算等价于每一维度取 maxAnd 运算等价于每一维取 min,而 Xor 就是每维加起来 mod2

然后考虑扩展到 k 进制数,那么 Or 运算等价于每一维度取 maxAnd 运算等价于每一维取 min,而 Xor 就是每维加起来 modk

Or

考虑 Or 运算,发现矩阵里面只会有 0,1 。然后考虑同一行,对于 y1<y2c(x,y1)c(x,y2)=c(x,max{y1,y2})=c(x,y2) 。也就是说,同一行内的构成必定是先 10 。然后为了保证矩阵可逆,其需要满秩,那么每行互不相同即可。我们采用如下矩阵:(假设 k=4

[1000110011101111]

及其逆:

[1000110001100011]

就是每个 1 下面带个 1

两个矩阵可以看做先(对各维子集)做前缀和,再做差分。

套用 FWT ,复杂度 O(nklogkn)

直接计算高维前缀和复杂度 O(nlogkn)(每次 k 进制下把一位挖掉),下同。

And

Or 正好相反,矩阵也恰好是为 Or 矩阵的转置。

[1111011100110001]

及其逆:

[1100011000110001]

两个矩阵可以看做先(对各维超集)做后缀和,再做差分。

Xor

多项式求逆

考虑问题 f(x)g(x)=a(x),求解 a(x)

朴素递推求法

发现,举例假设 g(x)=1x2 :

f(x)g(x)=a(x)

f(x)=a(x)(1x2)

f[i]=a[i]a[i2]

a[i]=f[i]+a[i2]

由此,上项由下项递推而来,可以递推求解。

g(x) 项数为 d, f(x) 项数为 n,则复杂度 O(nd)

参考文献

原根存在定理证明

NTT与多项式全家桶

posted @   cbdsopa  阅读(144)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示