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为虚部。

它可以在二维坐标系中表示,它表示为一个向量。

称此时它的长度为模长,与 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),但一个显然的问题是这个算法的常数太大了。

我们考虑将整个奇偶分组后各个系数数的位置:

原来的序列: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
3112
5122
17143
97355
193365
257183
768115917
1228931211
409615133
655371163
78643331810
576716911193
73400337203
2306867311213
10485760125223
1677721615253
4697620497263
998244353119233
1004535809479213
2013265921152731
228170137717273
32212254733305
751692768135313
773094113299337
20615843020933622
206158430208115377
27487790694415393
65970697666573415
395824185999379425
791648371998739435
2638827906662411547
123145302310912135453
133700613937561719463
379991218559385727475
4222124650659841154819
78812993478983967506
315251973915934737523
1801439850948198415556
194555503902405423727565
417934045419982028929573
  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值