Typesetting math: 100%

多项式

多项式

多项式基础

数域的定义

定义 复数集的子集 K ,满足 0,1K 且元素的和差积商(除数不为 0 )都属于 K ,那么称 K 是一个数域。

关于群环域的详细定义可以看看抽代,这里只提及必须知道的。

例如有理数集、复数集、模素数 m 剩余系都是数域,但整数集不是数域。

多项式的定义与基本性质

定义a0,a1,,an 是数域 K 上的数,其中 n 为非负整数,那么称 f(x)=ni=0aixi 是数域 K 上的一元多项式,其中 aixif(x)i 次项,ai 则是 f(x)i 次项系数。

此外,也可以用 [xi]f(x) 表示多项式 f(x)i 次项系数。

注意,这里的 x 是形式上的记号,而非真正的数。换句话说,单独写出系数序列也能代表一个多项式, x 的次数更多时候只是用来区分系数。

一元多项式环的定义 数域 K 上多项式的全体,称为一元多项式环,记作 K[x] ,而 K 称为 K[x] 的系数域。

次数的定义 对于多项式 f(x) ,其系数非零的最高次项的次数称为多项式的次数,记作 (f(x))

相等的定义 若两个多项式对应次数的各系数均相等,那么这两个多项式相等。

零多项式的定义 系数全为 0 的多项式,记为 0 ,其次数不考虑。

多项式加法的定义 对于两个多项式 f(x)=ni=0aixi,g(x)=mi=0bixi ,则 f(x)+g(x)=max(n,m)i=0(ai+bi)xi

多项式乘法的定义 对于两个多项式 f(x)=ni=0aixi,g(x)=mi=0bixi ,则 f(x)g(x)=ni=0mj=0aibjxi+j

多项式乘法有一个等价形式,f(x)g(x)=n+mk=0xkki=0aibki ,即求两个多项式系数的加法卷积(下标之和相等的位置的值的乘积之和),这是今后许多问题可以转化为多项式计算的关键。

多项式复合的定义 对于两个多项式 f(x)=ni=0aixi,g(x)=mi=0bixi ,则 f(g(x))=a0+ni=1aigi(x)

性质1 数域 K 上的两个多项式经过加、减、乘等运算后,所得结果仍然是数域 K 上的多项式。

性质2 对于两个多项式 f(x),g(x) ,满足加法结合律、加法交换律、乘法结合律、乘法交换律、乘法对加法分配律、乘法消去律。

性质3 任意 n+1 个不同的采样点,就可以唯一确定一个 n 次多项式。

多项式带余式除法

定理1(带余式除法) 在一元多项式环 K[x] 中,任意两个多项式 A(x),B(x)B(x)0 ,一定存在唯一的两个多项式 Q(x),R(x) 满足 A(x)=Q(x)B(x)+R(x) ,其中 (R(x))<(B(x))R(x)=0 ,称 Q(x)A(x) 除以 B(x) 的商, R(x)A(x) 除以 B(x) 的余式。

大部分数论整除同余的性质都可以类似地应用到多项式上,后面就不展开了。

形式幂级数的定义

定义a0,a1,,an 是数域 K 上的数,那么称 f(x)=i=0aixi 是数域 K 上的形式幂级数,简称幂级数。

形式幂级数环的定义 数域 K 上形式幂级数的全体,称为形式幂级数环,记作 K[[x]]

幂级数可以看作是一元多项式的扩展,其具有更多良好的性质,如形式导数和形式不定积分等。

在模意义下,幂级数可等价为有限项的多项式,因此通常会把多项式扩展到幂级数上,借由幂级数的诸多性质得到许多有用的结论,例如模意义下多项式的初等函数。

幂级数的导数和不定积分

注意,极限在环上可能并不存在,但依然可以在形式上的定义导数与积分。

形式导数 设形式幂级数 f(x)=i=0aixi ,其形式导数 f(x)=i=1iaixi1

此外,我们也可将 f(x)t 阶导数记作 f(t)(x)

