FFT(快速傅里叶变换)与NTT(快速数论变换)详解(11000+请耐心阅读)

参考自tly大佬与mys大佬的博客:友链:

orz tly大佬

orz mys大佬(他的博客似乎在某个我不知道的地方QAQ…其实看到他的博客是找他要的pdf版本)

也有一部分来自讲课时的课件。

Part.1 前置芝士

多项式的表示方法

多项式的表示方法分为两种,一种是系数表达法,另一种是点值表达法。

系数表达法

就是常见的解析式了,一般的多项式表示成 f ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ a n x n = ∑ i = 0 N a i x i f(x) = a_0+a_1x+a_2x^2+\cdots a_nx^n=\sum_{i=0}^{N}a_ix^i f(x)=a0+a1x+a2x2+anxn=i=0Naixi

点值表达法

对于一个多项式 f ( x ) = ∑ i = 0 N a i x i f(x)=\sum_{i=0}^{N}a_ix^i f(x)=i=0Naixi,我们选取 n + 1 n+1 n+1个不同的 x i x_i xi带进去,得到了 n + 1 n+1 n+1 y i y_i yi,这样用 n + 1 n+1 n+1个数对 ( x i , y i ) (x_i,y_i) (xi,yi)就可以表示一个多项式了。

不难发现一个点值表示唯一对应一个多项式,但一个多项式就能够对应多个点值表示了。

点值表示的性质

对于两个多项式 f ( x ) , g ( x ) f(x),g(x) f(x),g(x),我们把 { x 1 , x 2 , x 3 , … , x n } \{x_1,x_2,x_3,\ldots,x_n\} {x1,x2,x3,,xn}代进去,得到了 f ( x ) , g ( x ) f(x),g(x) f(x),g(x)的点值表示: { ( x 1 , f ( x 1 ) ) , ( x 2 , f ( x 2 ) ) , ( x 3 , f ( x 3 ) ) , … , ( x n , f ( x n ) ) } \{(x_1,f(x_1)),(x_2,f(x_2)),(x_3,f(x_3)),\ldots,(x_n,f(x_n))\} {(x1,f(x1)),(x2,f(x2)),(x3,f(x3)),,(xn,f(xn))} { ( x 1 , g ( x 1 ) ) , ( x 2 , g ( x 2 ) ) , ( x 3 , g ( x 3 ) ) , … , ( x n , g ( x n ) ) } \{(x_1,g(x_1)),(x_2,g(x_2)),(x_3,g(x_3)),\ldots,(x_n,g(x_n))\} {(x1,g(x1)),(x2,g(x2)),(x3,g(x3)),,(xn,g(xn))}

那么不难发现 f ( x ) ⋅ g ( x ) f(x)\cdot g(x) f(x)g(x)的点值表示其实就等于 { ( x 1 , f ( x 1 ) g ( x 1 ) ) , ( x 2 , f ( x 2 ) g ( x 2 ) ) , ( x 3 , f ( x 3 ) g ( x 3 ) ) , … , ( x n , f ( x n ) g ( x n ) ) } \{(x_1,f(x_1)g(x_1)),(x_2,f(x_2)g(x_2)),(x_3,f(x_3)g(x_3)),\ldots,(x_n,f(x_n)g(x_n))\} {(x1,f(x1)g(x1)),(x2,f(x2)g(x2)),(x3,f(x3)g(x3)),,(xn,f(xn)g(xn))}

这样一来,我们只要取足够多的 x i x_i xi,就可以按照如下方式将两个多项式乘起来:

  1. 选取足够多的值代入两个多项式 f ( x ) , g ( x ) f(x),g(x) f(x),g(x)中,得到它们的点值表示;
  2. 将两个多项式的点值表示对应项乘起来;
  3. 将得到的点值表示还原成系数表示。

但这样做看上去对原来 O ( n 2 ) O(n^2) O(n2)的多项式乘法似乎没什么优化,但没错,接下来,FFT将把这个过程优化到 O ( n log ⁡ n ) O(n\log n) O(nlogn)

