多项式


前置芝士

复数指数幂

有欧拉公式

显然$e^{i\pi}=1$

单位根

单位根的定义:
在复数域下,满足$x^n=1$的$x$被称为$n$次单位根。
根据代数基本定理,$n$次单位根一共有$n$个,且所有的$n$次单位根按照幅角排列是均匀分布的,第$k(0\le k<n)$个$n$次单位根为

$n$个单位根在复平面上平分单位圆。

本原单位根

$0$到$n-1$次方的值能生成所有$n$次单位根的$n$次单位根称为$n$次本原单位根。
记$n$次本原单位根为$\omega_n=e^{i\frac{2\pi}{n}}=\cos\dfrac{2\pi}{n}+i\sin\dfrac{2\pi}{n}$

设$n=2m$,有

阶与原根

欧拉定理

若正整数$a,m$满足$(a,m)=1$,则使得$a^n\equiv 1\pmod{m}$的最小正整数$n$称为$a$模$m$的阶,记作$\delta_m(a)$。
若$\delta_m(a)=\phi(m)$,则称$a$为$m$的一个原根。

阶的性质:

  • $a^0,a^1,…,a^{\delta - 1}$在模$m$意义下两两不同
  • $a^{\gamma}\equiv a^{\gamma’}\pmod{m}\Leftrightarrow \gamma\equiv\gamma’\pmod{\delta}$
  • $\delta |\phi(m)$

原根的存在定理:
只有模$2,4,p^a,2p^a$存在原根,其中$p$为奇质数。

原根的判定定理:
设$m>1$,$g$为正整数且$(m,g)=1$,则$g$是$m$的原根当且仅当对于任意$\phi(m)$的质因子$q_i$有$g^{\frac{\phi(m)}{q_i}}\not\equiv 1\pmod{m}$。

正整数$n$若存在原根,则其最小原根是$O(n^{\frac{1}{4}})$的,要求出$n$的所有原根可以通过先求出最小原根$g$,根据$g$来求所有原根。所有原根应满足$g’=g^k$且$(k,\phi(n))=1$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ll getrt(ll p) {
//求p的最小原根
ll tmp = p, phi = 1;
vector<int>q;
for (int i = 2; i * i <= tmp; ++i) {
if (tmp % i == 0) {
q.push_back(i);
tmp /= i;
phi *= i - 1;
while (tmp % i == 0)phi *= i, tmp /= i;
}
}
if (tmp != 1)phi *= tmp - 1, q.push_back(tmp);
if (p != 2 && p != 4 && (p % 4 == 0 || q.size() > 2 || (q.size() == 2 && q[0] != 2)))return -1;
q.clear();
tmp = phi;
for (int i = 2; i * i <= tmp; ++i) {
if (tmp % i == 0) {
q.push_back(phi / i);
while (tmp % i == 0)tmp /= i;
}
}
if (tmp != 1)q.push_back(phi / tmp);
for (int i = 1; ; ++i) {
if (GCD(p, i) > 1)continue;
bool tag = true;
for (auto j : q)
if (ksm(i, j, p) == 1) {
tag = false;
break;
}
if (tag)return i;
}
return -1;
}

指标

对于质数$p$,假设$g$是$p$的一个原根,则$g^0,g^1,…,g^{p-2}$在模$p$意义下是$1,2,…,p-1$的一个排列。假设对于$1\le x\le p-1$有$g^c\equiv x\pmod{p}$,则称$x$的指标为$c$,记作${\rm ind}(x)=c$。

指标的性质类似于对数,对于$\forall x,y\in[1,p-1]$:

  • ${\rm ind}(x\cdot y)={\rm ind}(x)+{\rm ind}(y)$
  • ${\rm ind}(x^c)=c\cdot {\rm ind}(x)$

求指标直接用BSGS。

离散快速傅里叶变换(FFT)

两个多项式做乘法,用系数表示法直接算显然需要$O(n^2)$时间复杂度,于是我们考虑用点值表示法来计算,如果我们能求出两个多项式的点值表示法,那么就可以直接相乘来得到多项式相乘后的点值表示法,注意由于两个$n$次多项式相乘后是$2n$次多项式,所以我们需要求$2n$个点值。