形式不定积分 设形式幂级数 f(x)=i=0aixi ,其形式不定积分 f(x)dx=i=1iaixi1+C

其他的基本求导法则皆可适用,就不再展开了。

常见幂级数展开

ex=i=01i!xisinx=i=0(1)i(2i+1)!x2i+1cosx=i=0(1)i(2i)!x2i11+x=i=0(1)ixi(1+x)α=i=0αii!xiln(1+x)=i=1(1)i1ixiarctanx=i=0(1)i2i+1x2i+1

多项式插值

多项式插值的定义

定义 给定 n 个点 (x1,y1),,(xn,yn) ,其中 xi 互不相同,求这些点确定的 n1 次多项式函数 f(x) 的过程,称为多项式插值。

多项式插值的方法

拉格朗日插值法

考虑构造 n 个辅助函数 gi(x)=yijixxjxixj ,显然 gi(x) 满足 gi(xi)=yigi(xj)=0,ji

因此我们令 f(x)=ni=1gi(x)=ni=1yijixxjxixj 即可唯一确定所求多项式,此为拉格朗日插值公式。

其中,若 xi=i ,可以预处理阶乘以及 xxj 的前后缀积,将公式化简为 O(n)

若我们只需要求出在 x=k 处的值,那么代入即可。

若要求出具体的系数则设计多项式运算的模拟。

这里只给出求单点值的代码。

时间复杂度 O(n2)

空间复杂度 O(n)

int lagrange_poly(const vector<pair<int, int>> &point, int x) {
int n = point.size() - 1;
int ans = 0;
for (int i = 1;i <= n;i++) {
int res1 = point[i].second;
int res2 = 1;
for (int j = 1;j <= n;j++) {
if (i == j) continue;
res1 = 1LL * res1 * (x - point[j].first + P) % P;
res2 = 1LL * res2 * (point[i].first - point[j].first + P) % P;
}
(ans += 1LL * res1 * qpow(res2, P - 2) % P) %= P;
}
return ans;
}

重心拉格朗日插值法

若插值点会新增 q 次,每次重新计算 f(k) 都是 O(n2) ,我们需要对公式做些调整。

f(x)=ni=1yijixxjxixj=ni=1(xxi)ni=1yi(xxi)ji(xixj)

我们设 A=ni=1(xxi),B(i)=ji(xixj)

每次新增插值点时 O(1) 更新 AO(n) 更新 B(i) 后,即可在 O(n) 内得到新的 f(k)=Ani=1yi(kxi)B(i)

代码可借鉴的拉格朗日插值法做修改,这里就不给出了。

时间复杂度 O(n2+qn)

空间复杂度 O(n)

加法卷积

加法卷积的定义

定义 对于两个序列 f,g ,它们的加法卷积序列 h 满足 hk=i+j=kfigj

把序列当作多项式系数理解, h 其实就是 f,g 表示的多项式的乘积的系数,因此可以用加法卷积的算法加速多项式乘积,下面也会用多项式的角度介绍加法卷积的算法。

加法卷积的变换

快速傅里叶变换(FFT)

多项式在系数域直接加法卷积是 O(n2) 的,但如果我们在若干个不同位置取两个多项式的点值(采样点),容易发现这些点值相乘后得到的新点值就落在所求的多项式上,最后只要把点值变换回系数,就得到目标多项式。

换句话说,系数域的加法卷积可以变换为点值域的对应相乘,而对应相乘这个过程是 O(n) 的,现在我们只需要一个能够快速在系数域和点值域之间变换算法即可。

这也是大多数变换加速卷积的核心思路,即找到一个快速的可逆变换,使得两个序列变换后的对应位置做运算的结果,恰好为两个序列卷积的变换,最后逆变换回去就是两个序列的卷积。

接下来就是加法卷积的需要的变换,离散傅里叶变换。

离散傅里叶变换(DFT)