Part.2 傅里叶变换

如何将算法优化到 O ( n log ⁡ n ) O(n\log n) O(nlogn)

这意味着我们需要选取一些特殊的值代进去。

这种值就叫单位复根。

补充:单位复根

单位复根的实质是复数。

复数

复数是形如 a + b i a+bi a+bi的一种数,其中 i = − 1 i=\sqrt{-1} i=1 。称此时的 a a a为实部, b b b为虚部。

它可以在二维坐标系中表示,它表示为一个向量。
FFT(快速傅里叶变换)与NTT(快速数论变换)详解(11000+请耐心阅读)_第1张图片

称此时它的长度为模长,与 x x x轴(实数轴)的夹角为幅角。

这样一来,复数的乘法就是模长相乘,幅角相加。

单位复根

使得 ω n = 1 \omega^n = 1 ωn=1的复数 ω \omega ω n n n次单位复根。

不难发现 n n n次单位复根恰好有 n n n个,它们是 e 2 π i k n , k = 0 , 2 , … , n − 1 e^\frac{2\pi ik}{n},k=0,2,\ldots,n-1 en2πik,k=0,2,,n1

复数的幂 e i θ = cos ⁡ θ + sin ⁡ θ i e^{i\theta}=\cos \theta+\sin\theta i eiθ=cosθ+sinθi

称此时的 ω n = e 2 π i n \omega_n=e^\frac{2\pi i}{n} ωn=en2πi为主 n n n次单位复根,其他的 n n n次单位复根都是主单位复根的幂。

这样一来,单位复根就有了一些性质:

  • ∣ ω n i ∣ = 1 |{\omega_n}^i|=1 ωni=1
  • ω n i {\omega_n}^i ωni的幅角为 2 π i n \frac{2\pi i}{n} n2πi
  • ω n n = ω n 0 {\omega_n}^n={\omega_n}^0 ωnn=ωn0,也就是说 n n n次单位复根最多有 n n n个,分别是 ω n 0 , ω n 1 , ω n 2 … , ω n n − 1 {\omega_n}^0,{\omega_n}^1,{\omega_n}^2\ldots,{\omega_n}^{n-1} ωn0,ωn1,ωn2,ωnn1

单位复根的数学性质

其实单位复根还有一些其他的数学性质:(这些性质都可以通过画图来证明)

相消引理

d > 0 d>0 d>0,则有 ω d n d k = ω n k {\omega_{dn}}^{dk}={\omega_{n}}^k ωdndk=ωnk

证明(推式子): ω d n d k = ( e 2 π i d n ) d k = ( e 2 π i n ) k = ω n k {\omega_{dn}}^{dk}=(e^\frac{2\pi i}{dn})^{dk}=(e^\frac{2\pi i}{n})^{k}={\omega_{n}}^k ωdndk=(edn2πi)dk=(en2πi)k=ωnk

它有一个推论: ω 2 n n = ω 2 = − 1 {\omega_{2n}}^{n}=\omega_2=-1 ω2nn=ω2=1

折半引理

ω 2 n k = − ω 2 n k + n {\omega_{2n}}^k=-{\omega_{2n}}^{k+n} ω2nk=ω2nk+n

证明就是将等式两边平方后发现它的值对应相等,而方向相反。

求和引理

对于不能被 n n n整除的数 k k k ∑ j = 0 n − 1 ( ω n k ) j = 0 \sum_{j=0}^{n-1}{({\omega_n}^k)^j}=0 j=0n1(ωnk)j=0

证明:不难发现这是一个等比数列,应用等比数列的求和公式可得: ∑ j = 0 n − 1 ( ω n k ) j = ( ω n k ) n − 1 ω n k − 1 = 1 k − 1 ω n k − 1 = 0 \sum_{j=0}^{n-1}{({\omega_n}^k)^j}=\frac{({\omega_n}^k)^n-1}{{\omega_n}^k-1}=\frac{1^k-1}{{\omega_n}^k-1}=0 j=0n1(ωnk)j=ωnk1(ωnk)n1=ωnk11k1=0

