快速数论变换与多项式常用运算

10 篇文章 1 订阅

导入

一般来说,我们需要进行的操作是在模意义下的。

此时我们就无法使用单位根和实数意义下的FFT了。

我们的解决办法是:

原根

我们选择单位根是因为其具有良好的性质可以被我们利用。

如果我们在模意义下找到了单位根的替代品的话,就可以摆脱狭窄的值域和缓慢的三角函数了。

首先我们看一下我们之前用到的单位根的性质:

  1. ω n k = ( ω n 1 ) k \omega^k_n = (\omega^1_n)^k ωnk=(ωn1)k

同时我们需要保证我们找到的这个 ω n 1 \omega^1_n ωn1 不会使得所有的 ω n i \omega^i_n ωni 都相同。

  1. ω n k = ω n k     m o d     n \omega^k_n = \omega^{k \ \bmod{\ n}}_n ωnk=ωnk mod n

这可以推导出 ω n k + n 2 = − ω n k \omega^{k + \frac{n}{2}}_n = -\omega^k_n ωnk+2n=ωnk

  1. ω 2 n 2 k = ω n k \omega^{2k}_{2n} = \omega^k_n ω2n2k=ωnk

等价于 ( ω 2 n k ) 2 = ω n k (\omega^k_{2n})^2 = \omega^k_n (ω2nk)2=ωnk

我们再来看看原根的定义。

首先我们定义「阶」。

对于一个数 a a a,其在模 p p p 意义下的阶为,最小的整数 k k k 使得 a k ≡ 1 ( m o d p ) a^k \equiv 1 \pmod{p} ak1(modp)
对于 a ⊥̸ p a \not \perp p ap 的情况,我们认为其阶为 ∞ \infty 或直接认为不存在。
当然,我们一般求阶的时候 p p p 都是质数,且 a ∈ [ 0 , p ) a \in [0,p) a[0,p)
δ p ( a ) \delta_p(a) δp(a) a a a 在模 p p p 意义下的阶。

然后是「原根」。

对于一个数 g g g,如果满足 δ p ( g ) = φ ( p ) \delta_p(g) = \varphi(p) δp(g)=φ(p),我们就称 g g g p p p 的一个原根。

筛选

当然,我们可以猜到,不是随便一个 g g g 就可以当做单位根来用的。

我们需要找到一个 g g g 使得其阶恰好为 n n n 才可以。

一般来说我们的 p p p 是个质数,所以其原根的阶会达到 p − 1 p-1 p1,而一般我们不会只处理 n = p − 1 n=p-1 n=p1 的情况。

但是我们可以发现, g k g^k gk 的阶数为 p − 1 gcd ⁡ ( k , p − 1 ) \frac{p-1}{\gcd(k,p-1)} gcd(k,p1)p1,这给了我们拓展的机会。
同时我们发现,其仍然是 p − 1 p-1 p1 的约数,所以当 n ∤ ( p − 1 ) n \not | (p-1) n(p1) 的时候我们是找不到阶恰好为 n n n 的数的。
这也解释了为什么我们常用的NTT模数都是形如 k × 2 r + 1 k \times 2^r + 1 k×2r+1 的,比如说 998244353 = 119 × 2 23 + 1 998244353 = 119 \times 2^{23} + 1 998244353=119×223+1 1004535809 = 479 × 2 21 + 1 1004535809 = 479 \times 2^{21} + 1 1004535809=479×221+1

NTT

此时我们把单位根换成筛选后的原根,然后套入 FFT 板子中就好了。

同时我们做一些小优化,比如说预处理单位根、用unsigned long long来减少取模次数等等。