点值表示法如果直接求,需要$O(n^2)$计算两个多项式的点值,$O(n^3)$高斯消元解出系数,我们需要将这两步的复杂度优化。
求出一个$n$次多项式在每个$n$次单位根下的点值的过程被称为离散傅里叶变换(DFT),将点值重新插值成系数表示法的过程称为逆离散傅里叶变换(IDFT)
下面我们设进行变换的多项式为$n-1$次多项式$A(x)=\sum\limits_{i=0}^{n-1}a_i\times x^i$,其中$n$是2的幂(若不是则补齐),$m=\dfrac{n}{2}$。

FFT

考虑对$A(x)$的下角标按奇偶分类,得到

将右式提一个x出来,得到

我们设

虽然规模减小了,但是$A_0,A_1$系数是不一样的,那每层往下递归两次还是$O(n^2)$的。
由前置芝士我们知道,$\omega_n^{m+k}=-\omega_n^{k}$,$(\omega_n^k)^2=\omega_m^k$,于是就来考虑能不能只求小于$m$的部分。
对于$0\le k< m$,我们有

考虑$m+k$的部分,我们有

观察(2)和(5),只要求出$0\le k<m$的部分的$A_0,A_1$,我们就能求出$A$了,问题规模缩小到原来的$\dfrac{1}{2}$。到这里,由系数表示法求点值的问题就解决了。
复杂度$O(n\log n)$

IDFT

现在我们来看,得到了$n$个点值后如何插值得到多项式的系数。
假设已知一个$n-1$次多项式$A(x)=\sum\limits_{i=0}^{n-1}a_ix^i$进行DFT后得到了点值$\{b_k\}$,那么求解$a$的过程相当于求解以下方程组

设为$\Omega\times A=B$,则$B=\Omega^{-1}\times A$
这里直接给出结论

令$M=\Omega^{-1}\times\Omega$,写出$M_{ij}$的表达式

于是求解$a$的方程就为

这就相当于给定了$B(x)=\sum\limits_{i=0}^{n-1}b_ix^i$,求点值$B(\omega_n^{-k}),0\le k<n$
该问题的形式与前面求$A(x)$的点值完全相同,唯一的区别就是$\omega_n$的指数多了一个负号,计算的时候把符号加上就行了。
时间复杂度也是求点值的复杂度$O(n\log n)$。

模板

以上只是利用FFT计算多项式乘法的基本原理,FFT本身常数很大,优化的手段很多,例如蝴蝶变换可以非递归实现FFT,还有两个多项式都需要求点值,这就需要三次FFT,而实际上可以一次FFT求出两个多项式的点值,下面的板子是最终版本,可以直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct Complex {
double x, y;
Complex(double x = 0.0, double y = 0.0) : x(x), y(y) {}
Complex operator +(const Complex &s)const { return Complex(x + s.x, y + s.y); }
Complex operator -(const Complex &s)const { return Complex(x - s.x, y - s.y); }
Complex operator *(const Complex &s)const { return Complex(x * s.x - y * s.y, x * s.y + y * s.x); }
};
struct Fast_Fourier_Transfrom {
Complex a[N];
int r[N];
void FFT(int len, Complex a[], int type) {
for (int i = 0; i < len; ++i)
if (i < r[i])
swap(a[i], a[r[i]]);
for (int mid = 1; mid < len; mid <<= 1) {
Complex wn = Complex(cos(PI / mid), type * sin(PI / mid));
for (int R = mid << 1, j = 0; j < len; j += R) {
Complex w = Complex(1, 0);
for (int k = 0; k < mid; ++k, w = w * wn) {
Complex x = a[j + k], y = w * a[j + mid + k];
a[j + k] = x + y;
a[j + k + mid] = x - y;
}
}
}
}
void Mul(int n, int m) {
int len = 1, x = 0;
//假设n>=m,将第一个多项式的系数放在实部,第二个多项式的系数放在虚部
//最终a[i].x就是指数为i的项的系数
for (; len <= n + m - 2; len <<= 1, ++x);
for (int i = 0; i < len; ++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (x - 1));
memset(a + n, 0, sizeof(Complex) * (len - n + 1));
FFT(len, a, 1);
for (int i = 0; i <= len; ++i)
a[i] = a[i] * a[i];
FFT(len, a, -1);
for (int i = 0; i <= n + m - 2; ++i)
a[i].x = a[i].y / len / 2;
}
}FFT;

快速数论变换(NTT)