首先,点值域的点不能随便取,我们要取 n 次单位根 ωnn 个幂 ω0n,ω1n,,ωn1nn 要大于等于多项式的项数。为了方便,我们通常需要令 n2 的幂。

n 次单位根等价于将复平面单位圆弧划分为 n 等分,其中第 k 份即 ωkn=cos2kπn+isin2kπn

单位根具有许多有用的性质:

  1. 互异性:若 ij ,则 ωinωjn
  2. 折半律:ω2i2n=ωin
  3. 周期律:ωi+nn=ωin
  4. 半周期律: ωi+n2n=ωin

互异性保证了 n 个点一定互不相同,接下来考虑如何求值。

f(x)=n1i=0aixi ,那么显然有 f(x)=a0+a2x2++an1xn1+x(a1+a3x2++anxn)

f1(x)=a0+a2x+an1xn1,f2(x)=a1+a3x++anxn ,那么有 f(x)=f1(x2)+xf2(x2)

i[0,n21] 时,我们代入单位根 ωin 可得

f(ωin)=f1((ωin)2)+ωinf2((ωin)2)=f1(ω2in)+ωinf2(ω2in)=f1(ωin2)+ωinf2(ωin2)

我们代入单位根 ωi+n2n 可得

f(ωi+n2n)=f1((ωi+n2n)2)+ωi+n2nf2((ωi+n2n)2)=f1(ω2in)ωinf2(ω2in)=f1(ωin2)ωinf2(ωin2)

注意到 f1(ωin2)f2(ωin2) 正是子问题的答案。

因此一个大小为 n 的问题,变成两个大小为 n2 的子问题外加 O(n) 复杂度计算,递归下去总体复杂度是 O(nlogn) 的。(如果随便取点,问题规模不会折半,也就不能快速了)

于是,多项式系数域到点值域的快速正变换就找到了。

位逆序置换

递归的常数较大,我们希望改为迭代,考虑 23 项多项式的变换过程:

  1. 初始层:{a0,a1,a2,a3,a4,a5,a6,a7}
  2. 第一层:{a0,a2,a4,a6},{a1,a3,a5,a7}
  3. 第二层:{a0,a4},{a2,a6},{a1,a5},{a3,a7}
  4. 第三层:{a0},{a4},{a2},{a6},{a1},{a5},{a3},{a7}

我们需要从下往上迭代,那么就需要知道最后一层的顺序。

容易知道,ai 最后会出现在 arevi ,其中 revi 表示 i 的二进制逆序,例如 110 的逆序就是 011

根据 rev 的定义,我们可以递推它在 n 项多项式的情况:

revi=revi22+[2i]2logn1

因此,我们预处理 rev 后,将对应系数置换即可迭代。

蝶形运算优化

在上面,当我们求出 f1(x)f2(x) 的各 n2 个点值后,设 i[0,n21] ,那么

f(ωin)=f1(ωin2)+ωinf2(ωin2)f(ωi+n2n)=f1(ωin2)ωinf2(ωin2)

注意到 f(ωin)f(ωi+n2n) 分别在 ii+n2 位置上,而 f1(ωin2)f2(ωin2) 也恰好在 ii+n2 位置上,因此我们不需要额外空间,只需要原地覆盖即可。

离散傅里叶逆变换(IDFT)

现在,我们尝试推导一下逆变换。

我们定义点值多项式 F(x)=n1i=0f(ωin)xi ,即 f(x) 点值当作系数的多项式。

我们代入 x=ωkn ,那么 F(ωkn)=n1i=0ωiknn1j=0ajωijn=n1j=0ajn1i=0ωi(k+j)n

构造辅助多项式 G(x)=n1i=0xi ,那么 F(ωkn)=n1j=0ajG(ωj+kn)

考虑 G(ωin) ,当 i=0G(ωin)=n ,否则单位根两两配对 G(ωin)=0

因此 F(ωkn)=nank ,特别地当 k=0F(ω0n)=na0 ,所以我们只需要对点值多项式进行一次DFT,随后将 [1,n1] 项反转,最后对所有项除以 n 即可还原多项式。

