导言
FFT/NTT在acm竞赛中常被用于处理多项式乘法优化问题,并常常与生成函数等数学内容一同考察,笔者在此整理了一些自己对FFT的理解,希望对读者有所帮助
多项式
表达
对于一个在实数域上阶多项式
,要唯一确定这样一个多项式,我们可以使用两种方法,一是如表达式一样使用序列
记录的系数来唯一确定这一矩阵,记为系数表示法,二可利用插值多项式唯一性的性质通过构造
个不同点
来唯一确定这一多项式,记为点值表示法。
乘法运算
在这里我们重点关注n阶多项式的乘法,考虑两个阶多项式 和 ,求乘积。直接采用系数表示法暴力相乘可得 记
,有 整体时间复杂度
如果使用点值表示法,则可以在已经知道个和对应的情况下,只需要就可求出目标多项式的相关点值,从而唯一确定,此时在处值为
复数的引入
点值的选取
根据上文,点值表示法的优越在于知道点值的情况下可大幅提高运算效率,可如果暴力求点值,每个点
需要次操作,进而使整体复杂度退化为
,故使用点值表示法加速多项式乘法的关键在于选取一组特殊点,使得我们能够快速求取多项式在这组点上的对应点值
欧拉公式和复数的单位根
如图在复平面上,以原点为圆心,半径为 构造单位圆,用 条过原点直线讲圆等分并交出 个点 。由欧拉公式 可知,对 有
单位根的特殊性质
引入复平面的原因就在于单位根
的一些特殊性质可提高计算效率,这里直接给出,读者往下便可体会到这些性质的作用
性质一:
性质二:
性质三:
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;
using namespace std; int n, m, r[N]; long long a[N], b[N]; ll qpow(ll a, ll 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; 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