傅里叶正变换

我们选取 n n n次单位复根代入 n − 1 n-1 n1次多项式。

为了加快运算,我们将多项式的奇次项和偶次项分开来算,最后将它们组合在一起。

记多项式 f ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n − 1 x n − 1 f(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1} f(x)=a0+a1x+a2x2++an1xn1

将其分成两个多项式: f [ 0 ] ( x ) = a 0 + a 2 x + a 4 x 2 + ⋯   , f [ 1 ] ( x ) = a 1 + a 3 x + a 5 x 2 + ⋯ f^{[0]}(x)=a_0+a_2x+a_4x^2+\cdots,f^{[1]}(x)=a_1+a_3x+a_5x^2+\cdots f[0](x)=a0+a2x+a4x2+,f[1](x)=a1+a3x+a5x2+

则不难得出 f ( x ) = f [ 0 ] ( x 2 ) + x f [ 1 ] ( x 2 ) f(x)=f^{[0]}(x^2)+xf^{[1]}(x^2) f(x)=f[0](x2)+xf[1](x2)

我们取 n = 2 p n=2p n=2p,将 ω n k {\omega_n}^k ωnk ω n k + p {\omega_n}^{k+p} ωnk+p代入多项式得到:

f ( ω n k ) = f [ 0 ] ( ( ω n 2 k 2 ) 2 ) + ω n k f [ 1 ] ( ( ω n 2 k 2 ) 2 ) = f [ 0 ] ( ω p k ) + ω n k f [ 1 ] ( ω p k ) f({\omega_n}^k)=f^{[0]}(({\omega_\frac{n}{2}}^\frac{k}{2})^2)+{\omega_n}^kf^{[1]}(({\omega_\frac{n}{2}}^\frac{k}{2})^2)=f^{[0]}({\omega_p}^k)+{\omega_n}^kf^{[1]}({\omega_p}^k) f(ωnk)=f[0]((ω2n2k)2)+ωnkf[1]((ω2n2k)2)=f[0](ωpk)+ωnkf[1](ωpk)

f ( ω n k + p ) = f [ 0 ] ( ω p k 2 ) + ω n k + p f [ 1 ] ( ω p k + p ) = f [ 0 ] ( ω p k ) − ω n k f [ 1 ] ( ω p k ) f({\omega_n}^{k+p})=f^{[0]}({\omega_p}^\frac{k}{2})+{\omega_n}^{k+p}f^{[1]}({\omega_p}^{k+p})=f^{[0]}({\omega_p}^k)-{\omega_n}^kf^{[1]}({\omega_p}^k) f(ωnk+p)=f[0](ωp2k)+ωnk+pf[1](ωpk+p)=f[0](ωpk)ωnkf[1](ωpk)

就像上面两个式子一样,只要我们知道了 f [ 0 ] ( ω p k ) f^{[0]}({\omega_p}^k) f[0](ωpk) f [ 1 ] ( ω p k ) f^{[1]}({\omega_p}^k) f[1](ωpk),就可以 O ( 1 ) O(1) O(1)的算出 f ( ω n k ) f({\omega_n}^k) f(ωnk) f ( ω n k + p ) f({\omega_n}^{k+p}) f(ωnk+p)

因此,只要我们递归的求解 f [ 0 ] ( ω p k ) f^{[0]}({\omega_p}^k) f[0](ωpk) f [ 1 ] ( ω p k ) f^{[1]}({\omega_p}^k) f[1](ωpk),就可以在 O ( n ) O(n) O(n)的时间内算出 f ( x ) f(x) f(x)的点值表示。

时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

迭代实现傅里叶正变换

递归版本的FFT虽然将计算多项式点值表示的时间复杂度降到了 O ( n log ⁡ n ) O(n\log n) O(nlogn),但一个显然的问题是这个算法的常数太大了。

我们考虑将整个奇偶分组后各个系数数的位置:
FFT(快速傅里叶变换)与NTT(快速数论变换)详解(11000+请耐心阅读)_第2张图片