时间复杂度 O(nlogn)

空间复杂度 O(n)

const db PI = acos(-1.0);
vector<int> rev;
vector<complex<db>> Wn = { {0, 0}, {1, 0} }; // 0, w20, w40, w41, w80, w81, w82, w83, ...
void FFT(vector<complex<db>> &A, bool is_inv) {
int n = A.size();
if (rev.size() != n) {
int k = __builtin_ctz(n) - 1;
rev.resize(n);
for (int i = 0;i < n;i++) rev[i] = rev[i >> 1] >> 1 | (i & 1) << k;
}
for (int i = 0;i < n;i++) if (i < rev[i]) swap(A[i], A[rev[i]]);
if (Wn.size() < n) {
int k = Wn.size();
Wn.resize(n);
while (k < n) {
complex<db> w(cos(PI / k), sin(PI / k));
for (int i = k >> 1;i < k;i++) {
Wn[i << 1] = Wn[i];
Wn[i << 1 | 1] = Wn[i] * w;
}
k <<= 1;
}
}
for (int k = 1;k < n; k <<= 1) {
for (int i = 0;i < n;i += k << 1) {
for (int j = 0;j < k;j++) {
complex<db> u = A[i + j];
complex<db> v = A[i + k + j] * Wn[k + j];
A[i + j] = u + v;
A[i + k + j] = u - v;
}
}
}
if (is_inv) {
reverse(A.begin() + 1, A.end());
for (int i = 0;i < n;i++) A[i] /= n;
}
}
vector<complex<db>> poly_mul(vector<complex<db>> A, vector<complex<db>> B) {
if (A.empty() || B.empty()) return vector<complex<db>>();
int n = 1, sz = A.size() + B.size() - 1;
while (n < sz) n <<= 1;
A.resize(n);
B.resize(n);
FFT(A, 0);
FFT(B, 0);
for (int i = 0;i < n;i++) A[i] *= B[i];
FFT(A, 1);
A.resize(sz);
return A;
}

快速数论变换(NTT)

考虑在素域内的多项式变换,我们发现原根刚好能代替单位根。

考虑一个素数 P ,表达为 P=r2k+1 ,其原根 G 的阶为 φ(P)=P1=r2k ,当多项式含有 n=2k 项时,我们令 Gn=GP1n 那么 Gn 等价为 n 次单位根。

注意到,一个素数能支持的多项式长度为 2k ,因此 k 越大越好,不过常见的 109+7 就比较鸡肋,因为 k=1

NTT其他部分和FTT完全一致。

时间复杂度 O(nlogn)

空间复杂度 O(n)

