FFT
问题引入
[!question] 示例问题
给定一个 \(n\) 次多项式 \(F(x)\),和一个 \(m\) 次多项式 \(G(x)\)。
请求出 \(F(x)\) 和 \(G(x)\) 的乘积。
这里如果我们暴力去做就是 \(\mathcal O(n m)\) 的,但是貌似这里已经没有更好的办法了。
但是,如果题目提供的是 "两个多项式上相同 \(x\) 下的多个点" (这里被称作 "点表示法") 那么对于在两个多项式上的两个点 \((x, F(x)), (x, G(x))\) ,他们在乘积多项式上的点就直接是 \((x, F(x)\cdot G(x))\) ,只需要 \(\mathcal O(n+m+1)\) 就可以求解。
于是现在的问题就变成了需要把原来的多项式转化为 "点表示法" 然后合并之后快速切换回去。
Step 1: 考虑如何转化过去,我们发现此时就算我们随机找 \(n + m + 1\) 个点也需要 \(\mathcal O\left((n+m+1)^2\right)\) ,但是由于选择任意不同点都是一样的,所以可以考虑选择一些特殊点进行计算。
Step 2: 考虑如何转化回去,首先有一个结论,对于一个 \(n\) 次多项式,需要至少 \(n+1\) 个点就能确定它,既然我们通过 Step 1 已经求出这 \(n+1\) 个点了,那么一定可以做出来。而最直观的办法就是使用高斯消元,但是时间复杂度 \(\mathcal O(n^3)\) 。
所以现在两个步骤都是不能直接使用前面的办法解决的,这时候就需要使用 \(\texttt{FFT}\) 。
复数
关于 Step 1 我们不是需要找到 \(n+1\) 个特殊点吗,所以这里我们可以使用性质良好的 复数 作为特殊点。
当然对于这个多项式,他当然支持 复数, 并且 Step 2 的那个结论仍然存在。
复数运算
加法 \((a + b i) + (c+di) = (a+c) + (b+d)i\) ,在几何意义中就是和向量加法相同。
乘法 \((a+bi)\times (c+di) = (ac - bd) + (ad + bc)i\) ,在几何意义中就是复平面上角度相加,长度相乘。
单位根
我们在复平面上画一个单位圆,然后均分这个园到 \(n\) 份,然后每两份之间的交界点就是一个单位根,使用 \(w_n^k\) ,图示如下:

此时有以下性质:
-
\(w_n^k = \left( cos(\frac{k \cdot 2\pi}{n}), sin(\frac{k \cdot 2\pi}{n}) \right)\)
-
\(w_n^k = w_{2n}^{2k}\)
-
\(w_n^k = w_n^{k+n}\)
-
\(w_n^{k + n / 2} = -w_n^{k}\)
-
\(w_n^{a+b} = w_n^{a} * w_n^{b}\)
FFT
现在终于到真正的 FFT 了,这里我们令一个多项式 \(f(x) = \sum_{i=0}^{n-1} a_i x^i\) 。
我们考虑求出对于 \(f(w_n),k \in [0, n)\) ,那么这里我们考虑分成两个子问题:
-
\(f_1(x) = a_0 + a_2 x + a_4 x^2 + \dots + a_{n-2} x^{\frac{n}2 -1}\) 。
-
\(f_2(x) = a_1 + a_3 x + a_4 x^2 + \dots + a_{n-1} x^{\frac{n}{2}-1}\)
那么不难发现: \(f(x) = f_1(x^2) + x * f_2(x^2)\) 。
那么把特殊点 \(w_n\) 带入:
此时分为两种情况:
-
\(0\le k < \frac{n}{2}\) : \(f(w_n^k) = f_1(w_n^{2k}) + w_n^k \times f_2(w_n^{2k}) = f_1(w_{n/2}^k) + w_n^k \times f_2(w_{n/2}^{k})\)
-
\(\frac{n}{2} \le \frac{n}{2} + k < n\) : \(f(w_n^{k+\frac{n}{2}}) = f_1(w_n^{2k+n}) + w_n^{k+\frac{n}{2}} \times f_2(w_n^{2k+n}) = f_1(w_{n/2}^k) - w_n^k \times f_2(w_{n/2}^{k})\)
所以我们发现这两个式子居然只有中间的符号不一样, 于是现在两个问题被分成了来嗯个规模相同的子问题。只需要递归下去。
然后当问题规模为 \(1\) 的时候直接结束,总共时间复杂度 \(\mathcal O(n \log n)\) 。
IFFT
这里我们已经得到了 \(c = (c_0, c_1, c_2, /dots, c_{n-1})\) ,然后我们考虑对于 \(f(x) = \sum_{i=1}^{n-1} c_i x^i\) 求解他的 FFT : \(d = (d_0, d_1, d_2, /dots, d_{n-1})\) 。
所以可以发现:
所以只有 \(j = k\) 时,所以 \(d_i = n \cdot a_i\) , 那么 \(a_i = \frac{d_i}n\) 。
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 | |
NTT
首先我们发现 FFT 有一个巨大的问题,其不能取膜,如果题目就是要求你取膜应当如何是好。
所以这里我们引入几个概念:
价: 在模 \(p\) 下,如果 \(a^x \equiv 1 \mod p\), 那么称 \(x\) 为 \(a\) 在 \(p\) 下的价,记作 \(|a|\) 。
原根 \(g\) : 对于 \(|g| = \varphi(p)\) , 那么称 \(g\) 为 \(p\) 下的原根。对于 \(p\) 为质数的情况,其一定有原根,其原根满足 \(g^{p-1} \equiv 1 \mod p\) 。
然后我们现在实际上就是要选择一个单位根,满足前面的那些性质,这里我们选择单位根为 \(g^{\frac{p-1}{n}}\) ,那么此时 \(w_n^k = \left(g^{\frac{p-1}{n}}\right)^k\) 。
此时我们验证以下各个性质: 验证个球 。
然后代码:
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 | |
多项式其他运算
多项式求逆元
这里我们另 \(F(x)\) 是原多项式, \(G(x)\) 为新的多项式,直接推式子:
后面就不写同余了。
因此我们直接上牛顿迭代法:
(这里 \(G_0\) 表示牛顿迭代法上一个,在 \(\bmod~ {x^{\lceil \frac{n}{2} \rceil}}\) 下,这是因为在多项式中每牛顿迭代法一次精度翻倍)
那么:
然后递归求解就可以了,代码:
1 2 3 4 5 6 7 8 9 10 11 12 | |
多项式对数
\(F, G\) 和上面相同,同样退式子:
然后直接使用 求导, 积分, 求逆元 就可以了。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
多项式指数
同样直接推式子:
然后直接使用牛顿迭代法:
真好,前面写的 \(\ln\) 有可以用上了阿
代码:
1 2 3 4 5 6 7 8 9 10 11 12 | |
公用模板
[!success]- 参考代码
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115struct Poly { #define norm(x) (((x)%mod+mod)%mod) typedef long long ll; static constexpr int mod = 998244353, G = 3; static constexpr int N = 5e5+5; vector<int> a; inline void resize(int n) { a.resize(n); } inline int operator [] (int x) const { return a[x]; } inline int& operator [] (int x) { return a[x]; } inline int size() { return a.size(); } inline int size() const { return a.size(); } inline void clean() { a.clear(); } inline void clean(int m) { a.clear(), a.resize(m); } static int rev[N]; static inline void init(int n) { int bit = 1; while((1<<bit) < n) bit++; for(int i=1; i<n; i++) rev[i] = (rev[i>>1]>>1) | ((i&1) << (bit-1)); } static int ksm(int x, int y) { int ans = 1; while(y) { if(y&1) ans = 1LL* ans*x%mod; x = 1LL* x*x%mod, y>>=1; } return ans; } static void NTT(int n, Poly& v, bool invert) { v.resize(n); for(int i=0; i<n; i++) if(i < rev[i]) swap(v[i], v[rev[i]]); static int invG = ksm(G, mod-2); for(int len=1; len <= (n>>1); len<<=1) { int W = ksm(invert ? invG : G, (mod-1) / (len<<1)); for(int i=0; i<n; i += (len<<1)) { int w = 1; for(int j=0; j<len; j++, w = 1LL* w*W%mod) { int a0 = v[i+j], a1 = 1LL* w*v[i+j+len]%mod; v[i+j] = (a0+a1)%mod, v[i+j+len] = norm(a0-a1); } } } if(invert) { int invN = ksm(n, mod-2); // 这里不能加 static for(int i=0; i<n; i++) v[i] = 1LL* v[i]*invN %mod; } } static void mul(const Poly& va, const Poly& vb, Poly& res) { static Poly a, b; a = va, b = vb; int t = 1; while(t <= a.size() + b.size() - 1) t<<=1; init(t), res.clean(), res.resize(t); NTT(t, a, 0), NTT(t, b, 0); for(int i=0; i<t; i++) res[i] = 1LL* a[i] * b[i] % mod; NTT(t, res, 1), res.resize(a.size() + b.size() - 1); } // 多项式逆元 static void inv(int n, const Poly& a, Poly& res) { if(n == 1) return res.clean(1), res[0] = ksm(a[0], mod-2), void(); inv((n+1)>>1, a, res), res.resize(n); static Poly b; b.clean(); int t=1; while(t <= (n<<1)) t<<=1; init(t), b=a, b.resize(n); NTT(t, b, 0), NTT(t, res, 0); for(int i=0; i<t; i++) res[i] = 1LL* norm(2 - 1LL* b[i]*res[i]) * res[i]%mod; NTT(t, res, 1), res.resize(n); } // 多项式求积分 static void Int(const Poly& a, Poly& res) { res.resize(a.size()+1); for(int i=1; i<=a.size(); i++) res[i] = 1LL* a[i-1] * ksm(i, mod-2) %mod; res[0] = 0; } // 多项式求导 static void Der(const Poly& a, Poly& res) { res.resize(a.size()-1); for(int i=1; i<a.size(); i++) res[i-1] = 1LL* i*a[i] %mod; } // 多项式 ln static void Ln(const Poly& x, Poly& res) { static Poly a, b; a.clean(), b.clean(); Der(x, a), inv(x.size(), x, b); mul(a, b, a), Int(a, res); } // 多项式 exp static void Exp(int n, const Poly& x, Poly& res) { if(n == 1) return res.clean(1), res[0]=1, void(); Exp((n+1)>>1, x, res), res.resize(n); static Poly lng, f; Ln(res, lng); int t=1; while(t <= (n<<1)) t<<=1; init(t), f=x, f.resize(n); NTT(t, f, 0), NTT(t, lng, 0), NTT(t, res, 0); for(int i=0; i<t; i++) res[i] = 1LL* norm(1 - lng[i] + f[i]) * res[i] %mod; NTT(t, res, 1), res.resize(n); } }; int Poly::rev[Poly::N];