这个东西是个玄乎的东西,反正最后还是背板子 理论知识也不用管太多吧
这里搬一些简单的证明
复数运算法则 设复数 A=a+bi,B=c+di A = a + b i , B = c + d i
A+B=(a+c)+(b+d)i
A
+
B
=
(
a
+
c
)
+
(
b
+
d
)
i
A−B=(a−c)+(b−d)i
A
−
B
=
(
a
−
c
)
+
(
b
−
d
)
i
A×B=(ac−bd)+(ad+bc)i
A
×
B
=
(
a
c
−
b
d
)
+
(
a
d
+
b
c
)
i
除法转化为乘共轭复数就好了
一般系统有自带的
std::complex<double>
s
t
d
::
c
o
m
p
l
e
x
<
d
o
u
b
l
e
>
可以直接用
但是手写速度要有保证一些
首先整篇文章讨论的多项式的次数都是 2k−1 2 k − 1 次的,以下不作说明
ωkn=cos(k2πn)+i×sin(k2πn) ω n k = c o s ( k 2 π n ) + i × s i n ( k 2 π n ) 这个根据复数乘法可知
ωkn=ω2k2n ω n k = ω 2 n 2 k 这个根据上面那条东西可以得到
ωk+n2n=−ωkn ω n k + n 2 = − ω n k 代入第一个式子得出 ωn2=−1 ω n 2 = − 1 即可
那么我们假设原多项式是 A(x)=∑n−1i=0aixi A ( x ) = ∑ i = 0 n − 1 a i x i
奇偶分组后两个多项式分别为
A1(x)=a0+a2x+...+an−2xn−12
A
1
(
x
)
=
a
0
+
a
2
x
+
.
.
.
+
a
n
−
2
x
n
−
1
2
A2(x)=a1+a3x+...+an−1xn−12
A
2
(
x
)
=
a
1
+
a
3
x
+
.
.
.
+
a
n
−
1
x
n
−
1
2
则
A(x)=A1(x2)+x×A2(x2)
A
(
x
)
=
A
1
(
x
2
)
+
x
×
A
2
(
x
2
)
我们试着推导以下已知
A1,A2
A
1
,
A
2
怎么还原出
A
A
设, 则
k和k+n2
k
和
k
+
n
2
可以取遍
[0,n−1]
[
0
,
n
−
1
]
的所有值
A(ωkn)=A1(ω2kn)+ωkn×A2(ω2kn)
A
(
ω
n
k
)
=
A
1
(
ω
n
2
k
)
+
ω
n
k
×
A
2
(
ω
n
2
k
)
=A1(ωkn2)+ωkn×A2(ωkn2)
=
A
1
(
ω
n
2
k
)
+
ω
n
k
×
A
2
(
ω
n
2
k
)
A(ωk+n2n)=A1(ω2k+nn)+ωk+n2n×A2(ω2k+nn)
A
(
ω
n
k
+
n
2
)
=
A
1
(
ω
n
2
k
+
n
)
+
ω
n
k
+
n
2
×
A
2
(
ω
n
2
k
+
n
)
=A1(ωkn2)−ωkn×A2(ωkn2)
=
A
1
(
ω
n
2
k
)
−
ω
n
k
×
A
2
(
ω
n
2
k
)
具体推式子只需要之前推出的单位复根的性质即可
这样子就可以分治下去直到一个常数项为止,我们得到了一个在
O(nlogn)
O
(
n
l
o
g
n
)
时间把多项式从系数转化为点值的算法
一般点值表示法不是很常用,所以我们还要考虑怎么把一个多项式从点值转换为系数
设
ck=∑n−1i=0yi(ω−kn)i
c
k
=
∑
i
=
0
n
−
1
y
i
(
ω
n
−
k
)
i
也就是多项式
C(x)=∑n−1i=0yixi
C
(
x
)
=
∑
i
=
0
n
−
1
y
i
x
i
在
x
x
取
其中
yi
y
i
表示
x取ωin时的A(x)的值
x
取
ω
n
i
时
的
A
(
x
)
的
值
下面开始推式子
ck=∑n−1i=0yi(ω−kn)i
c
k
=
∑
i
=
0
n
−
1
y
i
(
ω
n
−
k
)
i
ck=∑n−1i=0(ω−kn)i×(∑n−1j=0aj(ωin)j)
c
k
=
∑
i
=
0
n
−
1
(
ω
n
−
k
)
i
×
(
∑
j
=
0
n
−
1
a
j
(
ω
n
i
)
j
)
ck=∑n−1i=0∑n−1j=0(ωj−kn)iaj
c
k
=
∑
i
=
0
n
−
1
∑
j
=
0
n
−
1
(
ω
n
j
−
k
)
i
a
j
ck=∑n−1j=0aj∑n−1i=0(ωj−kn)i
c
k
=
∑
j
=
0
n
−
1
a
j
∑
i
=
0
n
−
1
(
ω
n
j
−
k
)
i
我们发现
∑n−1i=0(ωj−kn)i
∑
i
=
0
n
−
1
(
ω
n
j
−
k
)
i
是一个等比数列求和的形式
带入高中数学和蔼可亲的公式
Sn=a11−qn1−q
S
n
=
a
1
1
−
q
n
1
−
q
我们发现公比
q=ωj−kn,a1=1
q
=
ω
n
j
−
k
,
a
1
=
1
则
Sn=1−(ωj−kn)n1−q
S
n
=
1
−
(
ω
n
j
−
k
)
n
1
−
q
当分母不为
0
0
时, , 否则
Sn=n
S
n
=
n
则
ck=akn
c
k
=
a
k
n
即
ak=ckn
a
k
=
c
k
n
我们可以通过之前得到的
O(nlogn))
O
(
n
l
o
g
n
)
)
计算多项式点值的方法来实现
最后记得除以
n
n
就好了
总体就是快速将系数转为点值为了实现两个多项式相乘
最后再快速转为系数,通过单位复根优秀的性质来实现
贴个递归的板子
Codes
#include<bits/stdc++.h>
#define For(i, a, b) for(register int i = a; i <= b; ++ i)
using namespace std;
const int maxn = 4e6 + 10;
const double Pi = acos(-1);
int n, m;
struct Complex {
double x, y;
Complex (double xx = 0, double yy = 0) {
x = xx, y = yy;
}
Complex operator + (const Complex &T) {
return Complex{x + T.x, y + T.y};
}
Complex operator - (const Complex &T) {
return Complex{x - T.x, y - T.y};
}
Complex operator * (const Complex &T) {
return Complex{x * T.x - y * T.y, x * T.y + y * T.x};
}
}a[maxn], b[maxn];
void FFT(int limit, Complex *a, int fh) {
if(limit == 1) return;
Complex a1[limit >> 1], a2[limit >> 1];
for(int i = 0; i <= limit; i += 2)
a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];
FFT(limit >> 1, a1, fh), FFT(limit >> 1, a2, fh);
Complex wn1 = Complex(cos(2.0 * Pi / limit), sin(2.0 * Pi / limit) * fh), w = Complex(1, 0);
for(int i = 0; i < (limit >> 1); ++ i, w = w * wn1)
a[i] = a1[i] + w * a2[i], a[i + (limit >> 1)] = a1[i] - w * a2[i];
}
int main() {
#ifndef ONLINE_JUDGE
freopen("FFT.in", "r", stdin);
freopen("FFT.out", "w", stdout);
#endif
int x, limit = 1; scanf("%d%d", &n, &m);
For(j, 0, n) scanf("%d", &x), a[j].x = x;
For(j, 0, m) scanf("%d", &x), b[j].x = x;
while(limit <= n + m) limit <<= 1;
FFT(limit, a, 1), FFT(limit, b, 1);
For(i, 0, limit - 1) a[i] = a[i] * b[i];
FFT(limit, a, -1);
For(i, 0, n + m) printf("%d ", int(a[i].x / limit + 0.5));
return 0;
}
我们发现递归的效率不是很高 考虑迭代实现
盗用网上的一张图
发现最后每一位上数的下标为原下标的二进制位反转
对于
0,1,2,3,4,5,6,7
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
翻转后就是
0,4,2,6,1,5,3,7
0
,
4
,
2
,
6
,
1
,
5
,
3
,
7
把这几个数都写成二进制形式规律就很清楚了
那么迭代实现就直接加上
void FFT_Init(int limit, int *a) {
int k = log2(limit);
for(int j = 0; j < limit; ++ j) {
int t = 0;
for(int i = 0; i < k; ++ i)
if((1 << i) & j)
t |= 1 << (k - j - 1);
if(t < j) swap(a[t], a[j]);
}
}
我们来考虑一种更为优秀的基于递推的 O(n) O ( n ) 预处理
k = log2(limit);
for(int i = 0; i < limit; ++ i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (k - 1));
如果这个数是偶数,把它最后一位去掉后翻转,就相当于后面多了一个
0
0
所以删去这个即可
如果是奇数那第一位是
1
1
,所以补上就好了
上板子
#include<bits/stdc++.h>
#define For(i, a, b) for(register int i = a; i <= b; ++ i)
using namespace std;
const int maxn = 4e6 + 10;
const double Pi = acos(-1);
struct Complex {
double x, y;
Complex(double xx = 0, double yy = 0) {
x = xx, y = yy;
}
Complex operator + (const Complex &T) {
return Complex(x + T.x, y + T.y);
}
Complex operator - (const Complex &T) {
return Complex(x - T.x, y - T.y);
}
Complex operator * (const Complex &T) {
return Complex(x * T.x - y * T.y, x * T.y + y * T.x);
}
}a[maxn], b[maxn];
void FFT(int n, Complex *a, int fh) {
int k = log2(n);
For(i, 0, n - 1) {
int t = 0;
For(j, 0, k - 1)
if((1 << j) & i)
t |= (1 << (k - j - 1));
if(i < t) swap(a[i], a[t]);
}
for(int limit = 2; limit <= n; limit <<= 1) {
double xita = 2.0 * Pi / limit;
Complex Wn1 = Complex(cos(xita), sin(xita) * fh), W = Complex(1, 0);
for(int j = 0; j < n; j += limit, W = Complex(1, 0))
for(int i = j; i < j + (limit >> 1); ++ i, W = W * Wn1) {
Complex a1 = a[i], a2 = W * a[i + (limit >> 1)];
a[i] = a1 + a2, a[i + (limit >> 1)] = a1 - a2;
}
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("3803.in", "r", stdin);
freopen("3803.out", "w", stdout);
#endif
int n, m, limit = 1, x;
scanf("%d%d", &n, &m);
while(limit <= (n + m))
limit <<= 1;
For(i, 0, n)
scanf("%d", &x), a[i].x = x;
For(i, 0, m)
scanf("%d", &x), b[i].x = x;
FFT(limit, a, 1), FFT(limit, b, 1);
For(i, 0, limit - 1)
a[i] = a[i] * b[i];
FFT(limit, a, -1);
For(i, 0, n + m)
printf("%d ", int(a[i].x / limit + 0.5));
return 0;
}
从 FFT F F T 到 NTT N T T
NTT 快速数论变换
听到这个名字就知道
NTT
N
T
T
这个是在数论中使用的
FFT
F
F
T
已经很优秀了,那我们为什么还需要这样的东西代替它呢
因为
FFT
F
F
T
在复数运算的时候会有精度误差,不能保证得到答案的精确性
这个时候我们就想能不能找到一个东西代替复数的运算
这个时候
NTT
N
T
T
就横空出世了,首先
NTT
N
T
T
针对的是模意义下的运算
那么我们考虑怎么找到一个跟单位复根有着相同性质的东西
那就是原根 (本文讨论的模数为
p=qn+1(n=2k)
p
=
q
n
+
1
(
n
=
2
k
)
)
原根的定义
设
m
m
是正整数, 是整数,若
a
a
模 的阶等于
ϕ(m)
ϕ
(
m
)
,则称
a
a
为模 的一个原根
阶的定义
若
a,p
a
,
p
互素,且
p>1
p
>
1
,对于
an≡1(mod p)
a
n
≡
1
(
m
o
d
p
)
最小的n,我们称之为
a
a
模的阶,记做
δp(a)
δ
p
(
a
)
,例如
δ7(2)=3
δ
7
(
2
)
=
3
,因为
23≡1(mod 7)
2
3
≡
1
(
m
o
d
7
)
这样子我们就可以发现原根这个东西是不唯一的
并且原根有一个很重要的定理我不会证
若
p
p
为素数,假设一个数是
p
p
的原根,那么的结果两两不同,这样子就满足了可以代不同的
x
x
得到多项式的点值了
上面这个东西我在网上找了很多资料也没有证明 可能可以问一下数竞的选手应该会
1.令 则
ω0n,ω1n,...,ωn−1n
ω
n
0
,
ω
n
1
,
.
.
.
,
ω
n
n
−
1
互不相同
2.
ωkn=ω2k2n
ω
n
k
=
ω
2
n
2
k
代入第一个式子即可
3.
ωk+n2n=−ωkn
ω
n
k
+
n
2
=
−
ω
n
k
ωnn=gqn=gp−1≡1(mod p)
ω
n
n
=
g
q
n
=
g
p
−
1
≡
1
(
m
o
d
p
)
(费马小定理可知)
(ωn2n)2≡1(mod p)
(
ω
n
n
2
)
2
≡
1
(
m
o
d
p
)
又因为
ωn2n≠1(mod p)
ω
n
n
2
≠
1
(
m
o
d
p
)
所以
ωn2n≡−1(mod p)
ω
n
n
2
≡
−
1
(
m
o
d
p
)
即
ωk+n2n=−ωkn
ω
n
k
+
n
2
=
−
ω
n
k
这样子原根就有了单位复根的所有性质了,那么就可以直接用原根代替单位复根,模意义下运算代替复数运算,就得到了可爱的
NTT
N
T
T
了
还有要注意的几个地方 一般
NTT
N
T
T
模的数是
998244353
998244353
和
1004535809
1004535809
这两个数都有一个共同的原根为
3
3
,中用
3
3
就好了,如果你还想模其他的数那么就多用几个模数做,用中国剩余定理合并就好啦
每次分治时用的
ωn=gmod−1n
ω
n
=
g
m
o
d
−
1
n
我的理解是为了保证
ωnn=1
ω
n
n
=
1
因为推
ωk+n2n=−ωkn
ω
n
k
+
n
2
=
−
ω
n
k
有用到过
当然逆运算的时候不要忘记把
g
g
换成就好啦
Codes
#include<bits/stdc++.h>
using namespace std;
const int N = 4e6 + 10;
const int mod = 998244353;
int a[N], b[N];
int qpow(int a, int x) {
int ret = 1;
for(; x; a = 1ll * a * a % mod, x >>= 1)
if(x & 1)
ret = 1ll * ret * a % mod;
return ret;
}
void NTT_Init(int limit, int *a, int k) {
for(int i = 0; i < limit; ++ i) {
int t = 0;
for(int j = 0; j < k; ++ j)
if((1 << j) & i)
t |= 1 << (k - j - 1);
if(t < i) swap(a[t], a[i]);
}
}
void NTT(int n, int *a, int fh) {
int k = log2(n), inv = qpow(n, mod - 2);
NTT_Init(n, a, k);
for(int limit = 2; limit <= n; limit <<= 1) {
int Wn1 = qpow(3, (mod - 1) / (limit)), w = 1;
if(fh == -1) Wn1 = qpow(Wn1, mod - 2);
for(int j = 0; j < n; j += limit, w = 1)
for(int i = j; i < j + (limit >> 1); ++ i, w = 1ll * w * Wn1 % mod) {
int a1 = a[i], a2 = 1ll * a[i + (limit >> 1)] * w % mod;
a[i] = (a1 + a2) % mod, a[i + (limit >> 1)] = (a1 - a2 + mod) % mod;
}
}
if(fh == -1)
for(int i = 0; i < n; ++ i)
a[i] = 1ll * a[i] * inv % mod;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("34.in", "r", stdin);
freopen("34.out", "w", stdout);
#endif
int n, m, limit = 1;
scanf("%d%d", &n, &m);
while(limit <= (n + m))
limit <<= 1;
for(int i = 0; i <= n; ++ i)
scanf("%d", &a[i]);
for(int i = 0; i <= m; ++ i)
scanf("%d", &b[i]);
NTT(limit, a, 1), NTT(limit, b, 1);
for(int i = 0; i < limit; ++ i)
a[i] = 1ll * a[i] * b[i] % mod;
NTT(limit, a, -1);
for(int i = 0; i <= n + m; ++ i)
printf("%d ", a[i]);
return 0;
}
终于写完了 ovo o v o 这种东西对我这种蒟蒻选手来说真的理解起来不容易,感谢 menci m e n c i 写的博客