施工中 owo。
(omega) 何为「多项式」(omega)
多项式(polynomial)是指由变量(variable)、系数(coefficient)以及它们之间的加、减、乘、幂运算(非负整数次方)得到的表达式。多项式就是整式。
(omega) 基本概念 (omega)
在初中阶段就有所接触:整式可以包含加、减、乘、除、乘方五种对变量的运算,同时要求除数不含有变量。设 (x,y) 为变量,其余字母为常量,则 (xy,a+by) 是整式,(sqrt{a}x+pi y~(age0)) 也是整式,因为根号下的 (a) 是常量;(frac{x}y,sqrt{xy}) 则不是整式。
在本文中,我们所说的多项式通常指一元多项式,即仅含一个变量 (x) 的多项式。并简记一个多项式 (A(x)=a_0+a_1x+a_2x^2+cdots+a_nx^n),其中 (n) 是 (A(x)) 的次数。
(omega) 系数表示法 & 点值表示法 (omega)
在上文中 (A(x)=a_0+a_1x+a_2x^2+cdots+a_nx^n) 就是多项式的系数表示法。而可以证明,对于一个 (n) 次多项式,可以用任意 (n+1) 个其函数图像上不重合的点来唯一确定这个多项式。我们任取 (n+1) 个横坐标的值 (x_0,x_1,dots,x_n),代入 (A(x)),求得 (n+1) 个点 ((x_0,y_0),(x_1,y_1),dots,(x_n,y_n)),就可以用这 (n+1) 个点唯一确定 (A(x)),这也被称为 (A(x)) 的点值表示法。注意到 (x) 是任意的,所以同一个多项式的点值表示法不唯一。
接下来,抛出我们的问题:设有两个多项式 (A(x),B(x)),如何计算它们的乘积(亦称作卷积) (C(x)=A(x)cdot B(x)) 呢?
一个简单的想法,我们可以利用乘法分配律求到 (C(x)) 每一项的系数。于是:
注意求和中的 (+infty) 只是一种形式记法,事实上,当 (i) 大于 (A(x)) 的次数与 (B(x)) 的次数之和时,(x^i) 的系数 (c_i) 显然为 (0)。
不过呢……从算法设计的角度,这种求多项式乘法的方式的复杂度是 (mathcal{O}(n^2)) 的,好慢 qwq。
联系到点值表示法,如果对于同一个横坐标 (x),其在 (A(x)) 上的坐标为 ((x,y_a)),在 (B(x)) 上的坐标为 ((x,y_b)),那么代入最初的表达式,它在 (C(x)=A(x)cdot B(x)) 上的坐标就是 ((x,y_ay_b)) 呐。所以,在已知 (A(x)) 和 (B(x)) 的点值表示法时,我们可以用 (mathcal{O}(n)) 的时间求出 (C(x)) 的点值表示法!
(omega) 傅里叶(Fourier)变换 (omega)
我们继续解决上文多项式乘法的问题——找到一个高效的算法,实现系数与点值表示法的转换。
本节中,若非特别说明,(n=2^k,kinmathbb{N})。
(omega) 概述 (omega)
离散傅里叶变换(Discrete Fourier Transform,DFT),是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。
FFT 是一种高效实现 DFT 的算法,称为快速傅立叶变换(Fast Fourier Transform,FFT)。它对傅里叶变换的理论并没有新的发现,但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步。
(omega) 前置知识 - 复数 (omega)
其实很简单:我们把形如 (z=a+bi) 的数称为复数,其中 (a,binmathbb R); (a) 为实部,(b) 为虚部,(i) 有 (i^2=-1),为虚数单位。我们可以把复数与二维平面上的向量 ((a,b)) 一一对应,此时 (x) 轴称为实轴,(y) 轴称为虚轴。并记其与 (x) 轴正方向的夹角为辐角 ( heta),向量的模长为 (|z|)。如图,对于 (z=3+2i):
其中 (l) 为模长,( heta) 为辐角。接下来我们研究复数的一些基本运算:
-
加减法:(z_1pm z_2=(a_1pm a_2)+(b_1pm b_2)i)。
-
乘法:(z_1z_2=(a_1a_2-b_1b_2)+(a_1b_2+a_2b_1)i)。
进一步研究,发现:
[|z_1z_2|^2=(a_1^2a_2^2-2a_1a_2b_1b_2+b_1^2b_2^2)+(a_1^2b_2^2+2a_1a_2b_1b_2+a_2^2b_1^2)=(a_1^2+b_1^2)(a_2^2+b_2^2) ]最后一步用了简单的因式分解技巧。由于 (a_1^2+b_1^2=|z_1|^2,a_2^2+b_2^2=|z_2|^2),所以有 (|z_1z_2|=|z_1||z_2|)。
此外,运用 ( an) 的和角公式,还可以求到 ( heta_{z_1z_2}= heta_{z_1}+ heta_{z_2})。
所以,复数的乘法有一个重要的几何意义:模长相乘,辐角相加。
最后,我们定义 (overline{z}) 为 (z) 关于 (x) 轴对称的向量,称为 (z) 的共轭复数。即若 (z=a+bi),有 (overline{z}=a-bi)。可以发现 (zcdotoverline{z}=a^2+b^2inmathbb R)。 为后文方便叙述,记 (operatorname{conj}(z)=overline z)。
(omega) 单位根 (omega)
定义 (n) 次单位根为所有满足 (z^n=1) 的 (z)。根据乘法的几何意义,可知 (|z|=1),(n heta_z=2kpi),其中 (kinmathbb{Z})。于是满足条件且小于 (2pi) 的 ( heta_z) 集合为 ({0,frac{1 imes2pi}{n},frac{2 imes2pi}{n},dots,frac{(n-1) imes2pi}{n}}),共 (n) 个。我们记 (omega_n^k) ((omega),/'əʊmɪɡə/)为第 (k) 个 (n) 次单位根((k=0,1,dots,n-1))。如图,当 (n=8) 时:
运用简单的三角函数知识,得到:
可以发现,单位根实际上体现了一个周期意义:(omega_n^n=omega_n^0)。所以此后单位根的上标(乘方或是编号,它们在运算意义上等价)默认对单位根次数取模。单位根有如下性质:
- (omega_n^jomega_n^k=omega_n^{j+k})。
- (omega_{kn}^{kj}=omega_n^j)。
- (omega_{2n}^k=-omega_{2n}^{k+n})。
可以结合乘法几何意义与公式理解。
(omega) 快速傅里叶正变换(FFT)(omega)
有必要重申我们的目标:实现系数与点值表示法的转换。
现在,设有 (A(x)=a_0+a_1x+a_2x^2+cdots+a_{n-1}x^{n-1}),我们希望代入 (n) 次单位根 (omega_n^0,omega_n^1,dots,omega_n^{n-1}) 这 (n) 个复数来取得 (A(x)) 的点值表示法。
分开奇数项与偶数项,记:
利用 (A_e(x)) 和 (A_o(x)) 来表示 (A(x)),有:
设 (n=2m),尝试代入一个 (omega_n^k):
注意到 (omega_n^{2k}=omega_{2m}^{2k}=omega_m^k),所以:
类似地,代入 (omega_n^{k+m}=-omega_n^k),有:
所以,当已知 (A_e(omega_m^k)) 和 (A_o(omega_m^k)) 时,可以 (mathcal{O}(1)) 计算出 (A(omega_n^k)) 和 (A(omega_n^{k+m}))。现在,就有了一个算法雏形:
Function name: FFT.
Input: a polynomial (A(x)=a_0+a_1x_1+cdots) which's length is (n~(n=2^k,kinmathbb N)).
Output: a set (mathcal A={A(omega_n^0),A(omega_n^1),dots,A(omega_n^{n-1})}).
if (n=1) then
return ({(a_0,0)})
(mleftarrowfrac{n}2)
(A_e(x)leftarrowsum_{iin[0,n)land2|i}a_ix^{frac{i}2})
(A_o(x)leftarrowsum_{iin[0,n)land2 ot|i}a_ix^{frac{i-1}2})
(mathcal Aleftarrowvarnothing)
(mathcal{A}_eleftarrowoperatorname{FFT}(A_e,m))
(mathcal{A}_oleftarrowoperatorname{FFT}(A_o,m))
(kleftarrow0)
while (k<m) do
(mathcal{A}_kleftarrow{mathcal{A}_e}_k+omega_n^k{mathcal{A}_o}_k)
(mathcal{A}_{k+m}leftarrow{mathcal{A}_e}_k-omega_n^k{mathcal{A}_o}_k)
(kleftarrow k+1)
return (mathcal A)
复杂度是 (mathcal{T}(n)=2mathcal{T}(frac{n}2)+mathcal{O}(n)=mathcal{O}(nlog n))。在后文中,“多项式 (A(x)) 的点值表示”均指由上述 FFT 算法得到的 (mathcal A)。
别着急做题,这种实现方法 SPFA 了 qwq。
(omega) 快速傅里叶逆变换(IFFT)(omega)
利用 FFT,我们可以快速地将系数表示法转换成点值表示法,那怎么在同样优秀的复杂度内转回去呢?
从本质入手,考虑到 (mathcal A_j=sum_{k=0}^{n-1}a_k(omega_n^j)^k),我们把它写成矩阵乘法的形式:
令左侧的系数矩阵为 (U),考虑系数矩阵 (V):
那么:
当 (i=j) 时,显然 ((Ucdot V)_{ij}=n)。
当 (i ot=j) 时,发现是等比数列的形式。所以 ((Ucdot V)_{ij}=frac{1-omega_n^{n(j-i)}}{1-omega_n^{j-i}})。又因为 (omega_n^n=omega_n^0=1),所以分子有 (1-1=0),则原式 (=0)。
综上,((Ucdot V)_{ij}=[i=j]n),即 (frac{1}nV=U^{-1})。
以此,就有:
注意到 (omega_n^{-k}=omega_n^{n-k}),所以依然能够用 FFT 的递归形式求解。
(omega) 迭代实现 (omega)
常数惊人呐……
这里,我们将尝试把递归的 FFT Fast-Fast TLE 转换为仅用循环迭代的 FFT。
观察递归时各系数的位置:
观察第一层和最后一层:
原序列 dec | (0) | (1) | (2) | (3) | (4) | (5) | (6) | (7) |
---|---|---|---|---|---|---|---|---|
原序列 bin | (000) | (001) | (010) | (011) | (100) | (101) | (110) | (111) |
新序列 dec | (0) | (4) | (2) | (6) | (1) | (5) | (3) | (7) |
新序列 bin | (000) | (100) | (010) | (110) | (001) | (101) | (011) | (111) |
发现原序列下标的二进制翻转过来就是新序列下标。
所以我们可以先安排好新序列下标,再从下往上迭代就可以了。
(omega) 例题 (omega)
(omega)「洛谷 P3803」「模板」多项式乘法(FFT)(omega)
(omega) 题意简述 (omega)
link.
给两个多项式的每项系数,求其相乘得到的多项式的每项系数。
(omega) 数据规模 (omega)
多项式长度 (n,mle10^6)。
(omega) ( ext{Solution}) (omega)
直接给出完整代码,建议先模仿实现,以免常数惊人 owo。
不过我不知道怎么折叠显示代码,影响阅读很抱歉 qwq。
(omega) ( ext{Code}) (omega)
#include <cmath>
#include <cstdio>
#include <iostream>
inline int rint () {
int x = 0, f = 1; char s = getchar ();
for ( ; s < '0' || '9' < s; s = getchar () ) f = s == '-' ? -f : f;
for ( ; '0' <= s && s <= '9'; s = getchar () ) x = x * 10 + ( s ^ '0' );
return x * f;
}
template<typename Tp>
inline void wint ( Tp x ) {
if ( x < 0 ) putchar ( '-' ), x = -x;
if ( 9 < x ) wint ( x / 10 );
putchar ( x % 10 ^ '0' );
}
const int MAXL = 1 << 21;
const double PI = acos ( -1 );
int n, m, rev[MAXL + 5];
struct Complex { // 复数结构体。
double x, y;
Complex () {} Complex ( const double tx, const double ty ): x ( tx ), y ( ty ) {}
inline Complex operator + ( const Complex t ) const { return Complex ( x + t.x, y + t.y ); }
inline Complex operator - ( const Complex t ) const { return Complex ( x - t.x, y - t.y ); }
inline Complex operator * ( const Complex t ) const { return Complex ( x * t.x - y * t.y, x * t.y + y * t.x ); }
inline Complex operator / ( const double t ) const { return Complex ( x / t, y / t ); }
} A[MAXL + 5], B[MAXL + 5];
inline void FFT ( const int n, Complex* A, const int tp ) {
// n为多项式长度,保证是2的整数幂;A为每项系数;tp=1表示正变换,tp=-1表示逆变换。
for ( int i = 0; i < n; ++ i ) if ( i < rev[i] ) std :: swap ( A[i], A[rev[i]] ); // 安排系数位置。
for ( int i = 2, stp = 1; i <= n; i <<= 1, stp <<= 1 ) {
Complex w ( cos ( PI / stp ), tp * sin ( PI / stp ) ); // 当前正在处理$omega_i$。
for ( int j = 0; j < n; j += i ) {
Complex r ( 1, 0 );
for ( int k = 0; k < stp; ++ k, r = r * w ) {
// $r=omega_i^k$。
Complex ev ( A[j + k] ), ov ( r * A[j + k + stp] );
A[j + k] = ev + ov, A[j + k + stp] = ev - ov;
}
}
}
if ( ! ~ tp ) for ( int i = 0; i < n; ++ i ) A[i] = A[i] / n; // 逆变换,最后每项/n。
}
inline void ployConv ( const int n, const int m, const Complex* A, const Complex* B, Complex* C ) {
// 计算A(x)与B(x)的卷积,答案为C(x)。
int lg = 0, len = 1;
static Complex tmpA[MAXL + 5], tmpB[MAXL + 5];
for ( ; len < n + m - 1; len <<= 1, ++ lg ); // C(x)长度为n+m-1,找到适合的长度len。
for ( int i = 0; i < len; ++ i ) {
tmpA[i] = A[i], tmpB[i] = B[i];
rev[i] = ( rev[i >> 1] >> 1 ) | ( ( i & 1 ) << lg >> 1 ); // rev[i]为i翻转后的位置,自行理解。
}
FFT ( len, tmpA, 1 ), FFT ( len, tmpB, 1 ); // 正变换求到点值表示。
for ( int i = 0; i < len; ++ i ) C[i] = tmpA[i] * tmpB[i]; // 求到C(x)的点值表示。
FFT ( len, C, -1 ); // 逆变换求到C(x)。
}
int main () {
n = rint () + 1, m = rint () + 1; // 注意这里要+1。
for ( int i = 0; i < n; ++ i ) A[i].x = rint ();
for ( int i = 0; i < m; ++ i ) B[i].x = rint ();
ployConv ( n, m, A, B, A );
for ( int i = 0; i < n + m - 1; ++ i ) {
wint ( ( int ) round ( A[i].x ) );
putchar ( i == n + m - 2 ? '
' : ' ' );
}
return 0;
}
(omega) 快速数论变换(NTT)(omega)
事实上,由于 FFT 存在大量浮点数运算,其精度和常数都不尽人意。那么能否在整数域下找到单位根的替代品呢?
我们来总结一下单位根应具有的性质:
- (omega_n^jomega_n^k=omega_n^{j+k}),注意这里的上标就意义上来说不是乘方,而是编号(当然运算上就是乘方 www)。
- (omega_{kn}^{kj}=omega_n^j),我们需要这个性质进行递归处理。
- (omega_{2n}^k=-omega_{2n}^{k+n}),这一性质体现在了合并计算的步骤。
- ((forall j ot=k)(omega_n^j ot=omega_n^k)),它保证了点值表示法的坐标不重合。
- (omega_n^0=1),它保证了逆变换顺利进行。
考虑在模意义下……
(omega) 原根 (omega)
OI-Wiki 讲得好高深 qwq……
对于素数 (p),定义其原根 (g) 为满足 ((forall j,kin[0,p-1),j ot=k)left(g^j otequiv g^kpmod p ight)) 的数。模仿 (omega) 的定义,我们令 (alpha_n^k=(g^frac{p-1}n)^k)(这里 (alpha) 是随便取的名字 www),考虑其是否满足上述几条性质:
- (alpha_n^jalpha_n^kequivalpha_n^{jk}pmod p),乘方性质,显然。
- (alpha_{kn}^{kj}=(g^frac{p-1}{kn})^{kj}=(g^frac{p-1}n)^j=alpha_n^j),成立。
- (-alpha_{2n}^{k+n}=-g^frac{(p-1)(k+n)}{2n}=(-g^frac{p-1}2)g^frac{(p-1)k}{2n}equiv g^frac{(p-1)k}{2n}=alpha_{2n}^kpmod p)。注意因为 ((g^frac{p-1}{2})^2equiv1pmod p) 且根据 (g) 的定义, (g^frac{p-1}2=alpha_{2n}^n ot=alpha_{2n}^0landalpha_{2n}^0equiv1pmod p),所以 (g^frac{p-1}2equiv-1pmod p),等式成立。
- 由定义,成立。
- 显然 (g ot=0),成立。
于是,把 FFT 中的所有 (omega_n^k) 替换为 (alpha_n^k) 就变成 NTT 啦!记得取模 owo。
(omega) 实现 (omega)
inline void NTT ( const int n, int* A, const int tp ) {
for ( int i = 0; i < n; ++ i ) if ( i < rev[i] ) A[i] ^= A[rev[i]] ^= A[i] ^= A[rev[i]];
for ( int i = 2, stp = 1, w; i <= n; i <<= 1, stp <<= 1 ) {
w = qkpow ( G, ( MOD - 1 ) / i );
// G为原根,qkpow(a,b)为a的b次方模MOD的结果。
if ( ! ~ tp ) w = qkpow ( w, MOD - 2 );
for ( int j = 0; j < n; j += i ) {
for ( int r = 1, k = j; k < j + stp; ++ k, r = 1ll * r * w % MOD ) {
int ev = A[k], ov = 1ll * r * A[k + stp] % MOD;
A[k] = ( ev + ov ) % MOD, A[k + stp] = ( ev - ov + MOD ) % MOD;
}
}
}
if ( ! ~ tp ) {
int invn = qkpow ( n, MOD - 2 ); // invn为n的逆元。
for ( int i = 0; i < n; ++ i ) A[i] = 1ll * A[i] * invn % MOD;
}
}
(omega) NTT 模数 (omega)
注意到,(n) 的大小是随多项式长度变化的,而我们必须保证 (n|(p-1)),所以 (p-1) 所含 (2) 的因子个数决定了 NTT 能够接受的最大多项式长度。对于素数 (p) 的选取自然也有了考究。
记住一点,(998244353-1=2^{23} imes119),适合做 NTT 模数,原根为 (3);(10^9+7-1=2 imes500000003),不适合做 NTT 模数。
更多 NTT 模数可见这篇博客。
到此,多项式最最基础的知识就结束啦。
(omega) 奇怪的模数 - 任意模数 NTT (omega)
你 NTT 对模数有要求对吧?出题人就笑了……/xyx
好吧,在不接受 FFT 精度误差的情况下,我们来想想办法应对模数不是 NTT 模数,甚至不是素数的毒瘤题吧!
(omega) 三模 NTT (omega)
我偏要用 NTT 模数 www。
顾名思义,我们任取三个 NTT 模数 (p_1,p_2,p_3),分别求出多项式乘积的第 (k) 项模 (p_1,p_2,p_3) 的结果,然后用中国剩余定理(CRT)解出该项模题目给定模数 (M) 的结果就行了。不熟悉 CRT 的读者可以参考这篇博客。
我个人认为不太优美懒,因此没有具体实现。在抱歉之余推荐这篇博客的模板 owo。
(omega) 拆系数 FFT(MTT)(omega)
我偏要用 FFT。
缩写释义:MTT - MaoXiao Theory(?) Transformation。快速毛论变换(雾。
假设多项式 (A(x)=a_0+a_1x+a_2x^2+cdots,B(x)=b_0+b_1x+b_2x^2+cdots),长度均为 (n),给定的模数 (m),我们要求它们的卷积 (R(x)=A(x)cdot B(x))。如果直接 FFT,最大的系数可达到 (nm^2) 的级别。我们假定 (nle10^5,mle10^9),那么这样的精度误差是不被接受的。我们取 (M=lceilsqrt m ceil),并定义以下几个多项式:
可以发现,以上每个多项式的系数都是不超过 (M) 的,那么任意两个多项式卷积的最大系数不超过 (nM^2le10^{14}),在 double
的承受范围以内。(保险起见,建议开 long double
,不过对于某些 g++
版本貌似并没有作用 owo。)我们用这四个多项式表示 (R(x)):
这样,在 FFT 过程中,系数大小能被接受;作系数拼凑时步步取模亦能保证不溢出,我们就用 FFT 解决了任意模数 NTT 的问题。一般来说,取 (M=32768=2^{15}),可以用位运算节省常数;还应利用 (omega_n^k) 的通项预处理出所有单位根以保障精度。
(omega) 七次转五次 (omega)
数一数,我们对 (C(x),D(x),E(x),F(x)) 分别进行了一次 FFT,对 (D(x)F(x),) (C(x)F(x)+D(x)E(x),) (C(x)E(x)) 分别进行了一次 IFFT,一共有七次变换操作,常数有点点大 qwq。
接下来,我们假设有实系数多项式 (A(x)=a_0+a_1x+a_2x^2+cdots,B(x)=b_0+b_1x+b_2x^2+cdots),长度均为 (n~(n=2^k,kinmathbb N)),我们希望分别求到它们的点值表示 (mathcal A) 和 (mathcal B)。于是定义:
其中 (i) 是虚数单位。设 (P) 与 (Q) 的点值表示分别为 (mathcal P,mathcal Q),并记 (omega_n^k) 的辐角 (frac{2pi k}n) 为 ( heta_n^k)。那么:
推导时,时刻留意 ( heta_n) 的上标在模 (n) 意义下嗷!
最终,发现 (mathcal Q) 可以在求得 (mathcal P) 的前提下 (mathcal O(1)) 求到。只需要用 (mathcal P) 和 (mathcal Q) 表示出 (mathcal A) 和 (mathcal B):
就可以用一次 FFT 求出两个多项式的点值表示啦!(我认为毛啸的论文在 (mathcal B_k) 的表达式处有误,右侧分子应为本文的 (mathcal Q_k-mathcal P_k) 而非 (mathcal P_k-mathcal Q_k),欢迎指正 qwq。)
于是,我们可以把对 (C(x),D(x),E(x),F(x)) 的 FFT 合并为两组,就只需要五次 FFT 啦!
(omega) 五次转四次 (omega)
正变换已经足够优秀了,我们接下来考虑逆变换的合并。按照上文的推导,我们只需要求到 (P(x)) 或者 (Q(x)) 就可以了——因为原多项式是实系数的,所以对于 (P(x)) 或 (Q(x)) 的每一项,我们能够区分出其实部就是 (A(x)) 的系数而虚部就是 (B(x)) 的系数。既然已知 (mathcal A) 和 (mathcal B),直接利用定义式,有:
逆变换 (mathcal P) 得到 (P(x)),就可以把两次逆变换合并为一次了。最终,我们把四次正变换合并为两次,三次逆变换合并为两次,仅用四次 FFT 就解决了这一问题。
(omega) 例题 (omega)
(omega) 「洛谷 P4245」「模板」任意模数 NTT
(omega) 题意简述 (omega)
link.
给两个多项式,求其在模 (p) 意义下的卷积。
(omega) 数据规模 (omega)
多项式长度 (nle10^5),系数 (a_k,b_kle10^9),(ple10^9+9)。
(omega) ( ext{Solution}) (omega)
这里仅给出四次 FFT 的 MTT 做法。
(omega) ( ext{Code}) (omega)
#include <cmath>
#include <cstdio>
#include <iostream>
typedef long long LL;
inline int rint () { /* 快读 */ }
inline void wint ( const int x ) { /* 快输 */ }
const int MAXN = 1 << 18;
const double PI = acos ( -1 );
int n, m, p, A[MAXN + 5], B[MAXN + 5], rev[MAXN + 5];
struct Complex {
/* 复数结构体 */
} omega[MAXN + 5], P[MAXN + 5], Q[MAXN + 5], C[MAXN + 5], D[MAXN + 5], E[MAXN + 5], F[MAXN + 5];
inline void FFT ( const int n, Complex* A, const int tp ) { /* FFT,应使用预处理好的单位根omega[]。 */ }
int main () {
n = rint () + 1, m = rint () + 1, p = rint ();
for ( int i = 0; i < n; ++ i ) A[i] = rint () % p, P[i] = Complex ( A[i] & 0x7fff, A[i] >> 15 );
for ( int i = 0; i < m; ++ i ) B[i] = rint () % p, Q[i] = Complex ( B[i] & 0x7fff, B[i] >> 15 );
int lg = 0, len = 1;
for ( ; len < n + m - 1; len <<= 1, ++ lg );
for ( int i = 0; i < len; ++ i ) rev[i] = ( rev[i >> 1] >> 1 ) | ( ( i & 1 ) << lg >> 1 );
for ( int i = 1; i < len; i <<= 1 ) { // 预处理单位根。
for ( int k = 0; k < i; ++ k ) {
omega[len / i * k] = Complex ( cos ( PI * k / i ), sin ( PI * k / i ) );
}
}
FFT ( len, P, 1 ), FFT ( len, Q, 1 );
for ( int i = 0; i < len; ++ i ) {
Complex t ( P[( len - i ) % len].x, -P[( len - i ) % len].y );
C[i] = ( P[i] + t ) / 2, D[i] = Complex ( 0, 1 ) * ( t - P[i] ) / 2;
}
for ( int i = 0; i < len; ++ i ) {
Complex t ( Q[( len - i ) % len].x, -Q[( len - i ) % len].y );
E[i] = ( Q[i] + t ) / 2, F[i] = Complex ( 0, 1 ) * ( t - Q[i] ) / 2;
}
for ( int i = 0; i < len; ++ i ) {
Complex c ( C[i] ), d ( D[i] ), e ( E[i] ), f ( F[i] );
C[i] = c * e, D[i] = c * f + d * e, E[i] = d * f;
P[i] = C[i] + Complex ( 0, 1 ) * E[i];
}
FFT ( len, D, -1 ), FFT ( len, P, -1 );
for ( int i = 0; i < n + m - 1; ++ i ) {
int c = ( LL ( P[i].x + 0.5 ) % p + p ) % p, d = ( LL ( D[i].x + 0.5 ) % p + p ) % p, e = ( LL ( P[i].y + 0.5 ) % p + p ) % p;
wint ( ( c + ( 1ll << 15 ) % p * d % p + ( 1ll << 30 ) % p * e % p ) % p );
putchar ( i + 1 == n + m - 1 ? '
' : ' ' );
}
return 0;
}
(omega) 「CF 623E」Transforming Sequence (omega)
(omega) 题意简述 (omega)
link.
求有多少个长度为 (n) 的序列 ({a_n}),满足 (a_kin(0,2^k)land a_k otsubseteqigcup_{j=1}^{k-1}a_j)。这里的集合运算把 (a_k) 作为了二进制集合。对 (10^9+7) 取模。
(omega) 数据规模 (omega)
(nle10^{18},kin[1,3 imes10^4])。
(omega) ( ext{Solution}) (omega)
详见我的题解。题意描述略有不同,请自行理解。为什么我的 MTT 有八次变换啊……
专门提起这道题的目的,除了让大家实战运用 MTT 之外,还想让大家感受倍增这种几乎能融入所有 OI 知识点的思想。在下文的多项式运算中,也广泛应用了倍增思想。
(omega)「多项式运算」全家桶 (omega)
(omega) 多项式乘法逆((operatorname{inv}))(omega)
(omega) 多项式平方根((operatorname{sqrt}))(omega)
(omega) 多项式自然对数((ln))(omega)
(omega) 多项式 (e) 的指数((operatorname{exp}))(omega)
(omega) 参考资料 (omega)
按在文中第一次借鉴或引用的位置顺序排列: