0%

FFT & NTT

导言

FFT/NTT在acm竞赛中常被用于处理多项式乘法优化问题,并常常与生成函数等数学内容一同考察,笔者在此整理了一些自己对FFT的理解,希望对读者有所帮助

多项式

表达

对于一个在实数域上阶多项式 ,要唯一确定这样一个多项式,我们可以使用两种方法,一是如表达式一样使用序列 记录的系数来唯一确定这一矩阵,记为系数表示法,二可利用插值多项式唯一性的性质通过构造 个不同点 来唯一确定这一多项式,记为点值表示法。

乘法运算

在这里我们重点关注n阶多项式的乘法,考虑两个阶多项式 和 ,求乘积。直接采用系数表示法暴力相乘可得 记 ,有 整体时间复杂度

如果使用点值表示法,则可以在已经知道个和对应的情况下,只需要就可求出目标多项式的相关点值,从而唯一确定,此时在处值为

复数的引入

点值的选取

根据上文,点值表示法的优越在于知道点值的情况下可大幅提高运算效率,可如果暴力求点值,每个点 需要次操作,进而使整体复杂度退化为 ,故使用点值表示法加速多项式乘法的关键在于选取一组特殊点,使得我们能够快速求取多项式在这组点上的对应点值

欧拉公式和复数的单位根

picture 如图在复平面上,以原点为圆心,半径为 构造单位圆,用 条过原点直线讲圆等分并交出 个点 。由欧拉公式 可知,对 有

单位根的特殊性质

引入复平面的原因就在于单位根 的一些特殊性质可提高计算效率,这里直接给出,读者往下便可体会到这些性质的作用

性质一:

性质二:

性质三:

FFT求点值

对 阶多项式 ,按下标奇偶分为 和 ,显然有 ,将 、 分别代入。 前者得 由性质一可化简得 记为。 后者代入得 由性质二化简得 记为​。

由 和 可知求解 的问题最终可以层层递归至求 故可以用 的复杂度求出 和 ,最终可以用的复杂度求出所有

蝴蝶变换

考虑到每次拆分函数都会使用大量空间和时间,因此考虑优化此过程 对于系数序列 ,不足则高位补 ,手动操作FFT拆分函数的过程不难发现,对于系数,记其经过全部拆分后的位置为 始终有 ,其中为的二进制表示字串。因此我们可直接算出最终系数使时间复杂度真正成为 对于二进制长度为的数,尝试求其二进制反转后的数 ,显然0反转后仍为0,1反转后为,对于,有 ,即可递推求出全部

傅里叶逆变换(IDFT)

现在我们得到了 的点值表示,需要将点值表示转化为系数表示。

使用矩阵表示FFT的过程,得到

不妨记为,要得到,只需要在等式左右两行左乘

在实际进行矩阵运算之前,不妨回到的一些性质,由性质三可知, 考察公式 因式分解得 当 时,有 ,即 更进一步,有 由此构造矩阵 可得 即

故对傅里叶变换后的多项式在做一次选取根为 的傅里叶变换即可获得原多项式

FFT模板

NTT

由于FFT中存在复数和大量的三角函数运算且不支持取模,在整数域内取点的NTT应运而生,FFT的加速运用了复数单位根的性质,同样,NTT选取的根也应当具有类似性质。

原根

前置知识:欧拉函数,欧拉定理 对一对互质正整数 ,且 设满足 的最小n为a模p的阶,记为。 对所有 ,所有 结果不同, 证明:不妨设存在 使 则有 ,与阶定义矛盾

对正整数 ,若 模 的阶 且 ,则称 为 的一个原根,注意 为 欧拉函数 对正整数,整数为其原根的充要条件为 且 为的一组剩余化简系,由阶的性质和原根定义可直接证明

对质数 ,存在原根 设 有 ,

对 存在如下性质:

性质一: 性质二: 性质三: 其中性质一可直接由原根性质得出,下面给出性质二和三的证明 性质二: 对 ,由 定义可得 又 同时,根据原根定义有 故得 性质三:

将原根和单位根对比,不难发现原根具有与单位根相似性质,因此原本通过代入 的FFT就可以通过换成代入 变成NTT,最后逆变换的过程则可以视为代入 在下的逆元

关于原根的求取,一般采用直接枚举。

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <bits/stdc++.h>
typedef long long ll;
const int N = 4000000 + 100, g = 3, invg = 332748118, mod = 998244353;
//998244353的一个原根为3且998244353-1=2^23*119,3在模998244353意义下的逆元为332748118
using namespace std;
int n, m, r[N]; //rev[i]为i的二进制翻转
long long a[N], b[N];
ll qpow(ll a, ll k) //快速幂,a为底数,k为指数
{
ll res = 1;
while (k)
{
if (k & 1)
res = res * a % mod;
a = a * a % mod;
k >>= 1;
}
return res;
}
void NTT(ll *a,int type,int len)
{
for(int i = 0;i < len;++i)
if(i < r[i])
swap(a[i],a[r[i]]);
for(int mid = 1;mid < len;mid <<= 1)
{
ll wn = 1;
if(type == 1)
wn = qpow(g,(mod - 1) / (mid << 1));
else
wn = qpow(invg,(mod - 1) / (mid << 1));
for(int i = 0;i < len;i += (mid << 1))
{
for(int j = 0,w0 = 1;j < mid;++j,w0 = 1ll * w0 * wn % mod)
{
ll x = a[i + j], y = w0 * a[i + j + mid] % mod;
a[i + j] = (x + y) % mod;
a[i + j + mid] = (x - y + mod) % mod;
}
}
}
}
int main()
{
string s1,s2;
cin >> s1 >> s2;
// cout << s1 << endl << s2 << endl;
n = s1.length() - 1;
m = s2.length() - 1;
for(int i = 0;i <= n;++i)
a[i] = s1[n - i] - '0';
for(int i = 0;i <= m;++i)
b[i] = s2[m - i] - '0';
int len = 1 << max((int)ceil(log2(n + m)), 1);
for(int i = 0;i < len;++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (max((int)ceil(log2(n + m)), 1) - 1));
NTT(a,1,len),NTT(b,1,len);
for(int i = 0;i < len;++i)
a[i] = a[i] * b[i] % mod;
NTT(a,-1,len);
ll inv = qpow(len,mod-2);
for (int i = 0; i <= len; i++)
a[i] = a[i] * inv % mod;
for (int i=0; i < len; i++)
{
if (a[i]>=10)
{
a[i+1] += a[i] / 10;
a[i] %= 10;
}
}
while(!a[len] && len) len--;
while(a[len] >= 10)
{
a[len + 1] += a[len] / 10;
a[len++] %= 10;
}
for(int i = len;i >= 0;--i)
cout << a[i];
return 0;
}

练习

P1919 【模板】A*B Problem 升级版(FFT 快速傅里叶变换)

P3803 【模板】多项式乘法(FFT)

2020中国大学生程序设计竞赛(CCPC) - 网络选拔赛 M - Residual Polynomial

CF1824B2