假设质数$p$满足$p=r2^l+1$,$g$是$p$的原根,用$g_n=g^{\frac{p-1}{n}}$替换FFT中的$\omega_n$。
当项数$n\le 2^l$时,与$\omega_n$相似的性质仍然成立:

  • $g_{2n}^{2k}\equiv g_n^k\pmod{p}$
  • $g_{2n}^n\equiv-1\pmod{p}$
  • $\sum\limits_{k=0}^{n-1}g_{n}^{ik}g_n^{-kj}=\begin{cases}n&i=j\\0&i\not=j\end{cases}\pmod{p}$,其中$0\le i,j<n$

所以将$\omega_n$直接替换成$g_n$,DFT、IDFT的过程仍然正确,这样构成的算法就是NTT。值得注意的是,用NTT求解的多项式系数是模$p$后的结果,所以如果需要精确的系数,就需要选用较大的模数。

常见模数:

  • $65537=2^{16}+1,g=3$
  • $167772161=5\times2^{25}+1$
  • $469762049=7\times2^{26}+1$
  • $998244353=119\times 2^{23}+1,g=3$
  • $1004535809=479\times2^{21}+1,g=3$
  • $4179340454199820289=29\times 2^{57}+1,g=3$

模板

仿照FFT直接写出NTT的模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct Number_Theoretic_Transform {
ll G = 3, Gi = 332748118, mod = 998244353;
//特别注意,封装mod常数非常大,如果不是为了用MTT还是放外面好
ll a[N], b[N];
int r[N];
ll FastPow(ll a, ll b) {
ll ans = 1;
for (; b; b >>= 1, (a *= a) %= mod)
if (b & 1)(ans *= a) %= mod;
return ans;
}
ll ADD(ll a, ll b) { a += b; return a >= mod ? a - mod : a; }
ll DEL(ll a, ll b) { a -= b; return a < 0 ? a + mod : a; }
void NTT(int len, ll a[], int type) {
for (int i = 0; i < len; ++i)
if (i < r[i])
swap(a[i], a[r[i]]);
for (int mid = 1; mid < len; mid <<= 1) {
ll gn = FastPow(type ? G : Gi, (mod - 1) / (mid << 1));
for (int R = mid << 1, j = 0; j < len; j += R) {
ll g = 1;
for (int k = 0; k < mid; ++k, g = g * gn % mod) {
ll x = a[j + k], y = g * a[j + mid + k] % mod;
a[j + k] = ADD(x, y);
a[j + mid + k] = DEL(x, y);
}
}
}
}
void Mul(int n, int m) {
int len = 1, x = 0;
for (; len <= n + m - 2; len <<= 1, ++x);
for (int i = 0; i < len; ++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (x - 1));
memset(a + n, 0, sizeof(ll) * (len - n + 1));
memset(b + m, 0, sizeof(ll) * (len - m + 1));
NTT(len, a, 1); NTT(len, b, 1);
for (int i = 0; i < len; ++i)
a[i] = a[i] * b[i] % mod;
NTT(len, a, 0);
ll inv = FastPow(len, mod - 2);
for (int i = 0; i <= n + m - 2; ++i)
a[i] = a[i] * inv % mod;
}
}NTT;

任意模数NTT(MTT)

在NTT的基础上我们可以推广出求解模数为任意值的多项式运算方法。
其基本思想为选取若干个不同的NTT模数,通过NTT分别求出运算结果,最后利用exCRT将这些结果合并,由exCRT的性质我们知道最终合并的结果是在模NTT模数之积意义下的,所以需要保证NTT模数之积是大于运算结果的,这样就能求出运算结果的实际值,当然任意模意义下的值也能求了。

这里以取三个模数做多项式乘法为例。
设某一项系数待求答案为$x$,三个模数为$A,B,C$,则有

合并前两个方程得到

这就意味着$x\equiv x_0+k_0A\pmod{AB}$,令$x_3=x_1+k_0A$,继续合并得

