第一次做FFT/NTT的题就是一题超难的…想看系数优化的可以跳2。
总结来说:
假设我们现在要做大整数相乘 ( a N . . . a 2 a 1 a 0 ‾ ) 10 ∗ ( b N . . . b 2 b 1 b 0 ‾ ) 10 (\overline{a_N...a_2 a_1 a_0})_{10} * (\overline{b_N...b_2 b_1 b_0})_{10} (aN...a2a1a0)10∗(bN...b2b1b0)10正常来说我们需要一位一位来乘,需要 O ( N 2 ) O(N^2) O(N2)复杂度。
让我们看看另外一个思路:
其实我们要计算
F
a
(
10
)
∗
F
b
(
10
)
F_a(10)*F_b(10)
Fa(10)∗Fb(10)
,其中
{
F
a
(
x
)
=
a
0
+
a
1
∗
x
+
a
2
∗
x
2
+
.
.
.
+
a
n
∗
x
N
F
b
(
x
)
=
b
0
+
b
1
∗
x
+
b
2
∗
x
2
+
.
.
.
+
b
m
∗
x
N
令
G
(
x
)
=
F
a
(
x
)
∗
F
b
(
x
)
G(x) = F_a(x)*F_b(x)
G(x)=Fa(x)∗Fb(x)
如果我们可以求出
G
(
x
)
G(x)
G(x),那么代入
G
(
10
)
G(10)
G(10)就可以在
O
(
2
N
)
=
O
(
N
)
O(2N)=O(N)
O(2N)=O(N)(多项式应该会有
2
N
−
1
2N-1
2N−1项)时间内算出结果来了!
传统暴力求
G
(
x
)
G(x)
G(x)需要
O
(
N
2
)
O(N^2)
O(N2)时间,但是FFT/NTT可以做到在
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)求出来。我们上面看到的表示叫做系数表示法,其实多项式还可以用点表示:
{
F
a
(
x
)
=
[
(
x
0
,
F
a
(
x
0
)
)
,
(
x
1
,
F
a
(
x
1
)
)
,
.
.
.
,
(
x
n
,
F
a
(
x
N
)
)
]
F
b
(
x
)
=
[
(
x
0
,
F
b
(
x
0
)
)
,
(
x
1
,
F
b
(
x
1
)
)
,
.
.
.
,
(
x
n
,
F
b
(
x
N
)
)
]
得到这些点后,我们可以在
O
(
N
)
O(N)
O(N)时间内得到
G
(
x
)
G(x)
G(x)的点表示(乘起来就好啦):
G
(
x
)
=
[
(
x
0
,
G
(
x
0
)
)
,
(
x
0
,
G
(
x
0
)
)
,
.
.
.
,
(
x
n
,
G
(
x
N
)
)
]
=
[
(
x
0
,
F
a
(
x
0
)
F
b
(
x
0
)
)
,
(
x
1
,
F
a
(
x
1
)
F
b
(
x
1
)
)
,
.
.
.
,
(
x
n
,
F
a
(
x
N
)
F
b
(
x
N
)
)
]
问题是,光是求
F
a
(
x
)
F_a(x)
Fa(x)的点表示就需要
O
(
N
2
)
O(N^2)
O(N2)的时间!(因为总共有
N
N
N个
x
x
x,对于每个
x
x
x我们要用
N
N
N次加法来求
F
a
(
x
)
F_a(x)
Fa(x))。
假设
n
n
n是一个奇数(如果不够可以补零),
F
a
(
x
)
F_a(x)
Fa(x)可以变成这样子
F
a
(
x
)
=
a
0
+
a
1
∗
x
+
a
2
∗
x
2
+
.
.
.
+
a
n
∗
x
n
=
(
a
0
+
a
2
∗
x
2
+
.
.
.
+
a
n
−
1
∗
x
n
−
1
)
+
(
a
1
∗
x
+
a
3
∗
x
3
+
.
.
.
+
a
n
∗
x
n
)
=
(
a
0
+
a
2
∗
x
2
+
.
.
.
+
a
n
−
1
∗
x
n
−
1
)
+
x
(
a
1
+
a
3
∗
x
2
+
.
.
.
+
a
n
∗
x
n
−
1
)
=
U
(
x
)
+
x
V
(
x
)
拆解出来的奇偶项其实就有一点分制的意味了。如果我们可以找到两个特殊的输入
x
α
x_{\alpha}
xα、
x
β
x_{\beta}
xβ使得
{
F
a
(
x
α
)
=
U
(
x
′
)
−
x
V
(
x
′
)
F
a
(
x
β
)
=
U
(
x
′
)
+
x
V
(
x
′
)
怎么找到特殊的 x α x_{\alpha} xα、 x β x_{\beta} xβ和 k ′ k' k′呢?这里就涉及到各种复杂但巧妙的数学了。同时,我们还需要把点表示变回成多项式表示,这个可以用相似的步骤在 O ( N l o g N ) O(NlogN) O(NlogN)时间做到。
尽管FFT/NTT是 O ( N l o g N ) O(NlogN) O(NlogN)的,但它的常数有时可以很大以至于TLE。这次学到了一些技巧:
把递归版本写成自底向上的非递归,常规优化了
这个很多博客都有写,常规优化了
inverse的时候(求IFFT或者INTT),需要计算N的逆,这个缓存下来似乎不能省太多时间
NTT有一个特点就是除了第一个根,第二到最后一个根的顺序,在正向NTT和逆向NTT的时候恰好是相反的。可以只求一个。
观察一下 n = 2 2 n=2^2 n=22的根数组和 n = 2 3 n=2^3 n=23的根数组,后者其实是前者插入一些值得到的。我们可以维护并复用一个递增的根数组来省时间。
对于一个 N N N长的数组,我们可以只用 N / 2 N/2 N/2个根来完成正/反向NTT。但反向的NTT要求我们从Maxsize开始取根而不是从最小开始(我也暂时想不懂里面的数学T_T)。
当两个输入多项式相同的时候,第二个数组可以跳过正向的NTT
当 n n n很小(150?)的时候, O ( N 2 ) O(N^2) O(N2)的暴力不比高常数系数的 O ( N l o g N ) O(NlogN) O(NlogN)差
假设两个输入的多项式长度为 n n n、 m m m的时候,我们可以把结果裁剪到 n + m − 1 n+m-1 n+m−1长度。好处是如果有多个多项式要相乘,这样子做可以减少一些NTT时候的长度。
对于每个
n
n
n,可以把要交换的下标缓存下来,每次调用就可以了。注意如果用一个unordered_map来缓存数组的话,一定要用一个引用来访问vector里的东西,要不然indexing看起来
O
(
1
)
O(1)
O(1)的操作其实常数也是很高的。
官方题解挺全面的,就是要自己慢慢读:https://www.codechef.com/submit/EXPDIF?tab=solution
(如果有人看帖子的话)想要参考我的模板的话可以留言。