const int P = 998244353, G = 3;
int qpow(int a, ll k) {
int ans = 1;
while (k) {
if (k & 1) ans = 1LL * ans * a % P;
k >>= 1;
a = 1LL * a * a % P;
}
return ans;
}
std::vector<int> rev;
std::vector<int> Wn = { 0,1 }; // 0, w20, w40, w41, w80, w81, w82, w83, ...
/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
explicit Poly(int n = 0, int val = 0) : std::vector<int>(n, val) {}
explicit Poly(const std::vector<int> &src) : std::vector<int>(src) {}
explicit Poly(const std::initializer_list<int> &src) : std::vector<int>(src) {}
Poly modx(int k) const {
assert(k >= 0);
if (k > size()) {
Poly X = *this;
X.resize(k);
return X;
}
else return Poly(std::vector<int>(begin(), begin() + k));
}
Poly mulx(int k) const {
assert(k >= 0 || -k < size());
Poly X = *this;
if (k >= 0) X.insert(X.begin(), k, 0);
else X.erase(X.begin(), X.begin() + (-k));
return X;
}
Poly derive(int k = 0) const {
if (k == 0) k = std::max((int)size() - 1, 0);
Poly X(k);
for (int i = 1;i < std::min((int)size(), k + 1);i++) X[i - 1] = 1LL * i * (*this)[i] % P;
return X;
}
Poly integral(int k = 0) const {
if (k == 0) k = size() + 1;
Poly X(k);
for (int i = 0;i < std::min((int)size(), k - 1);i++) X[i + 1] = 1LL * qpow(i + 1, P - 2) * (*this)[i] % P;
return X;
}
Poly &operator+=(const Poly &X) {
resize(std::max(size(), X.size()));
for (int i = 0;i < size();i++) ((*this)[i] += X[i]) %= P;
return *this;
}
Poly &operator-=(const Poly &X) {
resize(std::max(size(), X.size()));
for (int i = 0;i < size();i++) ((*this)[i] -= X[i] - P) %= P;
return *this;
}
Poly &operator*=(int x) {
for (int i = 0;i < size();i++) (*this)[i] = 1LL * (*this)[i] * x % P;
return *this;
}
Poly &operator/=(int x) {
int val = qpow(x, P - 2);
for (int i = 0;i < size();i++) (*this)[i] = 1LL * (*this)[i] * val % P;
return *this;
}
friend Poly operator+(Poly A, const Poly &B) { return A += B; }
friend Poly operator-(Poly A, const Poly &B) { return A -= B; }
friend Poly operator*(Poly A, int x) { return A *= x; }
friend Poly operator*(int x, Poly A) { return A *= x; }
friend Poly operator/(Poly A, int x) { return A /= x; }
friend Poly operator-(Poly A) { return (P - 1) * A; }
friend std::ostream &operator<<(std::ostream &os, const Poly &X) {
for (int i = 0;i < X.size();i++) os << X[i] << ' ';
return os;
}
static void NTT(Poly &X, bool is_inv) {
int n = X.size();
if (rev.size() != n) {
int k = __builtin_ctz(n) - 1;
rev.resize(n);
for (int i = 0;i < n;i++) rev[i] = rev[i >> 1] >> 1 | (i & 1) << k;
}
for (int i = 0;i < n;i++) if (i < rev[i]) std::swap(X[i], X[rev[i]]);
if (Wn.size() < n) {
int k = __builtin_ctz(Wn.size());
Wn.resize(n);
while (1 << k < n) {
int w = qpow(G, P - 1 >> k + 1);
for (int i = 1 << k - 1;i < 1 << k;i++) {
Wn[i << 1] = Wn[i];
Wn[i << 1 | 1] = 1LL * Wn[i] * w % P;
}
k++;
}
}
for (int k = 1;k < n; k <<= 1) {
for (int i = 0;i < n;i += k << 1) {
for (int j = 0;j < k;j++) {
int u = X[i + j];
int v = 1LL * X[i + k + j] * Wn[k + j] % P;
X[i + j] = (u + v) % P;
X[i + k + j] = (u - v + P) % P;
}
}
}
if (is_inv) {
std::reverse(X.begin() + 1, X.end());
int inv = qpow(n, P - 2);
for (int i = 0;i < n;i++) X[i] = 1LL * X[i] * inv % P;
}
}
Poly &operator*=(Poly X) {
if (empty() || X.empty()) return *this = Poly();
int n = 1, sz = size() + X.size() - 1;
while (n < sz) n <<= 1;
resize(n);
X.resize(n);
NTT(*this, 0);
NTT(X, 0);
for (int i = 0;i < n;i++) (*this)[i] = 1LL * (*this)[i] * X[i] % P;
NTT(*this, 1);
resize(sz);
return *this;
}
friend Poly operator*(Poly A, const Poly &B) { return A *= B; }
};

常用NTT模数:

r2k+1 r k g
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
75161927681 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 44 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898369 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054273 27 56 5
4179340454199820289 29 57 3

任意模数NTT(MTT)

暂时不学。

多项式初等函数