板子如下:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned ll
const int N = 2100010;
const ll G = 114514, mod = 998244353;
ll qpow(ll a, ll x = mod - 2)
{
	ll res = 1;
	while(x)
	{
		if(x & 1)res = res * a % mod;
		a = a * a % mod;
		x >>= 1;
	}
	return res;
}
const ll invG = qpow(G);
int tr[N];
void NTT(ll *g, bool op, int n)
{
	static ull f[N], w[N] = { 1 };
	for(int i = 0; i < n; i++)f[i] = g[tr[i]];
	for(int l = 1; l < n; l <<= 1)
	{
		ull tmpG = qpow(op ? G : invG, (mod - 1) / (l * 2));
		for(int i = 1; i < l; i++)w[i] = w[i - 1] * tmpG % mod;
		for(int k = 0; k < n; k += l * 2)
		{
			for(int p = 0; p < l; p++)
			{
				ll tmp = w[p] * f[k | l | p] % mod;
				f[k | l | p] = f[k | p] + mod - tmp;
				f[k | p] = f[k | p] + tmp;
			}
		}
		if(l == (1 << 10))
			for(int i = 0; i < n; i++)f[i] %= mod;
	}
	if(!op)
	{
		ull invn = qpow(n);
		for(int i = 0; i < n; i++)g[i] = f[i] % mod * invn % mod;
	}
	else
	{
		for(int i = 0; i < n; i++)g[i] = f[i] % mod;
	}
}
int n, m;
ll f[N], g[N];
int main()
{
	scanf("%d%d", &n, &m);
	n++, m++;
	for(int i = 0; i < n; i++)
		scanf("%lld", &f[i]);
	for(int i = 0; i < m; i++)
		scanf("%lld", &g[i]);
	int len = 1;
	while(len < n + m)len <<= 1;
	for(int i = 0; i < len; i++)
		tr[i] = (tr[i >> 1] >> 1) | ((i & 1) ? len >> 1 : 0);
	NTT(f, 1, len), NTT(g, 1, len);
	for(int i = 0; i < len; i++)
		f[i] = f[i] * g[i] % mod;
	NTT(f, 0, len);
	for(int i = 0; i < n + m - 1; i++)
		printf("%lld ", f[i]);
	putchar('\n');
	return 0;
}

多项式四则运算

为方便,下面我们设多项式 F ( x ) F(x) F(x) n n n 次项系数为 f [ n ] f[n] f[n],与 [ x n ] F ( x ) [x^n]F(x) [xn]F(x) 是等价的形式,

加减

直接对应系数加减就好了。

F ( x ) ± G ( x ) = ∑ i = 0 ( f [ i ] ± g [ i ] ) x i F(x) \pm G(x) = \sum_{i=0} (f[i] \pm g[i])x^i F(x)±G(x)=i=0(f[i]±g[i])xi

乘法

根据我们之前的定义,多项式的乘法是类似卷积形式的:

F ( x ) × G ( x ) = ∑ i = 0 x i ∑ k = 0 i f [ k ] g [ i − k ] F(x) \times G(x) = \sum_{i=0} x^i \sum_{k=0}^i f[k]g[i-k] F(x)×G(x)=i=0xik=0if[k]g[ik]