原来的序列:0,1,2,3,4,5,6,7。

变换后的序列:0,4,2,6,1,5,3,7。

将它们变成二进制:

原序列:000,001,010,011,101,110,111。

变换后的序列000,100,010,110,101,011,111。

这样我们就发现它们的值就是二进制位的反转。

我们将系数中该换位置的都换了,倒着做FFT就够了。

如何获得二进制位的反转呢?只要简洁就行了。具体还是参考代码吧。

多项式点值表示->系数表示:傅里叶逆变换

但我们获得了点值表示,如果我们要将它变成系数表示呢?

观察FFT的实质,我们进行了一次矩阵乘法:

[ ( ω n 0 ) 0 ( ω n 0 ) 1 ( ω n 0 ) 2 ⋯ ( ω n 0 ) n − 1 ( ω n 1 ) 0 ( ω n 1 ) 1 ( ω n 1 ) 2 ⋯ ( ω n 1 ) n − 1 ⋮ ⋮ ⋮ ⋱ ⋮ ( ω n n − 1 ) 0 ( ω n n − 1 ) 2 ( ω n n − 1 ) 3 ⋯ ( ω n n − 1 ) n − 1 ] × [ a 0 a 1 ⋮ a n ] = [ f ( ω n 0 ) f ( ω n 1 ) ⋮ f ( ω n n − 1 ) ] \begin{bmatrix}({\omega_n}^0)^0&({\omega_n}^0)^1&({\omega_n}^0)^2&\cdots&({\omega_n}^0)^{n-1}\\({\omega_n}^1)^0&({\omega_n}^1)^1&({\omega_n}^1)^2&\cdots&({\omega_n}^1)^{n-1}\\\vdots&\vdots&\vdots&\ddots&\vdots\\({\omega_n}^{n-1})^0&({\omega_n}^{n-1})^2&({\omega_n}^{n-1})^3&\cdots&({\omega_n}^{n-1})^{n-1}\end{bmatrix} \times \begin{bmatrix}a_0\\a_1\\\vdots\\a_n\end{bmatrix}=\begin{bmatrix}f({\omega_n}^0)\\f({\omega_n}^1)\\\vdots\\f({\omega_n}^{n-1})\end{bmatrix} (ωn0)0(ωn1)0(ωnn1)0(ωn0)1(ωn1)1(ωnn1)2(ωn0)2(ωn1)2(ωnn1)3(ωn0)n1(ωn1)n1(ωnn1)n1×a0a1an=f(ωn0)f(ωn1)f(ωnn1)

记上面的系数矩阵(第一个矩阵)为 V V V

对于另一个矩阵 D = [ ( ω n − 0 ) 0 ( ω n − 0 ) 1 ( ω n − 0 ) 2 ⋯ ( ω n − 0 ) n − 1 ( ω n − 1 ) 0 ( ω n − 1 ) 1 ( ω n − 1 ) 2 ⋯ ( ω n − 1 ) n − 1 ⋮ ⋮ ⋮ ⋱ ⋮ ( ω n − n + 1 ) 0 ( ω n − n + 1 ) 2 ( ω n − n + 1 ) 3 ⋯ ( ω n − n + 1 ) n − 1 ] D=\begin{bmatrix}({\omega_n}^{-0})^0&({\omega_n}^{-0})^1&({\omega_n}^{-0})^2&\cdots&({\omega_n}^{-0})^{n-1}\\({\omega_n}^{-1})^0&({\omega_n}^{-1})^1&({\omega_n}^{-1})^2&\cdots&({\omega_n}^{-1})^{n-1}\\\vdots&\vdots&\vdots&\ddots&\vdots\\({\omega_n}^{-n+1})^0&({\omega_n}^{-n+1})^2&({\omega_n}^{-n+1})^3&\cdots&({\omega_n}^{-n+1})^{n-1}\end{bmatrix} D=(ωn0)0(ωn1)0(ωnn+1)0(ωn0)1(ωn1)1(ωnn+1)2(ωn0)2(ωn1)2(ωnn+1)3(ωn0)n1(ωn1)n1(ωnn+1)n1