于是最终得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct Multiple_Theoretic_Transform {
Number_Theoretic_Transform NTT[3];
ll ADD(ll a, ll b, ll p) { a += b; return a >= p ? a - p : a; }
ll DEL(ll a, ll b, ll p) { a -= b; return a < 0 ? a + p : a; }
ll mul(ll a, ll b, ll p) {
ll ans = 0;
for (; b; b >>= 1, a = ADD(a, a, p))
if (b & 1)ans = ADD(ans, a, p);
return ans;
}
ll ksm(ll a, ll b, ll p) {
ll ans = 1;
for (; b; b >>= 1, a = mul(a, a, p))
if (b & 1)ans = mul(ans, a, p);
return ans;
}
void init() {
NTT[0].mod = 167772161, NTT[0].G = 3, NTT[0].Gi = NTT[0].FastPow(3, NTT[0].mod - 2);
NTT[1].mod = 469762049, NTT[1].G = 3, NTT[1].Gi = NTT[1].FastPow(3, NTT[1].mod - 2);
NTT[2].mod = 998244353, NTT[2].G = 3, NTT[2].Gi = NTT[2].FastPow(3, NTT[2].mod - 2);
}
void Mul(int n, int m, ll a[], ll b[], ll p) {
init();
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < n; ++j)NTT[i].a[j] = a[j];
for (int j = 0; j < m; ++j)NTT[i].b[j] = b[j];
NTT[i].Mul(n, m);
}
for (int i = 0; i <= n + m - 2; ++i) {
ll k0 = mul(DEL(NTT[1].a[i], NTT[0].a[i], NTT[1].mod), ksm(NTT[0].mod, NTT[1].mod - 2, NTT[1].mod), NTT[1].mod);
ll x = ADD(NTT[0].a[i], mul(k0, NTT[0].mod, NTT[2].mod), NTT[2].mod);
ll k3 = mul(DEL(NTT[2].a[i], x, NTT[2].mod), ksm(NTT[0].mod * NTT[1].mod % NTT[2].mod, NTT[2].mod - 2, NTT[2].mod), NTT[2].mod);
a[i] = ADD(ADD(NTT[0].a[i] % p, mul(k0 % p, NTT[0].mod % p, p), p), mul(k3 % p, NTT[0].mod * NTT[1].mod % p, p), p);
}
}
}MTT;

其他运算也是类似的先分别求答案再合并。

拉格朗日插值

拉格朗日插值定理

$n$个点值$(x_i,y_i),(1\le i\le n)$,满足$x_i\not=x_j,(i\not=j)$,它们唯一确定一个$n-1$次多项式$f(x)$

如果已知$x_i=i$的连续$n$个点值,就可以$O(n)$地求出任意一个点值$f(x_0)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ll solve(int x) {
//求出f(x),已知x=0~n的点值
//处理分母,这里逆元可以递推,偷懒就快速幂
muln[1] = mod - 1, mulp[1] = 1;
for (int i = 2; i <= n + 1; ++i) {
muln[i] = (mod - muln[i - 1] * ksm(i, mod - 2) % mod) % mod;
mulp[i] = mulp[i - 1] * ksm(i,mod - 2) % mod;
}
//处理分子
L[0] = x, R[n] = x - n;
for (int i = 1; i < n; ++i) {
L[i] = L[i - 1] * ((x - i + mod) % mod) % mod;
R[n - i] = R[n - i + 1] * ((x - n + i + mod) % mod) % mod;
}
ll ans = 0;
for (int i = 0; i <= n; ++i) {
if (i == 0)
(ans += f[i] * R[1] % mod * muln[n] % mod) %= mod;
else if (i == n)
(ans += f[i] * L[n - 1] % mod * mulp[n] % mod) %= mod;
else
(ans += f[i] * L[i - 1] % mod * R[i + 1] % mod * muln[n - i] % mod * mulp[i] % mod) %= mod;
}
return ans;
}

牛顿迭代

给定多项式$g(x)$,求满足$g(f(x))=0$的形式幂级数$f(x)$。
牛顿迭代用于求解$f(x)$前$n$项。

  • $n=1$时,解$g(a_0)=0$得到$a_0$
  • 如果已知前$n$项$f(x)\equiv f_0(x)=a_0+a_1x+…+a_{n-1}x^{n-1}\pmod{x^n}$
  • 则$f(x)\equiv f_0-\dfrac{g(f_0(x))}{g’(f_0(x))}\pmod{x^{2n}}$

多项式(形式幂级数)求逆

设$h(x)$是给定的形式幂级数,求它的逆$f(x)$,则$g(f(x))=\dfrac{1}{f(x)}-h(x)=0$,得到的迭代形式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Inv(int n, ll f[], ll h[]) {
//求h的逆,答案放在f中,n为项数
f[0] = FastPow(h[0], mod - 2);
for (int len = 2, x = 2; (len >> 1) < n; len <<= 1, ++x) {
for (int i = 0; i < len; ++i)a[i] = f[i], b[i] = h[i];
memset(a + len, 0, sizeof(ll) * len);
memset(b + len, 0, sizeof(ll) * len);
for (int i = 0; i < (len << 1); ++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (x - 1));
NTT(len << 1, a, 1); NTT(len << 1, b, 1);
for (int i = 0; i < (len << 1); ++i)
f[i] = a[i] * DEL(2, a[i] * b[i] % mod) % mod;
NTT(len << 1, f, 0);
ll inv = FastPow(len << 1, mod - 2);
for (int i = 0; i < len; ++i)
f[i] = f[i] * inv % mod;
memset(f + len, 0, sizeof(ll) * len);
}
}

