多项式 学习笔记
和计算几何一样是东西很多很杂而且都是板子的一个工具性专题。
东西太多了,只学了 zaky 板子里包含的东西。
题单可以看 这个 和 卡老师的遗产。
拉格朗日插值
用途:
- P4781 【模板】拉格朗日插值
题意:给
n 个点(x_i,y_i) ,求一个唯一的度数不大于n-1 的多项式过这n 个点。
朴素的做法是设出这
复杂度是
拉格朗日插值的导出有两种形式,一种在模意义下用 CRT 构造,一种纯代数,这里只讲后者。
我们对每个点
假如我们构造了
这个构造是不难的。由于
而
因此:
答案即为:
这个式子可以方便地代入一个值
复杂度
fo(i,1,n) {
ll cur = y[i], inv = 1;
fo(j,1,n) if(i != j) {
cur = cur * (k - x[j]) % P;
inv = inv * (x[i] - x[j]) % P;
} ans = (ans + cur * power(inv, P - 2) % P) % P;
} ans = (ans % P + P) % P;
注意如果处理不当,拉格朗日插值得到的答案很可能是负数,一定要 ans = (ans % P + P) % P;!!!
考虑一些特殊的应用:
- 给出的
n 个点坐标连续,即保证x_i=i 。
这时式子变成:
我们预处理
这样就可以
- 每次插入一个点。
容易发现这个拉格朗日插值法没有办法支持动态插一个点。
我们整理一下式子:
其实每个部分都不难维护了。设
每次加入一个点可以
这被称为重心拉格朗日插值法。
- 需要求出每一项的系数。
其实不是很难,我们计算
暴力卷积预处理出
复杂度
一般解题过程中,如果可以证明答案是一个
- CF622F The Sum of the k-th Powers
显然这是一个
求这
求完了就是坐标连续插值的板子了。
快速傅里叶变换
用途:
离散傅里叶变换
所谓离散傅里叶变换(Discrete Fourier Transform, DFT),在多项式意义下就是把系数表示的多项式转为点值表示。
反之,IDFT 就是把点值表示的多项式转为系数表示。
为什么计算两多项式相乘要转成点值表示?因为多项式相乘可以直接转化为点值相乘。具体地:
十分漂亮。
由于点值表示的多项式对点的位置没有特殊要求,所以我们会选取若干特殊的
我们取一个
这个东西有什么好处呢?我们发现,如果把最终答案系数
这意味着我们只需要求出左边那个矩阵(这被称为傅里叶矩阵
事实上,我们将
这意味着,我们直接代入
分治优化
怎么进行 DFT 的过程呢?我们考虑分治:
对于一个
具体地,设
容易发现
然后,我们对
于是就可以
常数优化与实现
但是原始写法常数太大,我们优化一下。
-
我们提前记录好所有的
\omega_n^k 和\omega_n^{-k} 传入。 -
由于这个交换是有迹可寻的:每个数最后的位置在它二进制位相反的位置上,所以我们可以先交换好再还原。
这个过程被称为蝴蝶变换。
-
我们还可以在一个数组中操作,省去赋值的时间。
注意细节:
- 数组从
0 开始标号,不要打错! -
- 不要忘记预处理单位根,以及最后除以
n 。 - 个人习惯,不卡空间的话数组一般开
3 倍,即最后答案的\dfrac{3}{2} 。
https://duck.ac/problem/1002 代码:
#include<bits/stdc++.h>
using namespace std;
#define fo(v,a,b) for(int v = a; v <= b; v++)
#define fr(v,a,b) for(int v = a; v >= b; v--)
#define cl(a,v) memset(a, v, sizeof(a))
const int N = 3000000;
typedef double db;
typedef complex<db> cp;
inline int read()
{
char c=getchar();int x=0,f=1;
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int n, L, len1, len2;
void FFT(cp *a, cp *omg) {
fo(i,0,n-1) {
int t = 0;
fo(j,0,L-1) if(i >> j & 1) t |= (1 << (L - 1 - j));
if(t > i) swap(a[i], a[t]);
}
for(int l = 2; l <= n; l <<= 1) {
int m = l / 2;
for(cp *p = a; p != a + n; p += l)
fo(i,0,m-1) {
cp t = p[i + m] * omg[n / l * i];
p[i + m] = p[i] - t, p[i] += t;
}
}
}
cp a[N], b[N], c[N], omg[N], inv[N];
void poly_multiply(unsigned *v1, int len1, unsigned *v2, int len2, unsigned *v3)
{
fo(i,0,len1) a[i].real(v1[i]);
fo(i,0,len2) b[i].real(v2[i]);
n = 1; while(n <= len1 + len2) n <<= 1, L++;
fo(i,0,n-1) {
omg[i] = cp(cos(2.0 * M_PI * i / n), sin(2.0 * M_PI * i / n));
inv[i] = conj(omg[i]);
}
FFT(a, omg), FFT(b, omg);
fo(i,0,n-1) c[i] = a[i] * b[i];
FFT(c, inv);
fo(i,0,len1 + len2) v3[i] = (unsigned) floor(c[i].real() / n + 0.5);
}
FFT 的一个主要的运用是求离散卷积,即求:
- P3338 [ZJOI2014]力
把式子分成左右两边分别计算即可。
和卷积与差卷积
有的时候要计算的卷积不是传统的
这被叫做差卷积。
这时可以尝试把
再把
类似地还有和卷积:
这会我们有
快速数论变换
用途:带模数的卷积。
单位根与原根
NTT 是 FFT 在
具体地,我们给 FFT 在
比如,
这意味着
而
实现时可以预处理好所有蝴蝶变换的值,减小常数。
现在开始,我们要尝试对多项式板子做一个封装了。
这个板子在文章的最后会展示,所以后面的东西都不会放代码。
分治 FFT / NTT
一个技巧。
一种形式是:我们要计算
由于对于长度分别为
另一种形式是:我们有自己向自己的卷积形式的递推,如P4721 【模板】分治 FFT。
做法也很显然,就是 CDQ 分治。
void solve(int l, int r) {
if(l == r) return ;
int mid = (l + r) >> 1;
solve(l, mid);
A.reset(), A.resize(mid - l + 1);
B.reset(), B.resize(r - l + 1);
fo(i,l,mid) A[i - l] = f[i];
fo(i,0,r-l) B[i] = g[i];
A = A * B;
fo(i,mid+1,r) f[i] = (f[i] + A[i - l]) % P;
solve(mid + 1, r);
}
任意模数 NTT
做法是不难的,过程就是取若干模数进行 NTT(一般三个),保证它们的乘积大于答案。
然后 CRT 合并,容易发现可以确定出唯一答案,这样边做边取模即可。
这个合并可以直接 __int128 搞,也可以先合并前两个再快速乘搞出答案。
多项式求逆
考虑分治。
由于
如果我们已经求出
平方得到:
因此新的逆元满足:
可以直接递归 / 递推计算。
复杂度的话,由于我们每次只需要保留
得到
注意实现时可以少做几次 NTT 减小常数。
多项式指数函数 / 对数函数
泰勒级数与麦克劳林级数
我们先大致了解一下泰勒级数和麦克劳林级数,由于是大致了解所以不会写出什么严谨过程。
我们知道,对任意一个可导的函数
类似地,我们找到一个最接近
将其推广,我们找到了一个用
这被称为泰勒多项式,实际使用时可以取前几项作为近似。
而如果把
如果上面这个式子和
判断泰勒级数在
其中
比如,我们可以给
那么
由于
对于
我们考虑曲线救国,对
归纳可得:
所以有:
形式出奇地简单!
具体的证收敛过程不会,反正在
在多项式上的推广
我们考虑把这个定义推广到多项式上。对于一个多项式
代入
对数函数在模意义下收敛当且仅当
牛顿迭代法
求
给定多项式
g(x) ,求f(x) 使x^n\mid g(f(x)) 。
可以分治。
设我们已经找到
由于
整理一下:
这个形式被叫作牛顿迭代法,作
多项式 exp / ln 的求法
考虑求多项式
由于
有递推式:
好像走不通?
其实有一个更好用的性质:
因此:
可以
再考虑求多项式
在保证
多项式开方
依然采用牛顿迭代法,令
多项式快速幂
由于
这意味着
否则怎么办呢?我们可以考虑:
这适用于
板子
由于
#include<bits/stdc++.h>
using namespace std;
#define fo(v,a,b) for(int v = a; v <= b; v++)
#define fr(v,a,b) for(int v = a; v >= b; v--)
#define cl(a,v) memset(a, v, sizeof(a))
typedef long long ll;
const ll P = 998244353, gP = 3;
const int N = 600000;
ll power(ll a, ll x) {
ll res = 1;
while(x) {
if(x & 1LL) res = res * a % P;
a = a * a % P, x >>= 1;
} return res;
}
inline ll inv(ll x) { return power(x, P - 2); }
int Len, lg[N], Trans[2 * N], TransPos, Beg[30];
ll omgVal[N], invVal[N], numInv[N];
struct Poly
{
vector<ll> v;
// Basic Operations
int siz() { return (int)v.size(); }
ll &operator[](int x) { return v[x]; }
ll vv(int x) { return x < 0 || x >= (int)v.size() ? 0 : v[x]; }
void reset() { v.clear(); }
void resize(int x) { v.resize(x); }
Poly(vector<ll> vec = {0}) { v = vec; }
// Linear Operations
friend Poly operator*(Poly A, ll c) {
for(ll &x : A.v) x = x * c % P;
return A;
}
friend Poly operator*(ll c, Poly A) {
return A * c;
}
friend Poly operator+(Poly A, Poly B) {
Poly res; res.resize(max(A.siz(), B.siz()));
fo(i,0,res.siz()-1) res[i] = (A.vv(i) + B.vv(i)) % P;
return res;
}
friend Poly operator-(Poly A, Poly B) {
Poly res; res.resize(max(A.siz(), B.siz()));
fo(i,0,res.siz()-1) res[i] = (A.vv(i) + P - B.vv(i)) % P;
return res;
}
Poly& operator+=(Poly A) { return (*this) = (*this) + A; }
Poly& operator-=(Poly A) { return (*this) = (*this) - A; }
Poly& operator*=(Poly A) { return (*this) = (*this) * A; }
// Convolution
void NTT(ll *omg) {
int n = v.size(), L = lg[n];
if(n != (1 << L)) L++, v.resize(n = (1 << L));
assert(n <= Len);
fo(i,0,n-1) if(Trans[Beg[L] + i] > i) swap(v[i], v[Trans[Beg[L] + i]]);
for(int l = 2; l <= n; l <<= 1) {
int m = l / 2, rate = Len / l;
for(vector<ll>::iterator p = v.begin(); p != v.end(); p += l)
fo(i,0,m-1) {
ll cur = p[i + m] * omg[rate * i] % P;
p[i + m] = (p[i] + P - cur) % P, p[i] = (p[i] + cur) % P;
}
}
}
friend Poly operator*(Poly iA, Poly iB) {
Poly A = iA, B = iB;
int cur = A.siz() + B.siz() - 1, mx = 1;
while(mx <= cur) mx <<= 1;
A.resize(mx), B.resize(mx), A.NTT(omgVal), B.NTT(omgVal);
ll iv = inv(mx); fo(i,0,mx-1) A.v[i] = A.v[i] * B.v[i] % P;
A.NTT(invVal); fo(i,0,mx-1) A.v[i] = A.v[i] * iv % P;
return A.resize(mx), A;
}
// Inversion
Poly Inv() { // 0.2 s
Poly F, G; G.resize(1), G[0] = inv(v[0]);
for(int m = 2, pm = 4; m / 2 < (int)v.size(); m <<= 1, pm <<= 1) {
F.reset(), F.resize(pm), G.resize(pm);
fo(i,0,m-1) F[i] = this->vv(i);
G.NTT(omgVal), F.NTT(omgVal);
fo(i,0,pm-1) G[i] = G[i] * (P + 2 - G[i] * F[i] % P) % P;
G.NTT(invVal); ll invm = inv(pm);
fo(i,0,pm-1) G[i] = G[i] * invm % P;
G.resize(m);
} return G.resize(v.size()), G;
}
// Derivative & Integral
Poly deriv() {
Poly res; res.resize(v.size() - 1);
fo(i,1,(int)v.size()-1) res[i - 1] = v[i] * i % P;
return res;
}
Poly integ() {
Poly res; res.resize(v.size() + 1);
fo(i,0,(int)v.size()-1) res[i + 1] = v[i] * numInv[i + 1] % P;
return res;
}
// exp & ln
Poly Ln() { // 0.2 s
assert(v[0] == 1);
Poly res = ((this -> deriv()) * (this -> Inv())).integ();
return res.resize(v.size()), res;
}
Poly Exp() { // 0.5 s
assert(v[0] == 0); Poly res({1});
for(int m = 2, pm = 4; m / 2 < (int)v.size(); m <<= 1, pm <<= 1) {
Poly f; res.resize(m), f.resize(m);
fo(i,0,m-1) f[i] = this->vv(i);
res = res * (Poly({1}) - res.Ln() + f);
res.resize(m);
} return res.resize(v.size()), res;
}
// Square Root : 0.7 s
Poly Sqrt() {
assert(v[0] == 1); Poly res({1}); ll Inv2 = (P + 1) / 2;
for(int m = 2, pm = 4; m / 2 < (int)v.size(); m <<= 1, pm <<= 1) {
Poly f; res.resize(pm), f.resize(pm);
fo(i,0,m-1) f[i] = this->vv(i);
res = (res + f * res.Inv()) * Inv2;
res.resize(m);
} return res.resize(v.size()), res;
}
// Quick Pow : 0.7 s
Poly Shift(int x) { // x^{n} -> x^{n+k}
if(v.size() + x <= 0) return Poly();
Poly res; res.resize(v.size() + x);
fo(i,0,(int)v.size()-1) if(i + x >= 0) res[i + x] = v[i];
return res;
}
Poly Power_1(ll k) {
Poly res = (k * (this->Ln()));
res = res.Exp();
return res;
}
Poly Power_not0(ll k, ll k2) { // the real k mod P, k mod (P - 1)
Poly res(v); ll val = inv(v[0]);
res = res * val, res = res.Power_1(k);
return res = res * power(v[0], k2), res;
}
Poly Power(ll k, ll k2, bool fl = true) { // fl is (if the real k < P)
if(v[0]) return Power_not0(k, k2);
Poly res({0}); if(!fl) return res.resize(v.size()), res;
int c = -1; fo(i,1,(int)v.size()-1) if(v[i]) { c = i; break; }
assert(c != -1);
if(c * k >= (int)v.size())
return res = Poly(), res.resize(v.size()), res;
res = (this->Shift(-c)).Power_not0(k, k2), res = res.Shift(c * k);
return res.resize(v.size()), res;
}
} ;
void Pinit(int x) {
Len = 1; while(Len <= x) Len <<= 1;
fo(i,2,Len) lg[i] = lg[i >> 1] + 1;
fo(j,0,lg[Len]) {
Beg[j] = TransPos;
fo(i,0,(1<<j)-1) {
fo(now,0,j-1) if(i >> now & 1) Trans[TransPos] |= (1 << (j - 1 - now));
TransPos++;
}
}
omgVal[0] = invVal[0] = numInv[1] = 1;
ll Sg = power(gP, (P - 1) / Len), Sinvg = inv(Sg);
fo(i,1,Len-1) {
omgVal[i] = omgVal[i - 1] * Sg % P;
invVal[i] = invVal[i - 1] * Sinvg % P;
}
fo(i,2,Len) numInv[i] = numInv[P % i] * (P - P / i) % P;
}
int n; Poly A;
int main()
{
cin >> n, A.resize(n), Pinit(4 * n + 2);
return 0;
}
/*
在 Z_{998244353} 中的一个多项式板子,包含:
- 线性运算
- NTT & 卷积
- 求逆
- 导数 & 积分
- ln & exp
- 开平方
- 快速幂
上面标了对于 n = 10^5 在洛谷上的表现(含 IO 耗时),不得不说常数还是不小的。
*/
终于完结啦!!!
(真的吗?)
坑点一览
场上打 vector 的多项式板子还是有些困难的,把寄过的点都写这吧。
- 写板子顺序:NTT,乘法,求逆,
\ln/\exp 。 - NTT 中的蝴蝶变换,注意是
if(t > i) swap(v[i], v[t]);不是if(t > i) swap(i, t);!!! vector也会数组越界,务必小心。- 多项式求逆里的几个东西不要写反。
- 牛迭时所有东西都要记得
resize到足够的大小!!! - 和差卷积时记得
resize到原大小。 - 数组要开
4 倍! - 求逆如果用展开的写法,记得数组要开成
2m !!!