考虑 D × V D\times V D×V

( D × V ) i , j = ∑ k = 1 n − 1 D i , k ⋅ V i , k = ∑ i = 1 n − 1 ω n − i k ⋅ ω n j k = ∑ k = 1 n − 1 ω n ( j − i ) k \begin{aligned}(D\times V)_{i,j}&=\sum_{k=1}^{n-1}D_{i,k}\cdot V_{i,k}\\&=\sum_{i=1}^{n-1}{\omega_n}^{-ik}\cdot{\omega_n}^{jk}\\&=\sum_{k=1}^{n-1}{\omega_n}^{(j-i)k}\end{aligned} (D×V)i,j=k=1n1Di,kVi,k=i=1n1ωnikωnjk=k=1n1ωn(ji)k

i = j i=j i=j时, ( D × V ) i , j = ∑ k = 1 n − 1 ω n ( j − i ) k = ∑ k = 1 n − 1 ω n 0 = n \begin{aligned}(D\times V)_{i,j}&=\sum_{k=1}^{n-1}{\omega_n}^{(j-i)k}\\&=\sum_{k=1}^{n-1}{\omega_n}^{0}\\&=n\end{aligned} (D×V)i,j=k=1n1ωn(ji)k=k=1n1ωn0=n

i ≠ j i\ne j i=j时, ( D × V ) i , j = ∑ k = 1 n − 1 ω n ( j − i ) k = 1 + ( ω n j − i ) 1 + ( ω n j − i ) 2 + ⋯ + ( ω n j − i ) n − 1 = 1 − ( ω n j − i ) n 1 − ω n j − i = 0 \begin{aligned}(D\times V)_{i,j}&=\sum_{k=1}^{n-1}{\omega_n}^{(j-i)k}\\&=1+({\omega_n}^{j-i})^1+({\omega_n}^{j-i})^2+\cdots+({\omega_n}^{j-i})^{n-1}\\&=\frac{1-({\omega_n}^{j-i})^n}{1-{\omega_n}^{j-i}}\\&=0\end{aligned} (D×V)i,j=k=1n1ωn(ji)k=1+(ωnji)1+(ωnji)2++(ωnji)n1=1ωnji1(ωnji)n=0

于是 D × V × [ a 0 a 1 ⋮ a n ] = [ n 0 0 ⋯ 0 0 n 0 ⋯ 0 ⋮ ⋮ ⋮ ⋱ ⋮ 0 0 0 ⋯ n ] × [ a 0 a 1 ⋮ a n ] = [ n a 0 n a 1 ⋮ n a n ] D\times V\times\begin{bmatrix}a_0\\a_1\\\vdots\\a_n\end{bmatrix}=\begin{bmatrix}n&0&0&\cdots&0\\0&n&0&\cdots&0\\\vdots&\vdots&\vdots&\ddots&\vdots\\0&0&0&\cdots&n\end{bmatrix} \times \begin{bmatrix}a_0\\a_1\\\vdots\\a_n\end{bmatrix}=\begin{bmatrix}na_0\\na_1\\\vdots\\na_n\end{bmatrix} D×V×a0a1an=n000n000000n×a0a1an=na0na1nan

所以再给我们得到的点值表示乘上这个矩阵 D D D,每个值除以 n n n后就得到了多项式的系数表示。

FFT完整模板

代码中定义了复数类,方便运算。

好像STL里面也有一个复数类,但似乎跑得有点慢。

顺便提一句,由于FFT要求所代入的多项式必须是2的整数次幂,所以我们将多项式的项数扩充到了2的整数次幂。

const double PI = 3.1415926535;

