多项式基础操作全家桶
多项式入门教程
基础概念
形如 F(x)=a1+a2x+a3x2+...+anxnF(x)=a1+a2x+a3x2+...+anxn 的东西叫多项式。
然后,这个 nn 可以是无穷大的。
其中上面的 aiai 称为多项式的第 ii 项系数。
多项式乘法
即各项相乘,假设有两个多项式 F(x)=∑ni=0aixiF(x)=∑ni=0aixi 和 G(x)=∑ni=0bixiG(x)=∑ni=0bixi
那么:
然后我们发现这个式子里有一个部分很有意思,我们专门拿出来:
设两多项式的乘积的第 ii 项系数为 cici 那么有:
我们称这个为加法卷积。如果把那个加号换成别的就变成其他的什么卷积了。
那么我们如何计算多项式乘法?你发现可以容易的在 O(n2)O(n2) 内计算。但是这是不优的,我们接下来介绍一种 O(nlogn)O(nlogn) 内计算多项式乘法的算法,先了解一些前置知识。
复数
什么叫复数?形如 a+bia+bi 的叫复数。其中 i2=−1i2=−1,aa 称为一个复数的实部,bb 称为一个复数的虚部。|a+bi||a+bi| 表示其模长。其共轭复数为 a−bia−bi,对于复数 zz ,其共轭复数记作 ¯z¯¯¯z。
然后其四则运算为:
然后模长 |z|=|a+bi|=√z¯z=√a2+b2|z|=|a+bi|=√z¯¯¯z=√a2+b2
a+bi≠0a+bi≠0 时存在逆元 1a+bi=a−bi|a+bi|1a+bi=a−bi|a+bi|
复数几何意义
我们把 xx 坐标表示实部, yy 坐标表示虚部的平面称为复平面。可以发现是数轴的扩展。
可以把复数看做一个在复平面上的向量 (a,b)(a,b),记其幅角为 θθ ,模长为 rr 那么有 a=rcosθa=rcosθ 且 b=rsinθb=rsinθ,那么其实复数还可表示为 r(cosθ+isinθ)r(cosθ+isinθ) 。
然后你会惊奇的发现复数相乘相当于模长相乘,幅角相加。
单位根
我们称满足 zn=1zn=1 的复数 zz 为 nn 次单位根。
首先由于模长相乘,幅角相加的乘法,我们知道 nn 次单位根模长为 11,nθ=2kπnθ=2kπ。因此不同的 nn 次单位根总共有 nn 个,它们将单位圆 nn 等分。
一般我们用 winwin 表示第 i+1i+1 个 nn 次单位根,即 θin=2iπnθin=2iπn 。
一些性质
性质1
性质2
角度所占比相同,模长都为 11 ,因此固然相同。
性质3
单位根可以看做 1i1i 圆周。
性质4
旋转了一个半圆,因此相反。
性质5
证明:
当 a=0a=0 时,等价于 nn 个第 11 个 nn 次单位根相加,就是 nn 个 11 相加,于是得到 nn。
当 a≠0a≠0,
先了解一下等比数列求和公式
对于 a,aq,aq2,...,aqna,aq,aq2,...,aqn 求 Sn=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)+1−xn+1f(x)=xf(x)+1−xn+1 即 f(x)=1−xn+11−xf(x)=1−xn+11−x
那么 Sn=af(q)=a1−qn+11−qSn=af(q)=a1−qn+11−q
好,那么把 w0nw0n 代入 aa ,wanwan 代入 qq 得到
Q.E.D.
离散傅里叶变换
简称 DFT
(Discrete Fourier Transform)。
其逆变换称为 IDFT
。
所以实际上
我们知道, nn 个方程可以确定 nn 个未知数。也就是说对于一个 nn 次多项式,我们只需要 n+1n+1 个点值就可以求出多项式的系数。点值是什么?将 xx 的取值代入多项式得到的值。
然后经过简单验证可以发现,F,GF,G 在 xx 处的点值的乘积,等于 F×GF×G 在 xx 处的点值。
因为我们发现多项式在点值上相乘更快,所以我们考虑一种系数表示转点值表示再转回系数表示的做法。
而这个东西的作用和点值表示有一些相同的性质,以及一个更为优秀的性质。因为傅里叶变换的本质是把单位根代入多项式。
回到正题,傅里叶变换。
假设对于一个多项式 ff 我们知道其在 00 到 n−1n−1 处的系数。
那么定义其傅里叶变换 F[f]F[f] 满足:(两式的单位根的次方的正负号可以交换)
先证明一下这两个互为逆运算,即证明:
证明:
怎么证明?全部展开!
同理另一个得证。
Q.E.D.
然后实际上,你发现:
卷积定理
然后有:
证明:
也就是傅里叶变换的先后是无关的,所以可以用傅里叶变换的乘积进行逆傅里叶变换得到乘积。
快速傅里叶变换
简称 FFT
(Fast Fourier Transform),用来快速计算傅里叶变换。
为方便处理,我们把 nn 取到大于等于自身的最小的 22 的幂次。(不用担心会计算错误,因为不存在的高位系数都是 00)。
记 m=n2m=n2
设:
然后有 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 。(三个结论暴力拆解易证)
所以 F0F0 和 F1F1 都只计算前 mm 项,这个分治处理,边界是 m−1=1m−1=1,此时两式都是原多项式。
然后复杂度 T(n)=2T(n2)+O(n)T(n)=2T(n2)+O(n) 于是总复杂度 O(nlogn)O(nlogn)
然后再观察一下 FF 和 F−1F−1:
如果令 w′n=w−1nw′n=w−1n :
发现和 FF 长得其实巨像无比,所以我们只需要将 w′nw′n 作为单位根进行傅里叶变换就可以实现逆傅里叶变换。
代码实现
由于考虑到代码常数的问题,我们就不考虑实现递归版了。因为这个理解和实现起来都简单。这个递归版的实现可以看 这里
为了保证一个能够过题的常数,我们考虑以下的一些优化。
别的没什么好说的,我们重点说一下 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 7 第1层
0 2 4 6|1 3 5 7 第2层
0 4|2 6|1 5|3 7 第3层
0|4|2|6|1|5|3|7 第4层
成立。
假设结论对于 n=2kn=2k 成立,证明对于 n=2k+1n=2k+1 也成立。
有
左边的位置全部除去二进制下最低位的 00 得到
我们知道,对于 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 使得 ak≡1(modp)ak≡1(modp)。其中 gcd(a,p)=1gcd(a,p)=1。记作 δp(a)δp(a)。
本质上是幂次的最小循环节。
性质1
根据欧拉定理,我们发现一个数 aa 的阶会小于等于 φ(a)φ(a)。
性质2
若 an≡1(modm)an≡1(modm) , 则 δm(a)|nδm(a)|n
证明:
令 n=δm(a)q+r(0≤r<δm(a))n=δm(a)q+r(0≤r<δm(a))
当 r>0r>0 有 ar≡ar(aδm(a))q≡1(modm)ar≡ar(aδm(a))q≡1(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
证明:
必要性证明:
由阶的定义可得
由 性质2
可知
由 δm(ab)=δm(a)δm(b)δm(ab)=δ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.
充分性证明:
可得 δ(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(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)
再由:
于是有 δ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)≡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)=(x−x0)g(x)f(x)−f(x0)=(x−x0)g(x) ,因此可知 g(x)g(x) 在模 pp 意义下最多是 n−1n−1 次多项式,又因为对于 i∈[0,n]i∈[0,n] 都满足 f(xi)≡0(modp)f(xi)≡0(modp) ,于是有:
对于 i∈[1,n]i∈[1,n] 有,
又因为 xi≢x0(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 ,存在 c∈Zc∈Z 使得 δp(c)=lcm(δp(a),δp(b))δp(c)=lcm(δp(a),δp(b))
证明:
对于 δp(a),δp(b)δp(a),δp(b) 质因数分解,得到:
令 k1k1 为所有 ni≤mini≤mi 的 pnipni 的乘积,k2k2 为所有 ni>mini>mi 的 pmiipmii 的乘积。
然后取 x,yx,y 使得 δp(a)=k1x,δP(b)=k2yδp(a)=k1x,δP(b)=k2y。可以发现 gcd(x,y)=1gcd(x,y)=1 且 lcm(δ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))=1 且 gcd(a,p)=gcd(b,p)=1gcd(a,p)=gcd(b,p)=1)
可得 c=ak1bk2c=ak1bk2 。
Q.E.D.
性质1
对于任意奇素数(非 22 素数)pp ,其存在原根。
证明:
由上面的 定理1
,可以发现存在一个 gg 使得 δp(g)=lcm(δp(1),...,δp(p−1))δp(g)=lcm(δp(1),...,δp(p−1))
于是,对于任意 i∈[1,p−1]i∈[1,p−1] 有 δ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)≤p−1δp(g)≤p−1 ,同时由费马小定理,有 δp(g)≥p−1δp(g)≥p−1 ,可得 δp(g)=p−1=φ(p)δp(g)=p−1=φ(p) 。
所以 gg 是模 pp 意义下的原根。
Q.E.D.
性质2
对于奇素数 pp 和 a∈N∗a∈N∗ ,papa 有原根。
证明:
考虑先证明 p2p2 的原根存在。
记 pp 的一个原根为 gg ,其在模 p2p2 的阶为 dd ,则有 d|φ(p2)d|φ(p2) (欧拉定理
+阶的性质2
),再由 gd≡1(modp)gd≡1(modp) 于是有 gd≡1(modp2)gd≡1(modp2),因此 φ(p)|dφ(p)|d。
由于 φ(p)=(p−1)|dφ(p)=(p−1)|d 且 d|p(p−1)=φ(p2)d|p(p−1)=φ(p2) ,pp 为奇素数,可知 d=p−1d=p−1 或 d=p(p−1)d=p(p−1) 。
当 d=p−1d=p−1 ,则 gg 为 p2p2 的原根。当 d=p(p−1)d=p(p−1) 时,考虑证明 (g+p)(g+p) 是 p2p2 的原根。
首先 (g+p)(g+p) 为 pp 的原根。同上可知 g+pg+p 在 p2p2 的阶为 (p−1)(p−1) 或 p(p−1)p(p−1) 。由 gp−1≡1(modp2)gp−1≡1(modp2) 结合二项式定理可得:
可知 (g+p)p−1≢1(modp2)(g+p)p−1≢1(modp2) ,则 (g+p)p(p−1)≡1(modp2)(g+p)p(p−1)≡1(modp2)。
然后可以归纳证明:
假设 gg 是 papa 的原根 ,dd 是 gg 对于模 pa+1pa+1 的阶,考虑证明 gg 是 pa+1pa+1 。于是类似的可以得到 d=pa−1(p−1)d=pa−1(p−1) 或 d=pa(p−1)d=pa(p−1)。
由 欧拉定理
: gpa−2(p−1)≡1(modpa−1)gpa−2(p−1)≡1(modpa−1) ,即 gpa−2(p−1)=1+kpa−1gpa−2(p−1)=1+kpa−1
由于 gg 为 papa 的原根,d>pa−2(p−1)d>pa−2(p−1),有 gpa−2(p−1)≢1(modpa)gpa−2(p−1)≢1(modpa)
于是又有 pa⧸|kpa−1pa/|kpa−1 即 gcd(k,p)=1gcd(k,p)=1
然后二项式定理展开,有:
于是 d=pa(p−1)d=pa(p−1) , pa+1pa+1 存在原根 gg。
Q.E.D.
性质3
对于奇素数 pp 和 a∈N∗a∈N∗ ,2pa2pa 有原根。
证明:
假设对于 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
对于 p≠2,4,pc,2pcp≠2,4,pc,2pc ,其不存在原根。
证明:
分三种情况来讨论:
1.对于 p=2a,a≥3p=2a,a≥3
2.对于 pp 为 2a2a 与一个奇素数的乘积且 a≥2a≥2
3.对于 pp 为 2a2a 与多个奇素数的乘积且 a≥1a≥1
我们先处理第 2,32,3 种情况。这两种情况其实可以概括成:
对于 p=rsp=rs ,有 r,s>2r,s>2 且 gcd(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,有 ad≡1(modr)ad≡1(modr) 和 ad≡1(mods)ad≡1(mods) ,则有 ad≡1(modp)ad≡1(modp) 。发现 d<φ(p)d<φ(p),于是没有原根,
而第 11 种情况:
由于互质的限制,我们只需考虑奇数。
因此对于任意奇数 b=2k+1b=2k+1 ,有:
解释一下:
其中第二步,由于从第 44 项开始,都具有 2a−1(2a−2−2)2a−1(2a−2−2) 的组合,具有 2a2a 的因子,于是可以删去。
其中第三步 k+(2a−2−1)k2k+(2a−2−1)k2 是偶数。(因为两项奇偶性相同)
于是 a2a−1≢1(mod2a)a2a−1≢1(mod2a),没有原根。
Q.E.D.
性质5:原根存在定理
形如 1,2,4,pc,2pc1,2,4,pc,2pc (pp 为奇质数)的数才存在原根。
证明:
对于 p=1,2,4p=1,2,4 的情况,分别有原根 1,1,31,1,3。
对于 pcpc 我们在 性质2
中已经证明,对于 2pc2pc 我们在 性质3
中得到证明。
对于 p≠2,4,pc,2pcp≠2,4,pc,2pc 的,我们在 性质4
中证明。
综上,该定理成立。
Q.E.D.
性质6:原根的数量级
可以证明,如果 mm 有原根,那么其最小原根 g≤m0.25g≤m0.25 ,证明略(我不道啊)。
所以找最小原根是可以暴力找的。怎么找呢?
性质7:原根判定定理
对于 m≥3,gcd(a,m)=1m≥3,gcd(a,m)=1 ,则 aa 是模 mm 意义下的原根的充要条件是:对于 φ(m)φ(m) 的每个质因数 pp,都有 aφ(m)p≢1(modm)aφ(m)p≢1(modm)。
证明:
考虑反证,假设存在一个非模 mm 的原根 aa 使得存在一个质因数 pp 满足 aφ(m)p≢1(modm)aφ(m)p≢1(modm) 。
于是存在 t<φ(m)t<φ(m) 使得 at≡1(modm)at≡1(modm)。
由 裴蜀定理
可知,存在 x,yx,y 使得 ty=φ(m)x+gcd(φ(m),t)ty=φ(m)x+gcd(φ(m),t),于是有 aty≡1≡aφ(m)x+gcd(φ(m),t)≡agcd(t,φ(m))(modm)aty≡1≡aφ(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)p≡1(modm)aφ(m)p≡1(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
有了这条就然后需要推出:
展现出一个循环,或者说圆。
4
.ωkana=ωknωkana=ωkn
等分性质。
我们看看怎么使得原根满足这几个性质。
其实是考虑一个模意义下阶为 nn 的一个元素 gg。
发现对于模素数 pp,其原根 gg 的阶为 p−1p−1。
于是有 δp(gk)=δm(g)gcd(k,δp(g))=p−1gcd(k,p−1)δp(gk)=δm(g)gcd(k,δp(g))=p−1gcd(k,p−1)
我们发现,当 n|(p−1)n|(p−1) 时, δp(gp−1n)=nδp(gp−1n)=n ,此时 gp−1ngp−1n 满足条件 1,2,3
。
至于为什么满足 3
的推理:
由 (gn2)2=gn≡1(modp)(gn2)2=gn≡1(modp)
由 gi,i∈[1,n]gi,i∈[1,n] 互不相同,得到 gn2≡−1(modp)gn2≡−1(modp)
得 −gk+n2≡gk(modp)−gk+n2≡gk(modp) 。、
Q.E.D.
至于另一个推理则同理单位根的证明了。
4
也是易证的: (gp−1kn)k=gp−1n(gp−1kn)k=gp−1n 。
于是 n|(p−1)n|(p−1) 时,gn2gn2 是可以代替单位根存在的。
我们发现 p=998244353p=998244353 时, p−1=223×7×17p−1=223×7×17。而我们的 nn 是 22 的幂次,于是总是整除的。我们可以把这种模数称为 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)≡f(a)(mod(x−a))f(x)≡f(a)(mod(x−a)) 结合中国剩余定理易得。
计算的复杂度为 O(n2)O(n2)。
如果点值连续,可通过小转换得到:
通过预处理可以 O(n)O(n) 计算。
多项式快速插值
快速沃尔什变换
快速沃尔什变换, 即 FWT
(Fast Walsh Transform).
其意义是位运算卷积,也就是将我们熟知的加法卷积的加法换为位运算。即:
其中我们用 ⊕⊕ 来代指某种位运算。
我们期望的做法同样是将多项式转为点值进行运算后在转回来。
考虑构造一组 FWT[f(x)]FWT[f(x)] 的变化,形如:
其中 c(i,j)c(i,j) 表示式子的系数。
我们知道, A×B=CA×B=C ,则 FWT[A]×FWT[B]=FWT[C]FWT[A]×FWT[B]=FWT[C] 。
则:
得到 c(i,j)c(i,k)=c(i,j⊕k)c(i,j)c(i,k)=c(i,j⊕k) 。然后不难发现 c(i,x)c(i,x) 可以把 xx 按照当前二进制操作拆分然后相乘。
我们考虑如下的做法:
发现左边没有 nn 的最高位 00 ,而右边有。
记 x′x′ 表示 xx 在二进制上去除最高位,xixi 表示 xx 二进制上从高到底的第 ii 位。
得到:
发现一直这么拆是 O(logn)O(logn) ,合并两式复杂度为 O(n)O(n) ,总复杂度 O(nlogn)O(nlogn)。
发现 cc 是个矩阵,我们对其求逆然后再 FWT
,就可以做到 IFWT
。
然后我们要用的 cc 矩阵只有 2×22×2。
接下来为 33 种位运算构造矩阵。构造方法是把式子全列出来,然后手动枚举找可行情况,过于繁琐,于是直接给出构造和细节。
Or
或
一般采用第二种,因为其同时具有子集卷积的性质。也是记忆技巧。
其逆矩阵为:
And
或
采用第二种,其逆矩阵为:
记忆技巧,为 Or 矩阵的转置。
具有超集卷积的性质。
Xor
或
仍然采用第二种。
记忆技巧,c(i,j)=(−1)i&j。
其逆矩阵为:
#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 运算等价于每一维度取 max , And 运算等价于每一维取 min,而 Xor 就是每维加起来 mod2。
然后考虑扩展到 k 进制数,那么 Or 运算等价于每一维度取 max , And 运算等价于每一维取 min,而 Xor 就是每维加起来 modk。
Or
考虑 Or 运算,发现矩阵里面只会有 0,1 。然后考虑同一行,对于 y1<y2 有 c(x,y1)c(x,y2)=c(x,max{y1,y2})=c(x,y2) 。也就是说,同一行内的构成必定是先 1 后 0 。然后为了保证矩阵可逆,其需要满秩,那么每行互不相同即可。我们采用如下矩阵:(假设 k=4)
及其逆:
就是每个 1 下面带个 −1 。
两个矩阵可以看做先(对各维子集)做前缀和,再做差分。
套用 FWT
,复杂度 O(nklogkn)。
直接计算高维前缀和复杂度 O(nlogkn)(每次 k 进制下把一位挖掉),下同。
And
和 Or 正好相反,矩阵也恰好是为 Or 矩阵的转置。
及其逆:
两个矩阵可以看做先(对各维超集)做后缀和,再做差分。
Xor
多项式求逆
考虑问题 f(x)g(x)=a(x),求解 a(x)。
朴素递推求法
发现,举例假设 g(x)=1−x2 :
由此,上项由下项递推而来,可以递推求解。
设 g(x) 项数为 d, f(x) 项数为 n,则复杂度 O(nd)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具