我们可以使用 NTT 来在 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的时间复杂度内做完。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned ll
#define clr(f,n) memset(f,0,(n)*sizeof(ll));
#define cpy(f,g,n) memcpy(f,g,(n)*sizeof(ll));
const int N = 270010;
const ll G = 3, mod = 998244353;
ll qpow(ll a, ll x = mod - 2)
{
	ll res = 1;
	while(x)
	{
		if(x & 1)res = res * a % mod;
		a = a * a % mod;
		x >>= 1;
	}
	return res;
}
const ll invG = qpow(G);
int tr[N], tf;
void initr(int n)
{
	if(tf == n)return;
	tf = n;
	for(int i = 0; i < n; i++)
		tr[i] = (tr[i >> 1] >> 1) | ((i & 1) ? n >> 1 : 0);
}
void NTT(ll *g, bool op, int n)
{
	initr(n);
	static ull f[N], w[N] = { 1 };
	for(int i = 0; i < n; i++)f[i] = ((mod << 5) + g[tr[i]]) % mod;
	for(int l = 1; l < n; l <<= 1)
	{
		ull tmpG = qpow(op ? G : invG, (mod - 1) / (l * 2));
		for(int i = 1; i < l; i++)w[i] = w[i - 1] * tmpG % mod;
		for(int k = 0; k < n; k += l * 2)
		{
			for(int p = 0; p < l; p++)
			{
				ll tmp = w[p] * f[k | l | p] % mod;
				f[k | l | p] = f[k | p] + mod - tmp;
				f[k | p] = f[k | p] + tmp;
			}
		}
		if(l == (1 << 10))
			for(int i = 0; i < n; i++)f[i] %= mod;
	}
	if(!op)
	{
		ull invn = qpow(n);
		for(int i = 0; i < n; i++)g[i] = f[i] % mod * invn % mod;
	}
	else
	{
		for(int i = 0; i < n; i++)g[i] = f[i] % mod;
	}
}
void mul(ll *f, ll *g, int n)
{
	for(int i = 0; i < n; i++)
		f[i] = f[i] * g[i] % mod;
}
void timep(ll *f, ll *g, int m, int lim)
{
	static ll sav[N];
	int n = 1;
	while(n < m + m)n <<= 1;
	clr(sav, n); cpy(sav, g, n);
	NTT(f, 1, n); NTT(sav, 1, n);
	mul(f, sav, n); NTT(f, 0, n);
	clr(f + lim, n - lim); clr(sav, n);
}

在前面NTT的基础上增加了一些宏定义,并略微调整之使得其适配后面的操作。
后面的直接调用此模板即可。

乘法逆元

定义就是对于一个多项式 F ( x ) F(x) F(x),找到一个多项式 G ( x ) G(x) G(x) 是的 F ( x ) × G ( x ) = 1 F(x) \times G(x) = 1 F(x)×G(x)=1

首先很明显, g [ 0 ] g[0] g[0] 就是 f [ 0 ] f[0] f[0] 的乘法逆元。

然后我们尝试由此拓展出整个多项式。

直接递推显然不行,我们无法承受 O ( n 2 ) O(n^2) O(n2) 的时间复杂度。
那么试试倍增。

假设我们当前进行到了第 n n n 次项,我们尝试将其推广到 2 n 2n 2n 次项。