struct Complex {
	double a, b;
	Complex(double x = 0, double y = 0) {a = x, b = y;}
	Complex operator + (const Complex &rhs) const {return Complex(a + rhs.a, b + rhs.b);}
	Complex operator - (const Complex &rhs) const {return Complex(a - rhs.a, b - rhs.b);}
	Complex operator * (const Complex &rhs) const {return Complex(a * rhs.a - b * rhs.b, a * rhs.b + b * rhs.a);}
};
int rev[Maxsize + 5];
void GetReverse(int len, int l) {
	for(int i = 0; i < len; i++)
		rev[i] = 0;
	for(int i = 0; i < len; i++)
		rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
}
inline Complex EXP(double theta) {
	return Complex(cos(theta), sin(theta));
}
void FFT(vector<Complex> &a, int dir) {
	int len = a.size();
	for(int i = 0; i < len; i++)
		if(i < rev[i]) swap(a[i], a[rev[i]]);
	for(int stp = 1; stp < len; stp <<= 1)
		for(int k = 0; k < stp; k++) {
			Complex wk = EXP(dir * k * PI / stp);
			for(int even = k; even < len; even += stp << 1) {
				int odd = even + stp;
				Complex tmp = a[odd] * wk;
				a[odd] = a[even] - tmp;
				a[even] = a[even] + tmp;
			}
		}
	if(dir == -1)
		for(int i = 0; i < len; i++)
			a[i].a /= len;
}

Part.3 NTT快速数论变换

不难发现FFT中大量用到了实数的运算和三角函数,这会导致精度、常数等问题。

我们是否可以在模意义下找到一种数,使得这些都满足单位复根的性质?

想一想我们用到的单位复根的性质:

  1. ω n i × ω n j = ω n i + j {\omega_n}^i\times{\omega_n}^j={\omega_n}^{i+j} ωni×ωnj=ωni+j
  2. (相消引理) ω d n d k = ω n k {\omega_{dn}}^{dk}={\omega_{n}}^k ωdndk=ωnk
  3. (折半引理) ω 2 n k = − ω 2 n k + n {\omega_{2n}}^k=-{\omega_{2n}}^{k+n} ω2nk=ω2nk+n
  4. n n n个数互不相同且有 ω n 0 = 1 {\omega_n}^0=1 ωn0=1

换句话说,我们要找出一种数,要求在模意义下满足上述条件。

单位复根的替代——原根

原根的性质

对于一个素数 P P P,若有一个数 G G G使得 G 1 , G 2 , G 3 , … , G p − 2 ( m o d    P ) G^1,G^2,G^3,\ldots,G^{p-2}(\mod P) G1,G2,G3,,Gp2(modP)互不相同,则 G G G P P P的一个原根。

再定义 g n k = ( G P − 1 n ) k {g_n}^k=(G^\frac{P-1}{n})^k gnk=(GnP1)k,检验一下是否满足上述性质。

  1. 由幂的运算可知显然成立;
  2. 由幂的运算可知显然成立;
  3. g 2 n n + k = ( G P − 1 2 n ) n + k = ( G P − 1 2 n ) n ⋅ ( G P − 1 2 n ) k = G P − 1 2 ⋅ g 2 n k ≡ − g 2 n k ( m o d    P ) {g_{2n}}^{n+k}=(G^\frac{P-1}{2n})^{n+k}=(G^\frac{P-1}{2n})^n\cdot(G^\frac{P-1}{2n})^k=G^\frac{P-1}{2}\cdot {g_{2n}}^k\equiv-{g_{2n}}^k(\mod P) g2nn+k=(G2nP1)n+k=(G2nP1)n(G2nP1)k=G2P1g2nkg2nk(modP)【因为 G p − 2 ≡ 1 ( m o d    P ) G^{p-2}\equiv1(\mod P) Gp21(modP),又因为 G G G P P P的原根即 G P − 1 ≢ G P − 1 2 ( m o d    P ) G^{P-1}\not\equiv G^\frac{P-1}{2}(\mod P) GP1G2P1(modP),故有 G P − 1 2 ≡ − 1 ( m o d    P ) G^\frac{P-1}{2}\equiv-1(\mod P) G2P11(modP)】;
  4. 由原根的定义可得。

然后就是将所有涉及到复数运算的部分全部替换成原根即可。