初等函数 公式 方法 备注
乘法 f(x)g(x) NTT/FTT
求逆 f1(x)f10(x)(2f10(x)f(x))(modxn) 牛顿迭代、乘法 常数项逆元存在
整除 [f(x)g(x)]RfR(x)g1R(x)(modxnm+1) 求逆 除式非零
取模 f(x)modg(x)=f(x)g(x)[f(x)g(x)] 整除 除式非零
开方 f(x)12((f(x))0+f(x)(f(x))10)(modxn) 牛顿迭代、求逆 首非零项是偶次项,且二次剩余存在
对数函数 lnf(x)f(x)f1(x)dx(modxn) 求逆 常数项为 1
指数函数 ef(x)(ef(x))0(1ln(ef(x))0+f(x))(modxn) 牛顿迭代、对数函数 常数项为 0
幂函数 fk(x)eklnf(x)(modxn) 指数函数
三角函数 欧拉公式 指数函数 常数项为 0
反三角函数 求导积分 开方 常数项为 0

多项式牛顿迭代

给定多项式 g(x) ,求 f(x) ,满足 g(f(x))0(modxn)

考虑倍增法。

n=1 时, [x0]g(f(x))=0 需要单独解出。

假设在模 xn2 时的 f(x) 的解是 f0(x) ,那么我们对 g(f(x))f0(x) 处泰勒展开有

i=0g(i)(f0(x))i!(f(x)f0(x))i0(modxn)

显然,当 i2 时, (f(x)f0(x))i0(modxn) ,因此有

i=0g(i)(f0(x))i!(f(x)f0(x))ig(f0(x))+g(f0(x))(f(x)f0(x)))0(modxn)

最后化简得

f(x)f0(x)g(f0(x))g(f0(x))(modxn)

这就是模意义下的牛顿迭代,每次都能倍增多项式有效系数,一些关键多项式初等函数需要由此推导。

xn 是因为精确解实际上大概率是幂级数,但大部分时候我们的操作只需要前几项,就能保证覆盖所有有意义的部分,因此幂级数是不必要的,求出模意义下的就够了。

多项式求逆

给定多项式 f(x) ,求 f1(x) ,满足 f(x)f1(x)1(modxn)

g(f1(x))=1f1(x)f(x)0(modxn)

n=1 时,[x0]f1(x)=([x0]f(x))1 ,因此需要保证常数项逆元存在。

假设模 xn2 的解为 f0(x)

根据牛顿迭代

f1(x)f10(x)g(f10(x))g(f10(x))f10(x)1f10(x)f(x)1f20(x)f10(x)(2f10(x)f(x))(modxn)

因此,我们可以用这个公式迭代出多项式的逆,复杂度由主定理 T(n)=T(n/2)+O(nlogn)=O(nlogn)

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly inv(int n = 0) const {
assert(size() && (*this)[0] > 0); // assert [x^0]f(x) inverse exists
if (n == 0) n = size();
Poly X{ qpow((*this)[0], P - 2) };
int k = 1;
while (k < n) {
k <<= 1;
X = (X * (Poly{ 2 } - X * modx(k))).modx(k);
}
return X.modx(n);
}
};

多项式除法&取模

给定多项式 f(x),g(x) ,求 q(x),r(x) ,满足 f(x)=q(x)g(x)+r(x)

其中 (q(x))=(f(x))(g(x)),(r(x))<(g(x))

n=(f(x)),m=(g(x)) ,不妨设 (r(x))=m1

因为存在余式,我们不能直接使用模 xm 的多项式求逆。

fR(x)=xnf(1x) ,其实就是将系数反转。

我们对原式变形

df(x)=q(x)g(x)+r(x)f(1x)=q(1x)g(1x)+r(1x)xnf(1x)=xnq(1x)g(1x)+xnr(1x)fR(x)=gR(x)qR(x)+xnm+1rR(x)

(qR(x))=(q(x))=nm<nm+1 ,因此在模 xnm+1qR(x) 是不会被影响的,而 xnm+1rR(x) 会被模掉。

所以有 fR(x)g1R(x)qR(x)(modxnm+1)

求出 qR(x) 后,反转系数就是 q(x) ,最后 r(x)=f(x)q(x)g(x)

