题解P6828 任意模数 Chirp-Z Transform

Ruiqun2009

2022-11-26 15:19:29

Solution

本文分两部分:Chirp-Z Transform 和 MTT。 ## Chirp-Z Transform ### 简介 Chirp-Z 变换(Chirp-Z Transform),又称 Bluestein 算法,是任意长度的卷积。 这个算法可以在 $O(n\log n)$ 的时间复杂度内求解等比数列点值。 更形象化地: 给定三个整数 $n,m,c$,以及一个多项式 $A(x)$。 这个算法可以在 $\Theta((n+m)\log (n+m))$ 的时间复杂度内求出 $$ A(1),A(c),A(c^2),\cdots,A(c^{m-1}) $$ 当然,是在模意义下的。 ### 实现 我们有几种实现方式。讲讲最主流的方式: 先想想组合数。根据组合数的定义 $\binom{n}{m}=\frac{n!}{m!(n-m)!}(n\geq m\geq 0)$,我们可以得到 $$ \binom{n}{2}=\dfrac{n!}{2!(n-2)!}=\dfrac{n(n-1)}{2} $$ **这里即便除以零也没事,因为如同分式方程,没有必要在中途检验。** 首先是这个式子 $$ \begin{aligned} xy&=\dfrac{x^2+y^2+2xy-x^2-y^2}{2}\\ &=\dfrac{x^2+y^2+2xy-x-y-x^2-y^2+x+y}{2}\\ &=\dfrac{x^2+y^2+2xy-x-y}{2}-\dfrac{x^2-x}{2}-\dfrac{y^2-y}{2}\\ &=\binom{x+y}{2}-\binom{x}{2}-\binom{y}{2} \end{aligned} $$ 然后我们将要求的东西代入函数里 $$ \begin{aligned} A(c^{k})&=\sum_{i=0}^{\deg A}c^{ik}A\lbrack i\rbrack\\ &=\sum_{i=0}^{\deg A}c^{\binom{i+k}{2}-\binom{i}{2}-\binom{k}{2}}A\lbrack i\rbrack\\ &=c^{-\binom{k}{2}}\sum_{i=0}^{\deg A}c^{\binom{i+k}{2}-\binom{i}{2}}A\lbrack i\rbrack \end{aligned} $$ 这已经明摆着的是一个卷积的形式了。 我们只需要预处理出 $c$ 的幂,然后做卷积,最后乘上 $c^{-\binom{k}{2}}$ 就可以了。 --- 但是显然这**甚至没有直接计算快**。我们试着加速。 设 $$ \begin{aligned} P(x)&=\sum_{i=0}^{\deg A}A\lbrack \deg A-i\rbrack c^{-\binom{\deg A-i}{2}}x^i\\ Q(x)&=\sum_{i=0}^{\deg A}c^{\binom{i}{2}}x^i \end{aligned} $$ 于是我们有 $$ \begin{aligned} P(x)\lbrack i\rbrack&=A\lbrack \deg A-i\rbrack c^{-\binom{\deg A-i}{2}}\\ Q(x)\lbrack i\rbrack&=c^{\binom{i}{2}} \end{aligned} $$ 再推一波式子。 $$ \begin{aligned} (P(x)Q(x))\lbrack\deg A+k\rbrack&=\sum_{i=0}^{\deg A+k}(P(x)\lbrack \deg A+k-i\rbrack\times Q(x)\lbrack i\rbrack)\\ &=\sum_{i=0}^{\deg A+k}(A\lbrack{\color{red}i-k}\rbrack\times c^{-\binom{\color{red}i-k}{2}}\times c^\binom{i}{2}) \end{aligned} $$ 注意到此时 $i-k$ 可能小于零。于是我们设 $\forall i\not\in\lbrack0,\deg A\rbrack,A\lbrack i\rbrack=0$。 **在这一步中计算组合数发生除以零是没事的,因为如同分式方程,没有必要在中途检验。** $$ \begin{aligned} (P(x)Q(x))\lbrack\deg A+k\rbrack&=\sum_{i=0}^{\deg A+k}(A\lbrack i-k\rbrack\times c^{-\binom{i-k}{2}}\times c^\binom{i}{2})\\ &=\sum_{i=-k}^{\deg A}(A\lbrack i\rbrack\times c^{\binom{i+k}{2}-\binom{i}{2}})\\ &=\sum_{i=0}^{\deg A}(A\lbrack i\rbrack\times c^{\binom{i+k}{2}-\binom{i}{2}})\\ &=c^{\binom{k}{2}}\times A(c^k) \end{aligned} $$ 于是我们对 $P(x)$ 和 $Q(x)$ 做卷积,可以得到 $c^{\binom{0}{2}}\times A(c^0),c^{\binom{1}{2}}\times A(c^1),\cdots,c^{\binom{m-1}{2}}\times A(c^{m-1})$。 **注意到 $\binom{0}{2}$ 和 $\binom{1}{2}$ 是不合法的,所以我们只能套公式。** 由于得到的结果是 $m$ 项的,所以我们要做的卷积长度为 $\deg A+m$(精确来说应该是 $2^{\lceil\log(\deg A+m)\rceil}$)。 ### 代码 ```cpp inline void ChirpZ(size_t n, mint c, size_t m, vector<mint> a, vector<mint>& b) { static vector<mint> pwc, ipwc, f, g; pwc.resize(n + m + 1); ipwc.resize(n + m + 1); size_t i = 1, iend = std::max(n, m); pwc[1] = c; ipwc[1] = c.inv(); for (pwc[0] = i = 1; i <= n + m; i++) pwc[i] = pwc[i - 1] * pwc[1]; for (pwc[0] = i = 1; i <= n + m; i++) pwc[i] = pwc[i] * pwc[i - 1]; for (ipwc[0] = i = 1; i <= iend; i++) ipwc[i] = ipwc[i - 1] * ipwc[1]; for (ipwc[0] = i = 1; i <= iend; i++) ipwc[i] = ipwc[i] * ipwc[i - 1]; for (i = 1; i < n; i++) a[i] *= ipwc[i - 1]; size_t len = 1; int x = -1; while (len < n + m) len <<= 1, x++; get_rev(len, x); f.resize(len); g.resize(len); f[0] = 1; std::copy_n(pwc.begin(), n + m, f.begin() + 1); for (i = 1; i <= n; i++) g[i] = a[n - i]; mul(n + m, n + m, f, g, f, len); b.resize(m); b[0] = f[n]; for (i = 1; i < m; i++) b[i] = f[i + n] * ipwc[i - 1]; } ``` ## MTT ### 预处理单位根 还是一个比较简单的提高精度的科技。 通过预处理单位根算出 $\omega_{len}^i$,而不是每次都乘,这样会掉精度。 同时能减小常数,避免重复的运算。 ```cpp inline void get_rev(size_t len, int x) { if (len == rev.size()) return; rev.resize(len); for (size_t i = 0; i < len; i++) rev[i] = rev[i >> 1ull] >> 1ull | (i & 1ull) << x; omegas.resize(len); for (size_t i = 0; i < len; i++) omegas[i] = cp(std::cos(two * pi / len * i), std::sin(two * pi / len * i)); } inline void FFT(vector<cp>& a, size_t n, bool type) { for (size_t i = 1ull; i < n; ++i) if (i < rev[i]) std::swap(a[i], a[rev[i]]); for (size_t i = 2ull; i <= n; i <<= 1ull) for (size_t j = 0ull, l = (i >> 1ull), ch = n / i; j < n; j += i) for (size_t k = j, now = 0ull; k < j + l; k++) { cp x = a[k], y = a[k + l] * (type ? omegas[now] : omegas[now].conj()); a[k] = x + y; a[k + l] = x - y; now += ch; } if (!type) { for (size_t i = 0; i < n; i++) { a[i].real() /= n; a[i].imag() /= n; } } } ``` ### 第一轮合并 我们发现,在 $7$ 次 FFT 的拆系数 FFT 中,复数中我们只用了实部,并没有使用虚部。我们尝试使用。 我们设 $$ \begin{aligned} P_0(x)&=A_0(x)+\mathrm{i}A_1(x)\\ P_1(x)&=A_0(x)-\mathrm{i}A_1(x)\\ Q(x)&=B_0(x)+\mathrm{i}B_1(x) \end{aligned} $$ 然后我们将他们乘起来,得到 $$ \begin{aligned} P_0(x)Q(x)&=(A_0(x)+\mathrm{i}A_1(x))(B_0(x)+\mathrm{i}B_1(x))\\ &=A_0(x)B_0(x)-A_1(x)B_1(x)+\mathrm{i}(A_0(x)B_1(x)+A_1(x)B_0(x))\\ P_1(x)Q(x)&=(A_0(x)-\mathrm{i}A_1(x))(B_0(x)+\mathrm{i}B_1(x))\\ &=A_0(x)B_0(x)+A_1(x)B_1(x)+\mathrm{i}(A_0(x)B_1(x)-A_1(x)B_0(x))\\ \end{aligned} $$ 于是 $$ \begin{aligned} P_0(x)Q(x)+P_1(x)Q(x)&=2(A_0(x)B_0(x)+\mathrm{i}A_0(x)B_1(x))\\ P_1(x)Q(x)-P_0(x)Q(x)&=2(A_1(x)B_1(x)-\mathrm{i}A_1(x)B_0(x)) \end{aligned} $$ 我们可以在上述式子中找出所有我们所需要的项。 总计 $5$ 次 FFT。 代码: ```cpp inline void mul(size_t n, size_t m, const vector<mint>& a, const vector<mint>& b, vector<mint>& c, size_t len) { const size_t up = sqrt(mint::mod()); mint v1, v2, v3; static size_t i; vector<cp> P(len, cp(0, 0)), Q(len, cp(0, 0)), R(len, cp(0, 0)); for (i = 0ull; i < n; i++) { P[i] = cp(a[i].val() % up, a[i].val() / up); Q[i] = P[i].conj(); } for (i = 0ull; i < m; i++) R[i] = cp(b[i].val() % up, b[i].val() / up); FFT(P, len, true); FFT(Q, len, true); FFT(R, len, true); for (i = 0ull; i < len; i++) { P[i] = P[i] * R[i]; Q[i] = Q[i] * R[i]; } FFT(P, len, false); FFT(Q, len, false); c.resize(n + m - 1); for (i = 0ull; i < n + m - 1; i++) { v1 = llround((P[i].real() + Q[i].real()) / two); v2 = llround((Q[i].real() - P[i].real()) / two); v3 = llround(P[i].imag()); c[i] = v1 + v2 * up * up + v3 * up; } } ``` 实测[不能通过](https://www.luogu.com.cn/record/93881015)。 ### 第二轮合并 本质上就是对 $P_0(x)$ 和 $P_1(x)$ 的合并。 #### 第一种合并方式 这是 OIer 最常写的拆系数 FFT 版本。 通过上边提到的特性计算出了 $\mathcal{F}(F_0(x))\lbrack i\rbrack$ 和 $\mathcal{F}(F_1(x))\lbrack i\rbrack$。但是这种方法不同就在于,它直接求出了 $A_0(x),A_1(x)$。然后将 $A_0(x)B_0(x)+\mathrm{i}A_0(x)B_1(x)$ 和 $A_1(x)B_0(x)+\mathrm{i}A_1(x)B_1(x)$ 算出。 然后按照定义合并。 #### 第二种合并方式 设 FFT 长度为 $n$。根据 FFT 的定义 $\mathcal{F}(F(x))\lbrack i\rbrack=F(\omega_{n}^i)=\sum_{j=0}^{n-1}\omega_{n}^{ij}F\lbrack j\rbrack$,我们能得到 $$ \begin{aligned} \mathcal{F}(P_0(x))\lbrack i\rbrack&=P_0(\omega_{n}^i)\\ &=A_0(\omega_{n}^i)+\mathrm{i}A_1(\omega_{n}^i)\\ &=\sum_{j=0}^{n-1}\omega_{n}^{ij}A_0\lbrack j\rbrack +\mathrm{i} \sum_{j=0}^{n-1}\omega_{n}^{ij}A_1\lbrack j\rbrack\\ &=\sum_{j=0}^{n-1}\omega_{n}^{ij}(A_0\lbrack j\rbrack +\mathrm{i} A_1\lbrack j\rbrack)\\ \mathcal{F}(P_1(x))\lbrack n-i\rbrack&=P_1(\omega_{n}^{-i})\\ &=A_0(\omega_{n}^{-i})-\mathrm{i}A_1(\omega_{n}^{-i})\\ &=\sum_{j=0}^{n-1}\omega_{n}^{-ij}A_0\lbrack j\rbrack -\mathrm{i} \sum_{j=0}^{n-1}\omega_{n}^{-ij}A_1\lbrack j\rbrack\\ &=\sum_{j=0}^{n-1}\omega_{n}^{-ij}(A_0\lbrack j\rbrack-\mathrm{i}A_1\lbrack j\rbrack)\\ \end{aligned} $$ 考虑单位根的几何意义 $\omega_{n}^i=\cos\dfrac{2\pi i}{n}+\mathrm{i}\sin\dfrac{2\pi i}{n}$。 代入上式,得到 $$ \begin{aligned} \mathcal{F}(P_0(x))\lbrack i\rbrack &=\sum_{j=0}^{n-1}\omega_{n}^{ij}(A_0\lbrack j\rbrack +\mathrm{i} A_1\lbrack j\rbrack)\\ &=\sum_{j=0}^{n-1}(\cos\dfrac{2\pi ij}{n}+\mathrm{i}\sin\dfrac{2\pi ij}{n})(A_0\lbrack j\rbrack +\mathrm{i} A_1\lbrack j\rbrack)\\ &=\sum_{j=0}^{n-1}(\cos\dfrac{2\pi ij}{n}\times A_0\lbrack j\rbrack-\sin\dfrac{2\pi ij}{n}\times A_1\lbrack j\rbrack+\mathrm{i}(\sin\dfrac{2\pi ij}{n}\times A_0\lbrack j\rbrack+\cos\dfrac{2\pi ij}{n}\times A_1\lbrack j\rbrack))\\ \mathcal{F}(P_1(x))\lbrack n-i\rbrack&=\sum_{j=0}^{n-1}\omega_{n}^{-ij}(A_0\lbrack j\rbrack-\mathrm{i}A_1\lbrack j\rbrack)\\ &=\sum_{j=0}^{n-1}(\cos\dfrac{-2\pi ij}{n}+\mathrm{i}\sin\dfrac{-2\pi ij}{n})(A_0\lbrack j\rbrack-\mathrm{i}A_1\lbrack j\rbrack)\\ &=\sum_{j=0}^{n-1}(\cos\dfrac{2\pi ij}{n}-\mathrm{i}\sin\dfrac{2\pi ij}{n})(A_0\lbrack j\rbrack-\mathrm{i}A_1\lbrack j\rbrack)\\ &=\sum_{j=0}^{n-1}(\cos\dfrac{2\pi ij}{n}\times A_0\lbrack j\rbrack-\sin\dfrac{2\pi ij}{n}\times A_1\lbrack j\rbrack-\mathrm{i}(\sin\dfrac{2\pi ij}{n}\times A_0\lbrack j\rbrack+\cos\dfrac{2\pi ij}{n}\times A_1\lbrack j\rbrack)\\ \end{aligned} $$ 然后我们发现 $\mathcal{F}(P_0(x))\lbrack i\rbrack$ 和 $\mathcal{F}(P_1(x))\lbrack n-i\rbrack$ 是共轭的。(这个我在 [这篇文章](https://www.luogu.com.cn/blog/_post/458869) 里提到过) 于是我们对 $P_0$ 做 FFT 之后,可以根据定义得到 $P_1$ FFT 后的结果。 总计 $4$ 次 FFT。 代码: ##### 第一种 $4$ 次 FFT 的拆系数 FFT ```cpp inline void mul(int n, int m, const int& p, vector<int> a, vector<int> b, vector<int>& c) { static cp da, db, dc, dd, rel(0.5, 0), img(0, -0.5); static const int up = 1 << 15; static int v1, v2, v3; int len = 1, x = -1; while (len < n + m) len <<= 1, x++; a.resize(len); b.resize(len); get_rev(len, x); for (int i = 0; i < len; i++) { a[i] = i < n ? (a[i] % p + p) % p : 0; b[i] = i < m ? (b[i] % p + p) % p : 0; } vector<cp> P(len, cp(0, 0)), Q(len, cp(0, 0)), R(len, cp(0, 0)); for (int i = 0; i < len; i++) { P[i] = cp(a[i] >> 15, a[i] & 32767); Q[i] = cp(b[i] >> 15, b[i] & 32767); } FFT(P, len, 1); FFT(Q, len, 1); for (int i = 0; i < len; i++) { R[i] = P[(len - i) & (len - 1)].conj(); } for (int i = 0; i < len; i++) { auto x = P[i]; auto y = R[i]; P[i] = (x + y) * rel; R[i] = (x - y) * img; } for (int i = 0; i < len; i++) { P[i] = P[i] * Q[i]; Q[i] = R[i] * Q[i]; } FFT(P, len, -1); FFT(Q, len, -1); c.clear(); c.resize(len); for (int i = 0; i < len; i++) { v1 = (int)(P[i].real() + 0.5); v2 = (int)(P[i].imag() + 0.5) + (int)(Q[i].real() + 0.5); v3 = (int)(Q[i].imag() + 0.5); c[i] = 1ll * v1 * up % p * up % p + 1ll * v2 * up % p + v3 % p; c[i] = (c[i] % p + p) % p; } } ``` ##### 第二种 $4$ 次 FFT 的拆系数 FFT ```cpp inline void mul(size_t n, size_t m, const vector<mint>& a, const vector<mint>& b, vector<mint>& c, size_t len) { const size_t up = sqrt(mint::mod()); mint v1, v2, v3; vector<cp> P(len, cp(0, 0)), Q(len, cp(0, 0)), R(len, cp(0, 0)); for (size_t i = 0ull; i < n; i++) P[i] = cp(a[i].val() % up, a[i].val() / up); for (size_t i = 0ull; i < m; i++) R[i] = cp(b[i].val() % up, b[i].val() / up); FFT(P, len, true); FFT(R, len, true); Q[0] = P[0].conj(); for (size_t i = 1ull; i < len; i++) Q[i] = P[len - i].conj(); for (size_t i = 0ull; i < len; i++) { P[i] = P[i] * R[i]; Q[i] = Q[i] * R[i]; } FFT(P, len, false); FFT(Q, len, false); c.resize(n + m - 1); for (size_t i = 0ull; i < n + m - 1; i++) { v1 = llround((P[i].real() + Q[i].real()) / two); v2 = llround((Q[i].real() - P[i].real()) / two); v3 = llround(P[i].imag()); c[i] = v1 + v2 * up * up + v3 * up; } } ```