多项式(形式幂级数)开方

设$h(x)$是给定的形式幂级数,求它的逆$f(x)$,则$g(f(x))=f^2(x)-h(x)=0$,得到的迭代形式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ll invf[N];
void Sqrt(int n, ll f[], ll h[]) {
f[0] = sqrt(h[0]);//h[0]为完全平方数
ll inv2 = FastPow(2, mod - 2);
for (int len = 2, x = 2; (len >> 1) < n; len <<= 1, ++x) {
memset(invf, 0, sizeof(ll) *len);
Inv(len, invf, f);
for (int i = 0; i < len; ++i)a[i] = f[i], b[i] = h[i];
memset(a + len, 0, sizeof(ll) * len);
memset(b + len, 0, sizeof(ll) * len);
for (int i = 0; i < (len << 1); ++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (x - 1));
NTT(len << 1, a, 1); NTT(len << 1, b, 1); NTT(len << 1, invf, 1);
for (int i = 0; i < (len << 1); ++i)
f[i] = DEL(a[i], DEL(a[i] * a[i] % mod, b[i]) * invf[i] % mod * inv2 % mod);
NTT(len << 1, f, 0);
ll inv = FastPow(len << 1, mod - 2);
for (int i = 0; i < len; ++i)
f[i] = f[i] * inv % mod;
memset(f + len, 0, sizeof(ll) * len);
}
}

多项式(形式幂级数)对数

考虑$(\ln(f(x)))’=f’(x)\dfrac{1}{f(x)}$,求导很明显可以$O(n)$,求积分也是$O(n)$,求逆$O(n\log n)$,因此总复杂度是$O(n\log n)$。

1
2
3
4
5
6
7
8
9
10
void Ln(int n, ll f[], ll h[]) {
//求ln(h),答案放在f中,n为项数
Inv(n, f, h);
for (int i = 0; i < n - 1; ++i)b[i] = h[i + 1] * (i + 1) % mod;
for (int i = 0; i < n; ++i)a[i] = f[i];
Mul(n, n - 1);
f[0] = 0;
for (int i = 1; i < n; ++i)
f[i] = a[i - 1] * FastPow(i, mod - 2) % mod;
}

多项式(形式幂级数)指数

设$h(x)$是给定的形式幂级数,满足$[x^0]h(x)=0$,设$f(x)=\exp(h(x))$,则有$g(f(x))=\ln(f(x))-h(x)=0$,得到迭代形式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ll lnf[N];
void Exp(int n, ll f[], ll h[]) {
//求exp(h),答案放在f中,n为项数
f[0] = 1;
for (int len = 2, x = 2; (len >> 1) < n; len <<= 1, ++x) {
memset(lnf, 0, sizeof(ll) * len);
Ln(len, lnf, f);
for (int i = 0; i < len; ++i)a[i] = f[i], b[i] = h[i];
memset(a + len, 0, sizeof(ll) * len);
memset(b + len, 0, sizeof(ll) * len);
for (int i = 0; i < (len << 1); ++i)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (x - 1));
NTT(len << 1, a, 1); NTT(len << 1, b, 1); NTT(len << 1, lnf, 1);
for (int i = 0; i < (len << 1); ++i)
f[i] = a[i] * ADD(DEL(1, lnf[i]), b[i]) % mod;
NTT(len << 1, f, 0);
ll inv = FastPow(len << 1, mod - 2);
for (int i = 0; i < len; ++i)
f[i] = f[i] * inv % mod;
memset(f + len, 0, sizeof(ll) * len);
}
}

多项式(形式幂级数)快速幂

求$f^k(x)\pmod{x^n}$,等同于求$\exp(k\ln f)\pmod{x^n}$,当然这里需要保证$[x^0]f(x)=1$。

1
2
3
4
5
6
void Pow(int n, ll f[], ll h[], ll k) {
//求h^k,答案放在f中,n为项数
Ln(n, f, h);
for (int i = 0; i < n; ++i)h[i] = f[i] * k % mod;
Exp(n, f, h);
}