实现上注意处理除式的后导 0 ,会导致结果出错,虽然一般题目不需要这个处理。

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly &operator/=(Poly X) {
while (X.size() && X.back() == 0) X.pop_back();
assert(X.size()); // assert X(x) is not 0-polynomial
if (size() < X.size()) return *this = Poly();
std::reverse(begin(), end());
std::reverse(X.begin(), X.end());
*this = (modx(size() - X.size() + 1) * X.inv(size() - X.size() + 1)).modx(size() - X.size() + 1);
std::reverse(begin(), end());
return *this;
}
Poly &operator%=(Poly X) {
while (X.size() && X.back() == 0) X.pop_back();
return *this = (*this - *this / X * X).modx(X.size() - 1);
}
friend Poly operator/(Poly A, const Poly &B) { return A /= B; }
friend Poly operator%(Poly A, const Poly &B) { return A %= B; }
};

多项式开方

给定多项式 f(x) ,求 f(x)modxn

g(f(x))=(f(x))2f(x)0(modxn)

n=1 时, [x0]f(x)=[x0]f(x) ,因此需要保证常数项二次剩余存在。

假设模 xn2 的解为 f0(x)

根据牛顿迭代

f(x)(f(x))0(f(x))20f(x)2(f(x))012((f(x))0+f(x)(f(x))10)(modxn)

代码没实现求二次剩余,目前只能对常数项为 1 的开方。

特别地,出现前导 0 时,前导 0 个数 cnt 是偶数(即第一个非零项是偶次)则多项式可以整体除以 xcnt 再开方,最后乘 xcnt/2 ,否则无解。

时间复杂度 O(nlogn)

空间复杂度 O(n)

struct Poly : public std::vector<int> {
Poly sqrt(int n = 0) const {
if (n == 0) n = size();
int cnt = 0;
while (cnt < size() && (*this)[cnt] == 0) cnt++;
if (cnt == size()) return Poly(n);
assert(!(cnt & 1) && (*this)[cnt] == 1); // assert cnt is even and [x^cnt]f(x) exists 2-residue
Poly X{ 1 };
int k = 1;
while (k < n) {
k <<= 1;
X = (P + 1 >> 1) * (X + mulx(-cnt).modx(k) * X.inv(k)).modx(k);
}
return X.mulx(cnt >> 1).modx(n);
}
};

多项式对数函数

给定多项式 f(x) ,求 lnf(x)modxn

求导再积分后, lnf(x)f(x)f(x)dx(modxn)

注意根据 ln 的定义, f(x) 的常数项必须为 1 ,否则对数无意义。

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly log(int n = 0) const {
assert(size() && (*this)[0] == 1); // assert [x^0]f(x) = 1
if (n == 0) n = size();
return (derive(n) * inv(n)).integral(n);
}
};

多项式指数函数

给定多项式 f(x) ,求 ef(x)modxn

g(ef(x))=lnef(x)f(x)0(modxn)

n=1 时,[x0]ef(x)=e[x0]f(x) ,因此需要保证常数项为 0 ,否则无意义。

假设模 xn2 的解为 f0(x)

根据牛顿迭代

ef(x)(ef(x))0ln(ef(x))0f(x)1(ef(x))0(ef(x))0(1ln(ef(x))0+f(x))(modxn)

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly exp(int n = 0) const {
assert(empty() || (*this)[0] == 0); // assert [x^0]f(x) = 0
if (n == 0) n = size();
Poly X{ 1 };
int k = 1;
while (k < n) {
k <<= 1;
X = (X * (Poly{ 1 } - X.log(k) + modx(k))).modx(k);
}
return X.modx(n);
}
};

多项式幂函数

给定多项式 f(x) ,求 fk(x)modxn

显然有 fk(x)eklnf(x)(modxn)

指数并非真正的指数,而是多项式函数的自变量,因此指数上的 k 也是对 P 取模。

