多项式
多项式
多项式基础
数域的定义
定义 复数集的子集 K ,满足 0,1∈K 且元素的和差积商(除数不为 0 )都属于 K ,那么称 K 是一个数域。
关于群环域的详细定义可以看看抽代,这里只提及必须知道的。
例如有理数集、复数集、模素数 m 剩余系都是数域,但整数集不是数域。
多项式的定义与基本性质
定义 设 a0,a1,⋯,an 是数域 K 上的数,其中 n 为非负整数,那么称 f(x)=n∑i=0aixi 是数域 K 上的一元多项式,其中 aixi 是 f(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)=n∑i=0aixi,g(x)=m∑i=0bixi ,则 f(x)+g(x)=max(n,m)∑i=0(ai+bi)xi 。
多项式乘法的定义 对于两个多项式 f(x)=n∑i=0aixi,g(x)=m∑i=0bixi ,则 f(x)⋅g(x)=n∑i=0m∑j=0aibjxi+j 。
多项式乘法有一个等价形式,f(x)⋅g(x)=n+m∑k=0xkk∑i=0aibk−i ,即求两个多项式系数的加法卷积(下标之和相等的位置的值的乘积之和),这是今后许多问题可以转化为多项式计算的关键。
多项式复合的定义 对于两个多项式 f(x)=n∑i=0aixi,g(x)=m∑i=0bixi ,则 f(g(x))=a0+n∑i=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=1iaixi−1 。
此外,我们也可将 f(x) 的 t 阶导数记作 f(t)(x) 。
形式不定积分 设形式幂级数 f(x)=∞∑i=0aixi ,其形式不定积分 ∫f(x)dx=∞∑i=1iaixi−1+C 。
其他的基本求导法则皆可适用,就不再展开了。
常见幂级数展开
多项式插值
多项式插值的定义
定义 给定 n 个点 (x1,y1),⋯,(xn,yn) ,其中 xi 互不相同,求这些点确定的 n−1 次多项式函数 f(x) 的过程,称为多项式插值。
多项式插值的方法
拉格朗日插值法
考虑构造 n 个辅助函数 gi(x)=yi∏j≠ix−xjxi−xj ,显然 gi(x) 满足 gi(xi)=yi 且 gi(xj)=0,j≠i 。
因此我们令 f(x)=n∑i=1gi(x)=n∑i=1yi∏j≠ix−xjxi−xj 即可唯一确定所求多项式,此为拉格朗日插值公式。
其中,若 xi=i ,可以预处理阶乘以及 x−xj 的前后缀积,将公式化简为 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)=n∑i=1yi∏j≠ix−xjxi−xj=n∏i=1(x−xi)n∑i=1yi(x−xi)∏j≠i(xi−xj) 。
我们设 A=n∏i=1(x−xi),B(i)=∏j≠i(xi−xj) 。
每次新增插值点时 O(1) 更新 A , O(n) 更新 B(i) 后,即可在 O(n) 内得到新的 f(k)=An∑i=1yi(k−xi)B(i) 。
代码可借鉴的拉格朗日插值法做修改,这里就不给出了。
时间复杂度 O(n2+qn)
空间复杂度 O(n)
加法卷积
加法卷积的定义
定义 对于两个序列 f,g ,它们的加法卷积序列 h 满足 hk=∑i+j=kfigj 。
把序列当作多项式系数理解, h 其实就是 f,g 表示的多项式的乘积的系数,因此可以用加法卷积的算法加速多项式乘积,下面也会用多项式的角度介绍加法卷积的算法。
加法卷积的变换
快速傅里叶变换(FFT)
多项式在系数域直接加法卷积是 O(n2) 的,但如果我们在若干个不同位置取两个多项式的点值(采样点),容易发现这些点值相乘后得到的新点值就落在所求的多项式上,最后只要把点值变换回系数,就得到目标多项式。
换句话说,系数域的加法卷积可以变换为点值域的对应相乘,而对应相乘这个过程是 O(n) 的,现在我们只需要一个能够快速在系数域和点值域之间变换算法即可。
这也是大多数变换加速卷积的核心思路,即找到一个快速的可逆变换,使得两个序列变换后的对应位置做运算的结果,恰好为两个序列卷积的变换,最后逆变换回去就是两个序列的卷积。
接下来就是加法卷积的需要的变换,离散傅里叶变换。
离散傅里叶变换(DFT)
首先,点值域的点不能随便取,我们要取 n 次单位根 ωn 的 n 个幂 ω0n,ω1n,⋯,ωn−1n ,n 要大于等于多项式的项数。为了方便,我们通常需要令 n 为 2 的幂。
n 次单位根等价于将复平面单位圆弧划分为 n 等分,其中第 k 份即 ωkn=cos2kπn+isin2kπn 。
单位根具有许多有用的性质:
- 互异性:若 i≠j ,则 ωin≠ωjn 。
- 折半律:ω2i2n=ωin 。
- 周期律:ωi+nn=ωin 。
- 半周期律: ωi+n2n=−ωin 。
互异性保证了 n 个点一定互不相同,接下来考虑如何求值。
设 f(x)=n−1∑i=0aixi ,那么显然有 f(x)=a0+a2x2+⋯+an−1xn−1+x(a1+a3x2+⋯+anxn) 。
设 f1(x)=a0+a2x+⋯an−1xn−1,f2(x)=a1+a3x+⋯+anxn ,那么有 f(x)=f1(x2)+xf2(x2) 。
当 i∈[0,n2−1] 时,我们代入单位根 ωin 可得
我们代入单位根 ωi+n2n 可得
注意到 f1(ωin2) 和 f2(ωin2) 正是子问题的答案。
因此一个大小为 n 的问题,变成两个大小为 n2 的子问题外加 O(n) 复杂度计算,递归下去总体复杂度是 O(nlogn) 的。(如果随便取点,问题规模不会折半,也就不能快速了)
于是,多项式系数域到点值域的快速正变换就找到了。
位逆序置换
递归的常数较大,我们希望改为迭代,考虑 23 项多项式的变换过程:
- 初始层:{a0,a1,a2,a3,a4,a5,a6,a7} 。
- 第一层:{a0,a2,a4,a6},{a1,a3,a5,a7} 。
- 第二层:{a0,a4},{a2,a6},{a1,a5},{a3,a7} 。
- 第三层:{a0},{a4},{a2},{a6},{a1},{a5},{a3},{a7} 。
我们需要从下往上迭代,那么就需要知道最后一层的顺序。
容易知道,ai 最后会出现在 arevi ,其中 revi 表示 i 的二进制逆序,例如 110 的逆序就是 011 。
根据 rev 的定义,我们可以递推它在 n 项多项式的情况:
因此,我们预处理 rev 后,将对应系数置换即可迭代。
蝶形运算优化
在上面,当我们求出 f1(x) 和 f2(x) 的各 n2 个点值后,设 i∈[0,n2−1] ,那么
注意到 f(ωin) 和 f(ωi+n2n) 分别在 i 和 i+n2 位置上,而 f1(ωin2) 和 f2(ωin2) 也恰好在 i 和 i+n2 位置上,因此我们不需要额外空间,只需要原地覆盖即可。
离散傅里叶逆变换(IDFT)
现在,我们尝试推导一下逆变换。
我们定义点值多项式 F(x)=n−1∑i=0f(ωin)xi ,即 f(x) 点值当作系数的多项式。
我们代入 x=ωkn ,那么 F(ωkn)=n−1∑i=0ωiknn−1∑j=0ajωijn=n−1∑j=0ajn−1∑i=0ωi(k+j)n 。
构造辅助多项式 G(x)=n−1∑i=0xi ,那么 F(ωkn)=n−1∑j=0ajG(ωj+kn) 。
考虑 G(ωin) ,当 i=0 时 G(ωin)=n ,否则单位根两两配对 G(ωin)=0 。
因此 F(ωkn)=nan−k ,特别地当 k=0 时 F(ω0n)=na0 ,所以我们只需要对点值多项式进行一次DFT,随后将 [1,n−1] 项反转,最后对所有项除以 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=r⋅2k+1 ,其原根 G 的阶为 φ(P)=P−1=r⋅2k ,当多项式含有 n=2k′ 项时,我们令 Gn=GP−1n 那么 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模数:
r⋅2k+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 | |
求逆 | f−1(x)≡f−10(x)(2−f−10(x)f(x))(modxn) | 牛顿迭代、乘法 | 常数项逆元存在 |
整除 | [f(x)g(x)]R≡fR(x)g−1R(x)(modxn−m+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)f−1(x)dx(modxn) | 求逆 | 常数项为 1 |
指数函数 | ef(x)≡(ef(x))0(1−ln(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 需要单独解出。
假设在模 x⌈n2⌉ 时的 f(x) 的解是 f0(x) ,那么我们对 g(f(x)) 在 f0(x) 处泰勒展开有
显然,当 i≥2 时, (f(x)−f0(x))i≡0(modxn) ,因此有
最后化简得
这就是模意义下的牛顿迭代,每次都能倍增多项式有效系数,一些关键多项式初等函数需要由此推导。
模 xn 是因为精确解实际上大概率是幂级数,但大部分时候我们的操作只需要前几项,就能保证覆盖所有有意义的部分,因此幂级数是不必要的,求出模意义下的就够了。
多项式求逆
给定多项式 f(x) ,求 f−1(x) ,满足 f(x)f−1(x)≡1(modxn) 。
设 g(f−1(x))=1f−1(x)−f(x)≡0(modxn) 。
当 n=1 时,[x0]f−1(x)=([x0]f(x))−1 ,因此需要保证常数项逆元存在。
假设模 x⌈n2⌉ 的解为 f0(x) 。
根据牛顿迭代
因此,我们可以用这个公式迭代出多项式的逆,复杂度由主定理 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))=m−1。
因为存在余式,我们不能直接使用模 xm 的多项式求逆。
设 fR(x)=xnf(1x) ,其实就是将系数反转。
我们对原式变形
有 ∂(qR(x))=∂(q(x))=n−m<n−m+1 ,因此在模 xn−m+1 下 qR(x) 是不会被影响的,而 xn−m+1rR(x) 会被模掉。
所以有 fR(x)⋅g−1R(x)≡qR(x)(modxn−m+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))2−f(x)≡0(modxn) 。
当 n=1 时, [x0]√f(x)=√[x0]f(x) ,因此需要保证常数项二次剩余存在。
假设模 x⌈n2⌉ 的解为 f0(x) 。
根据牛顿迭代
代码没实现求二次剩余,目前只能对常数项为 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 ,否则无意义。
假设模 x⌈n2⌉ 的解为 f0(x) 。
根据牛顿迭代
时间复杂度 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))k⋅xk⋅cnt ,其中 [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)modxn 和 cosf(x)modxn 。
考虑欧拉公式 eiθ=cosθ+isinθ 。
因此有 sinθ=eiθ−e−iθ2i,cosθ=eiθ+e−iθ2 。
令 θ=f(x) 即可,其中 i 在模 P 下等价于 gP−14 ,注意 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)modxn 和 arctanf(x)modxn 。
考虑求导再积分回去, arcsinf(x)=∫f′(x)√1−f2(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)
并卷积
交卷积
子集卷积
多项式分治
多项式多点求值
多项式快速插值
分治加法卷积
多项式线性齐次递推
多项式平移
本文来自博客园,作者:空白菌,转载请注明原文链接:https://www.cnblogs.com/BlankYang/p/18063076
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