NTT的特殊要求

你以为将单位复根换成原根就完了?

事实上,我们知道FFT需要将多项式的项数扩充到二的整数次幂,而NTT也一样。

换句话说,NTT的模数的 P P P应该满足 P = k × 2 m + 1 P=k\times 2^m+1 P=k×2m+1,例如常用的模数 998244353 = 7 × 17 × 2 23 + 1 998244353=7\times17\times2^{23}+1 998244353=7×17×223+1

如果记不住原根可以按文章末尾的方法求出。

NTT完整模板

const int Mod = 998244353;
const int G = 3;

int QuickPow(int a, int k) {
	int ret = 1;
	while(k) {
		if(k & 1) ret = 1LL * ret * a % Mod;
		a = 1LL * a * a % Mod;
		k >>= 1;
	}
	return ret;
}

int rev[Maxsize + 5];
inline void GetReverse(int len, int lg) {
	for(int i = 0; i < len; i++) rev[i] = 0;
	for(int i = 0; i < len; i++)
		rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (lg - 1));
}
void NTT(vector<int> &a, int dir) {
	int len = a.size();
	for(int i = 0; i < len; i++)
		if(i < rev[i]) swap(a[i], a[rev[i]]);
	int g = (dir == 1 ? G : QuickPow(G, Mod - 2));
	for(int stp = 1; stp < len; stp <<= 1) {
		int wn = QuickPow(g, (Mod - 1) / (stp << 1));
		int w = 1;
		for(int k = 0; k < stp; k++) {
			for(int even = k; even < len; even += stp << 1) {
				int odd = even + stp;
				int tmp = 1LL * w * a[odd] % Mod;
				a[odd] = (a[even] - tmp + Mod) % Mod;
				a[even] = (ll)(a[even] + tmp) % Mod;
			}
			w = 1LL * w * wn % Mod;
		}
	}
	if(dir == -1) {
		int inv = QuickPow(len, Mod - 2);
		for(int i = 0; i < len; i++)
			a[i] = 1LL * a[i] * inv % Mod;
	}
}

补充:原根的求法

如何求一个奇素数 p p p的原根?

常用的方法是先将 p − 1 p-1 p1唯一分解成 p 1 k 1 p 2 k 2 ⋯ p m k m p_1^{k_1}p_2^{k_2}\cdots p_m^{k_m} p1k1p2k2pmkm

然后再暴力枚举原根,对于一个数 g g g,只要满足对于 p − 1 p-1 p1的任意一个质因数都有 g p − 1 p i ≡ 1 ( m o d    P ) g^\frac{p-1}{p_i}\equiv1(\mod P) gpip11(modP)成立,那么这个数就是原根。

一般的数的原根都不大,直接枚举即可。

补充:NTT常用模数及其原根表

参考自:https://blog.csdn.net/hnust_xx/article/details/76572828

对于NTT的模数 P = k × 2 m + 1 P=k\times 2^m+1 P=k×2m+1列出常用的部分和它们的原根。

P = k × 2 m + 1 P=k\times 2^m+1 P=k×2m+1 k k k m m m g g g
3 1 1 2
5 1 2 2
17 1 4 3
97 3 5 5
193 3 6 5
257 1 8 3
7681 15 9 17
12289 3 12 11
40961 5 13 3
65537 1 16 3
786433 3 18 10
5767169 11 19 3
7340033 7 20 3
23068673 11 21 3
104857601 25 22 3
167772161 5 25 3
469762049 7 26 3
998244353 119 23 3
1004535809 479 21 3
2013265921 15 27 31
2281701377 17 27 3
3221225473 3 30 5
7516927681 35 31 3
77309411329 9 33 7
206158430209 3 36 22
2061584302081 15 37 7
2748779069441 5 39 3
6597069766657 3 41 5
39582418599937 9 42 5
79164837199873 9 43 5
263882790666241 15 4 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898396 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054237 27 56 5
4179340454199820289 29 57 3

你可能感兴趣的:(#,多项式算法)