那么我们当前的多项式 KaTeX parse error: Undefined control sequence: \* at position 3: R_\̲*̲(x) 就可以看做是 G ( x ) ( m o d x n ) G(x) \pmod{x^n} G(x)(modxn),而我们将要推广到的多项式 R ( x ) R(x) R(x) 就可以看做是 G ( x ) ( m o d x 2 n ) G(x) \pmod{x^{2n}} G(x)(modx2n)
这样才可以从 g [ 0 ] g[0] g[0] 开始倍增。
很明显有 KaTeX parse error: Undefined control sequence: \* at position 10: R(x) = R_\̲*̲(x) \pmod{x^n},作差得 KaTeX parse error: Undefined control sequence: \* at position 10: R(x) - R_\̲*̲(x) = 0 \pmod{x…
平方一下得 KaTeX parse error: Undefined control sequence: \* at position 13: R^2(x) - 2R_\̲*̲(x)R(x) + R^2_\…
等式两边同时乘以 F ( x ) F(x) F(x) 可以把 R ( x ) R(x) R(x) 消掉,得 KaTeX parse error: Undefined control sequence: \* at position 11: R(x) - 2R_\̲*̲(x) + R^2_\*(x)…
由此可以根据 F ( x ) F(x) F(x)KaTeX parse error: Undefined control sequence: \* at position 3: R_\̲*̲(x) 求出 R ( x ) R(x) R(x)

void invp(ll *f, int m)
{
	int n = 1;
	while(n < m)n <<= 1;
	static ll w[N], r[N], sav[N];
	w[0] = qpow(f[0]);
	for(int len = 2; len <= n; len <<= 1)
	{
		for(int i = 0; i < (len >> 1); i++)r[i] = w[i];
		cpy(sav, f, len); NTT(sav, 1, len);
		NTT(r, 1, len); mul(r, sav, len);
		NTT(r, 0, len); clr(r, len >> 1);
		cpy(sav, w, len); NTT(sav, 1, len);
		NTT(r, 1, len); mul(r, sav, len);
		NTT(r, 0, len);
		for(int i = len >> 1; i < len; i++)
			w[i] = (w[i] * 2 - r[i] + mod) % mod;
	}
	cpy(f, w, m);
	clr(sav, n); clr(w, n); clr(r, n);
}

注意此处需要开两倍空间,所有带有乘法逆元操作的都需要开两倍空间。

带余数的除法

如果我们可以直接把余式减去,然后就可以直接做上面的多项式除法了。

但是余式的系数集中在低次项,我们能做的只有   m o d   x k \bmod{x^k} modxk

考虑翻转一下多项式的系数,定义 F T ( x ) = ∑ i = 0 f [ n − i ] x i F^T(x) = \sum_{i=0} f[n-i]x^i FT(x)=i=0f[ni]xi
稍微换个形式可以得到 F T ( x ) = x n F ( x − 1 ) F^T(x) = x^n F(x^{-1}) FT(x)=xnF(x1)

首先我们有式子 F ( x ) = Q ( x ) × G ( x ) + R ( x ) F(x) = Q(x) \times G(x) + R(x) F(x)=Q(x)×G(x)+R(x),其中 F ( x ) F(x) F(x) G ( x ) G(x) G(x) 已知。
换元得到 F ( x − 1 ) = Q ( x − 1 ) × G ( x − 1 ) + R ( x − 1 ) F(x^{-1}) = Q(x^{-1}) \times G(x^{-1}) + R(x^{-1}) F(x1)=Q(x1)×G(x1)+R(x1)
两边同时乘以 x n x^n xn 得到 x n F ( x − 1 ) = x n Q ( x − 1 ) × G ( x − 1 ) + x n R ( x − 1 ) x^n F(x^{-1}) = x^n Q(x^{-1}) \times G(x^{-1}) + x^n R(x^{-1}) xnF(x1)=xnQ(x1)×G(x1)+xnR(x1)
写成翻转的形式得到 F T ( x ) = Q T ( x ) × G T ( x ) + x n − m + 1 R T ( x ) F^T(x) = Q^T(x) \times G^T(x) + x^{n-m+1} R^T(x) FT(x)=QT(x)×GT(x)+xnm+1RT(x)
此时余式 R ( x ) R(x) R(x) 原本集中在低次项的系数全部到了高次项,而低次项都为 0 0 0
我们对 x n − m + 1 x^{n-m+1} xnm+1 取模,得到 F T ( x ) = Q T ( x ) × G T ( x ) ( m o d x n − m + 1 ) F^T(x) = Q^T(x) \times G^T(x) \pmod{x^{n-m+1}} FT(x)=QT(x)×GT(x)(modxnm+1)
然后我们就可以求出来 Q T ( x ) Q^T(x) QT(x),进而得到 Q ( x ) Q(x) Q(x)
剩下的 R ( x ) R(x) R(x) 直接减就好了。

void divp(ll *f, ll *g, int n, int m)
{
	static ll q[N], t[N];
	int l = n - m + 1;
	rev(g, m); cpy(q, g, l); rev(g, m);
	rev(f, n); cpy(t, f, l); rev(f, n);
	invp(q, l); timep(q, t, l, l); rev(q, l);
	timep(g, q, n, n);
	for(int i = 0; i < m - 1; i++)
		g[i] = (f[i] - g[i] + mod) % mod;
	clr(g + m - 1, l);
	cpy(f, q, l); clr(f + l, n - l);
}

函数执行完之后,f数组内是商式,g数组内是余式。

多项式开根

我们有式子 F ( x ) − G 2 ( x ) = 0 F(x) - G^2(x) = 0 F(x)G2(x)=0,其中 F ( x ) F(x) F(x) 已知。

H ( G ( x ) ) = G ( x ) 2 − F ( x ) H(G(x)) = G(x)^2 - F(x) H(G(x))=G(x)2F(x),则 H ′ ( G ( x ) ) = 2 G ( x ) H'(G(x)) = 2G(x) H(G(x))=2G(x)

根据牛顿迭代 KaTeX parse error: Undefined control sequence: \* at position 13: G(F(x)) = F_\̲*̲(x) - \frac{G(F…,得到:

KaTeX parse error: Undefined control sequence: \* at position 26: …ign} G(x) &= G_\̲*̲(x) - \frac{H(G…

由此可以倍增。

因为题目保证了 f [ 0 ] = 1 f[0] = 1 f[0]=1,所以我们的 g [ 0 ] g[0] g[0] 也就是 1 1 1 了。

void sqrtp(ll *f, int m)
{
	int n = 1;
	while(n < m)n <<= 1;
	static ll b1[N], b2[N];
	b1[0] = 1;
	for(int len = 2; len <= n; len <<= 1)
	{
		for(int i = 0; i < (len >> 1); i++)b2[i] = (b1[i] << 1) % mod;
		invp(b2, len);
		NTT(b1, 1, len); mul(b1, b1, len); NTT(b1, 0, len);
		for(int i = 0; i < len; i++)b1[i] = (f[i] + b1[i]) % mod;
		timep(b1, b2, len, len);
	}
	cpy(f, b1, m); clr(b1, n + n); clr(b2, n + n);
}

多项式求导与积分

根据积分公式和求导的公式直接做就好了。

导数: F ′ ( x ) = ∑ i = 1 f [ i ] i x i − 1 F'(x) = \sum_{i=1} f[i]ix^{i-1} F(x)=i=1f[i]ixi1
积分: ∫ F ( x ) = C + ∑ i = 0 f [ i ] x i + 1 i \int F(x) = C + \sum_{i=0} \frac{f[i]x^{i+1}}{i} F(x)=C+i=0if[i]xi+1

ll inv[N];
void initi(int lim)
{
	inv[1] = 1;
	for(int i = 2; i <= lim; i++)
		inv[i] = inv[mod % i] * (mod - mod / i) % mod;
}
void derp(ll *f, int m)
{
	for(int i = 1; i < m; i++)
		f[i - 1] = f[i] * i % mod;
	f[m - 1] = 0;
}
void intp(ll *f, int m)
{
	for(int i = m; i; i--)
		f[i] = f[i - 1] * inv[i] % mod;
	f[0] = 0;
}

需要预处理逆元。

多项式ln与exp

为了保证我们可以只使用四则运算来得到 ln ⁡ ( F ( x ) ) \ln(F(x)) ln(F(x)) e F ( x ) e^{F(x)} eF(x),我们需要使用麦克劳林级数:

ln ⁡ ( F ( x ) ) = − ∑ i = 1 ( 1 − F ( x ) ) i i exp ⁡ ( F ( x ) ) = ∑ i = 0 F i ( x ) i ! \begin{gather} \ln(F(x)) = - \sum_{i=1} \frac{(1-F(x))^i}{i} \\ \exp(F(x)) = \sum_{i=0} \frac{F^i(x)}{i!} \end{gather} ln(F(x))=i=1i(1F(x))iexp(F(x))=i=0i!Fi(x)

当然,因为我们有了积分和求导两个操作,我们可以利用 ln ⁡ ′ ( x ) = 1 x \ln'(x)=\frac{1}{x} ln(x)=x1 来简化我们的操作。

G ( x ) = ln ⁡ ( F ( x ) ) G(x) = \ln(F(x)) G(x)=ln(F(x)),则

ln ⁡ ( F ( x ) ) = G ( x ) d d x ln ⁡ ( F ( x ) ) = d d x G ( x ) d F ( x ) d x d d F ( x ) ln ⁡ ( F ( x ) ) = d d x G ( x ) F ′ ( x ) F ( x ) = G ′ ( x ) ∫ F ′ ( x ) F ( x ) = G ( x ) \begin{align} \ln(F(x)) &= G(x) \\ \frac{d}{dx} \ln(F(x)) &= \frac{d}{dx} G(x) \\ \frac{dF(x)}{dx} \frac{d}{dF(x)} \ln(F(x)) &= \frac{d}{dx} G(x) \\ \frac{F'(x)}{F(x)} &= G'(x)\\ \int \frac{F'(x)}{F(x)} &= G(x) \end{align} ln(F(x))dxdln(F(x))dxdF(x)dF(x)dln(F(x))F(x)F(x)F(x)F(x)=G(x)=dxdG(x)=dxdG(x)=G(x)=G(x)

因为exp是ln的逆运算,所以可以直接牛顿迭代来倍增。

void lnp(ll *f, int m)
{
	static ll g[N];
	cpy(g, f, m);
	invp(g, m); derp(f, m);
	timep(f, g, m, m);
	intp(f, m - 1);
	clr(g, m);
}
void exp(ll *f, int m)
{
	static ll s[N], s2[N];
	int n = 1;
	while(n < m)n <<= 1;
	s2[0] = 1;
	for(int len = 2; len <= n; len <<= 1)
	{
		cpy(s, s2, len >> 1);
		lnp(s, len);
		for(int i = 0; i < len; i++)s[i] = (f[i] - s[i] + mod) % mod;
		s[0] = (s[0] + 1) % mod;
		timep(s2, s, len, len);
	}
	cpy(f, s2, m);
	clr(s, n); clr(s2, n);
}

注意求exp的时候需要调用求ln的函数。

多项式快速幂

为了做 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的快速幂,我们需要利用 ln ⁡ ( a b ) = ln ⁡ ( a ) + b \ln(ab) = \ln(a)+b ln(ab)=ln(a)+b 将幂运算转化为乘法。

具体来说, A k ( x ) = e ln ⁡ ( A ( x ) ) × k A^k(x) = e^{\ln(A(x)) \times k} Ak(x)=eln(A(x))×k

此时需要满足 a [ 0 ] = 1 a[0] = 1 a[0]=1

如果不是的话,我们可以将 A ( x ) A(x) A(x) 写作 B ( x ) × c x p B(x) \times cx^p B(x)×cxp 的形式,此时 b [ 0 ] = 1 b[0]=1 b[0]=1,可以套用上面的方法。

具体来说, A k ( x ) = ( B ( x ) × c x p ) k = B k ( x ) × c k x p k A^k(x) = (B(x) \times cx^p)^k = B^k(x) \times c^kxp^k Ak(x)=(B(x)×cxp)k=Bk(x)×ckxpk
此时需要注意 c c c 的幂次需要根据费马小定理对 m o d − 1 mod-1 mod1 取模。

int n;
char s[N];
ll f[N];
int main()
{
	scanf("%d%s", &n, s);
	for(int i = 0; i < n; i++)
		scanf("%lld", &f[i]);
	int p = 0, c;
	while(!f[p])p++;
	c = qpow(f[p]);
	ll k = 0, k2 = 0;
	for(int i = 0; isdigit(s[i]); i++)
	{
		k = (k * 10 + s[i] - '0') % mod;
		k2 = (k2 * 10 + s[i] - '0') % (mod - 1);
		if(k * p >= n)
		{
			for(int i = 0; i < n; i++)printf("0 ");
			putchar('\n');
			return 0;
		}
	}
	initi(n = n - p * k);
	for(int i = 0; i < n; i++)f[i] = f[i + p] * c % mod;
	clr(f + n, p * k);
	lnp(f, n);
	for(int i = 0; i < n; i++)f[i] = f[i] * k % mod;
	exp(f, n);
	for(int i = 0; i < p * k; i++)printf("0 ");
	c = qpow(c, mod - 1 - k2);
	for(int i = 0; i < n; i++)f[i] = f[i] * c % mod;
	for(int i = 0; i < n; i++)
		printf("%lld ", f[i]);
	putchar('\n');
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值