[x0]f(x)1 时,我们找到第一个非零项 [xcnt]f(x) ,多项式可以整体除以 [xcnt]f(x)xcnt 再用上面的公式,最后乘 ([xcnt]f(x))kxkcnt ,其中 [xcnt]f(x)k 次方要模 φ(P) ,因为他是真正意义的指数。

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly pow(int k = 0, int n = 0) const {
if (n == 0) n = size();
int cnt = 0;
while (cnt < size() && (*this)[cnt] == 0) cnt++;
if (cnt == size()) return Poly(n);
if (1LL * k * cnt >= n) return Poly(n);
int k1 = k % P, k2 = k % (P - 1);
return ((k1 * (mulx(-cnt).modx(n) / (*this)[cnt]).log(n)).exp(n) * qpow((*this)[cnt], k2)).mulx(cnt * k1).modx(n);
}
Poly pow(const std::string &s, int n = 0) const {
if (n == 0) n = size();
int cnt = 0;
while (cnt < size() && (*this)[cnt] == 0) cnt++;
if (cnt == size()) return Poly(n);
int k1 = 0, k2 = 0;
for (auto ch : s) {
if ((1LL * 10 * k1 + ch - '0') * cnt >= n) return Poly(n);
k1 = (1LL * 10 * k1 % P + ch - '0') % P;
k2 = (1LL * 10 * k2 % (P - 1) + ch - '0') % (P - 1);
}
return ((k1 * (mulx(-cnt).modx(n) / (*this)[cnt]).log(n)).exp(n) * qpow((*this)[cnt], k2)).mulx(cnt * k1).modx(n);
}
};

多项式三角函数

给定多项式 f(x) ,求 sinf(x)modxncosf(x)modxn

考虑欧拉公式 eiθ=cosθ+isinθ

因此有 sinθ=eiθeiθ2i,cosθ=eiθ+eiθ2

θ=f(x) 即可,其中 i 在模 P 下等价于 gP14 ,注意 f(x) 常数项必须为 0 ,否则无意义。

此外,用这两个函数可以推导出其他三角函数(但有些无法在 0 处展开,且在其他位置展开都存在超越数,就不存在于这个模多项式体系了),这里就不一一列举了。

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly sin(int n = 0) const {
if (n == 0) n = size();
return ((I * modx(n)).exp(n) - (((P - I) % P) * modx(n)).exp(n)).modx(n) * qpow(2LL * I % P, P - 2);
}
Poly cos(int n = 0) const {
if (n == 0) n = size();
return ((I * modx(n)).exp(n) + (((P - I) % P) * modx(n)).exp(n)).modx(n) * qpow(2, P - 2);
}
};

多项式反三角函数

给定多项式 f(x) ,求 arcsinf(x)modxnarctanf(x)modxn

考虑求导再积分回去, arcsinf(x)=f(x)1f2(x)dx,arctanf(x)=f(x)1+f2(x)dx

为什么没有 arccos ?因为他的多项式常数是超越数,在这个体系无意义。上面能求的积分出来的常数是 0

时间复杂度 O(nlogn)

空间复杂度 O(n)

/// 有限域多项式板子(部分)
struct Poly : public std::vector<int> {
Poly asin(int n = 0) const {
if (n == 0) n = size();
return (derive(n) * (Poly{ 1 } - (modx(n) * modx(n))).sqrt(n).inv(n)).integral(n);
}
Poly atan(int n = 0) const {
if (n == 0) n = size();
return (derive(n) * (Poly{ 1 } + (modx(n) * modx(n))).inv(n)).integral(n);
}
};

位运算卷积

位运算卷积的定义

快速沃尔什变换(FWT)

异或卷积

或卷积

与卷积

子集卷积

子集卷积的定义

快速莫比乌斯变换(FMT)

并卷积

交卷积

子集卷积

多项式分治

多项式多点求值

多项式快速插值

分治加法卷积

多项式线性齐次递推

多项式平移

posted @   空白菌  阅读(352)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示