算法学习笔记:多项式
多项式基础
为了与幂次相对应,下文中系数均采用
多项式的表示方法
我们习惯于将一个
我们不妨考虑代入
既然这两种表示法都可以唯一确定一个多项式,那么必然可以相互转化。具体地,系数表示法转化为点值表示法的过程就是求值,点值表示法转化为系数表示法的过程就是插值。
拉格朗日插值
我们简单研究一下插值。一个显然的方法就是
现在给出
拉格朗日插值的思想就是构造
因此
实现时可以
Luogu P4781 【模板】拉格朗日插值 只要求单点插值,直接带入拉格朗日插值公式计算即可,依然是
横坐标是连续整数的快速单点插值
若
设
注意到分子是经典的去除一个单点的形式,考虑预处理前后缀积,即令
预处理阶乘逆元即可
快速傅里叶变换
快速傅里叶变换(FFT)是多项式的基石。这里默认读者具有良好的线性代数与复数知识储备。
离散傅里叶变换
离散傅里叶变换(DFT)是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。在 OI 中,其恰好能用来将多项式的系数表示法转化为点值表示法。
序列
若将
通常以符号
也记作
容易发现,离散傅里叶变换可以看作一个
分治实现 FFT
FFT 是一种高效实现 DFT 的算法,不过其中的一些细节又与 DFT 略有区别,后文将会提及。
我们的目标就是,在可接受的时间复杂度下,快速求出一个
FFT 的核心思想就是分治。我们考虑
这样并不能得到很好的复杂度,因为
此时
接下来,由偶函数和奇函数的性质,考虑代入
再利用
我们以
注意上述过程都是在
我们还需要支持复数运算,可以手写结构体,也可以使用 STL complex
。
分治实现的代码:
void FFT(complex<double> *a, int n) {
if (n == 1) return;
int mid = n >> 1;
for (int i = 0; i < n; i += 2)
tmp[i >> 1] = a[i], tmp[(i >> 1) + mid] = a[i + 1];
for (int i = 0; i < n; ++i) a[i] = tmp[i];
FFT(a, mid, tp), FFT(a + mid, mid, tp);
w[0] = { 1, 0 }, w[1] = { cos(PI * 2 / n), sin(PI * 2 / n) };
for (int i = 2; i < mid; ++i) w[i] = w[i - 1] * w[1];
for (int i = 0; i < mid; ++i)
tmp[i] = a[i] + w[i] * a[i + mid],
tmp[i + mid] = a[i] - w[i] * a[i + mid];
for (int i = 0; i < n; ++i) a[i] = tmp[i];
}
倍增实现 FFT/蝴蝶变换
分治实现 FFT 需要递归处理,效率较慢,我们希望找到非递归的实现方式。
考察 FFT 对系数位置的变换。对于一个规模为
显然
我们在一开始就做一次位逆序置换,即令
此时,
我们理一下算法的过程:先对系数数组做位逆序置换,然后枚举问题规模
由
时间复杂度还是
蝴蝶变换的代码:
void FFT(complex<double> *a, int n) {
int mid = n >> 1;
for (int i = 1; i < n; ++i) {
rev[i] = (rev[i >> 1] >> 1) + (i & 1 ? mid : 0);
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
for (int k = 1; k < n; k <<= 1) {
w[0] = { 1, 0 }, w[1] = { cos(PI / k), sin(PI / k) };
for (int i = 2; i < k; ++i) w[i] = w[i - 1] * w[1];
for (int i = 0; i < n; i += k << 1) for (int j = 0; j < k; ++j) {
complex<double> x = a[i | j], y = w[j] * a[i | j | k];
a[i | j] = x + y, a[i | j | k] = x - y;
}
}
}
多项式求值和线性代数
在讲解快速傅里叶逆变换之前,我们先讲一下多项式求值和线性代数之间的关系。
其实多项式求值很显然可以用矩阵乘法描述。例如,把多项式
左侧的矩阵就是范德蒙德矩阵。
那么 FFT 也可以用这种方式表示:
快速傅里叶逆变换
设 FFT 中的范德蒙德矩阵为
由前面的矩阵乘法表示,容易想到逆变换就是左乘上这个矩阵的逆矩阵
考虑利用拉格朗日插值公式求逆。观察拉格朗日插值公式,容易得出
分子和分母都是
的形式,考虑研究其性质。由代数基本定理,
模拟短除法,可以得到
因此
即我们所求的逆矩阵
所以 IFFT 的过程和 FFT 没有很大的区别。我们只需要将 FFT 中代入的单位根变为原来的共轭,并在最后对每一项系数除以
为了方便,我们可以把 FFT 与 IFFT 放在一个函数里,用
FFT/IFFT 的代码:
void FFT(complex<double> *a, int n, int tp = 1) {
int mid = n >> 1;
for (int i = 1; i < n; ++i) {
rev[i] = (rev[i >> 1] >> 1) + (i & 1 ? mid : 0);
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
for (int k = 1; k < n; k <<= 1) {
w[0] = { 1, 0 }, w[1] = { cos(PI / k), tp * sin(PI / k) };
for (int i = 2; i < k; ++i) w[i] = w[i - 1] * w[1];
for (int i = 0; i < n; i += k << 1) for (int j = 0; j < k; ++j) {
complex<double> x = a[i | j], y = w[j] * a[i | j | k];
a[i | j] = x + y, a[i | j | k] = x - y;
}
}
if (tp == -1) for (int i = 0; i < n; ++i) a[i] /= n;
}
DFT 和 FFT
回顾前面的内容,可以总结出 DFT 和 FFT 之间的几点差别:
- FFT 要求
是 的整数次幂,DFT 则不做要求。 - DFT 与 FFT 代入单位根的顺序相反。
快速数论变换
数论变换(NTT)是 DFT 在数论基础上的实现,快速数论变换(FNTT)则是 FFT 在数论基础上的实现。更具体地,FNTT 是在模
对次数为
根据原根的性质,
因此我们只需要将 FFT 时的
下面列出一些 NTT 常见大模数:
,有原根 。 ,有原根 。 ,有原根 。
NTT 的代码(
const int g1 = 3, g2 = (MOD + 1) / g1;
void NTT(int *a, int n, int tp = 1) {
int mid = n >> 1;
for (int i = 1; i < n; ++i) {
rev[i] = (rev[i >> 1] >> 1) + (i & 1 ? mid : 0);
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
for (int k = 1; k < n; k <<= 1) {
w[0] = 1, w[1] = qpow(tp == 1 ? g1 : g2, (MOD - 1) / (k << 1));
for (int i = 2; i < k; ++i) w[i] = 1ll * w[i - 1] * w[1] % MOD;
for (int i = 0; i < n; i += k << 1) for (int j = 0; j < k; ++j) {
int x = a[i | j], y = 1ll * w[j] * a[i | j | k] % MOD;
a[i | j] = (x + y) % MOD, a[i | j | k] = (x - y + MOD) % MOD;
}
}
if (tp == -1) {
int iv = qpow(n, MOD - 2);
for (int i = 0; i < n; ++i) a[i] = 1ll * a[i] * iv % MOD;
}
}
FFT/NTT 的应用
多项式乘法
题意:给出一个次数为
下面视
进一步地,我们发现卷积后的系数不超过
代码中的函数calc_len
和 clr
在后面的代码中会用到:
int calc_len(int n) { return 1 << (int)ceil(log2(n)); }
void clr(int *a, int n) { memset(a, 0, n << 2); }
cin >> n >> m; ++n, ++m;
for (int i = 0; i < n; ++i) cin >> f[i];
for (int i = 0; i < m; ++i) cin >> g[i];
int len = calc_len(n + m - 1);
NTT(f, len), NTT(g, len);
for (int i = 0; i < len; ++i) f[i] = 1ll * f[i] * g[i] % MOD;
NTT(f, len, -1);
for (int i = 0; i < n + m - 1; ++i) cout << f[i] << " \n"[i == n + m - 2];
分治 FFT/NTT
半在线卷积
题意:给定长度为
答案对
对于一类形如
这是一个在线问题,考虑用 CDQ 分治将其转化为离线问题。具体来说,假设我们求出了
代码:
void solve(int l, int r) {
if (l == r) return;
static int a[N], b[N];
int mid = (l + r) >> 1;
solve(l, mid);
copy(f + l, f + mid + 1, a), copy(g, g + r - l + 1, b);
int len = calc_len(r - l + 1);
NTT(a, len), NTT(b, len);
for (int i = 0; i < len; ++i) a[i] = 1ll * a[i] * b[i] % MOD;
NTT(a, len, -1);
for (int i = mid + 1; i <= r; ++i) f[i] = (f[i] + a[i - l]) % MOD;
clr(a, len), clr(b, len);
solve(mid + 1, r);
}
单项式乘积
给出
如果我们从左往右 NTT 合并,复杂度会是
考虑分治计算,令
乍一看这似乎不太合理,为什么把从左到右合并改成分治合并就能得到更优的时间复杂度呢?究其原因,从左到右合并就是不断求一个
多项式牛顿迭代
泰勒级数和麦克劳林级数
这里简单介绍学习牛顿迭代的一些基础知识。读者需要有简单的微积分基础。
对于一个光滑的函数
上式称为
这里列出一些常见的麦克劳林展开式,后文会用到一部分:
一般的牛顿迭代
一般的牛顿迭代按以下方式求出方程
- 设我们得到的近似根为
。 - 将
在 处做泰勒展开。 - 取前两项的系数来近似成
,即 。 - 解得
,将其作为新的近似根继续迭代下去。
容易发现,一般的牛顿迭代的本质上就是不断求切线的过程。
多项式牛顿迭代
我们可以用牛顿迭代的方式解决这样一类问题:给出关于多项式
与一般的牛顿迭代不同,由于多项式的一些特性,用牛顿迭代求解这类问题并无精度问题。
假设我们求出了
注意到,
所以对于
类似解得
我们只需要在初始时给出
有一些细节需要注意:
- 由于
, 中次数 的项会被 截断,所以 只需要做到 的精度。 是将 视作主元,而非 ,即分母要求的是 。
显然多项式牛顿迭代的精度成平方增长,这通常允许我们使用倍增求解。读者可以结合接下来的一些例子体会。
多项式乘法逆
题意:给定一个
这里给出两种求解方法:
方法一:平方法
显然
那么倍增求解到最小的
方法二:牛顿迭代
我们相当于要让
这和前面推的式子是一样的,同样
回顾多项式牛顿迭代的推导过程,读者可以体会平方法本质上与牛顿迭代的相似性。
倍增法的细节
倍增法有一些很重要的细节,不加注意就会出现问题:
- 注意考虑好 NTT/FFT 的变换长度。
- 记得清空!!!记得清空!!!记得清空!!!
代码:
void poly_inv(const int *a, int *res, int n) {
static int ta[N], tr[N], b[N];
b[0] = qpow(a[0], MOD - 2);
for (int k = 1; k >> 1 < n; k <<= 1) {
clr(ta, k << 1), clr(tr, k << 1);
copy(a, a + min(n, k), ta), copy(b, b + min(n, k), tr);
NTT(ta, k << 1), NTT(tr, k << 1);
for (int i = 0; i < k << 1; ++i)
b[i] = (2 - 1ll * ta[i] * tr[i] % MOD + MOD) % MOD * tr[i] % MOD;
NTT(b, k << 1, -1);
clr(b + k, k);
}
copy(b, b + n, res);
}
多项式开根
Luogu P5205 【模板】多项式开根 | Luogu P5277 【模板】多项式开根(加强版)
题意:给定一个
多项式开根同样可以平方法做,读者可以自行推导。这里直接给出牛顿迭代做法。
构造函数
对分母求逆即可计算上式。对于初值,若保证
加强版代码:
void poly_sqrt(const int *a, int *res, int n) {
int k;
static tmpa[N], tmpr[N], inv[N];
res[0] = cipolla(a[0]);
chk_min(res[0], MOD - res[0]);
for (k = 1; k >> 1 < n; k <<= 1) {
clr(tmpa, k << 1), clr(tmpr, k << 1), clr(inv, k << 1);
for (int i = 0; i < k << 1; ++i) tmpa[i] = tmpr[i] = inv[i] = 0;
copy(a, a + k, tmpa), copy(res, res + k, tmpr);
poly_inv(tmpr, inv, k), NTT(inv, k << 1);
NTT(tmpa, k << 1);
for (int i = 0; i < k << 1; ++i) res[i] = 1ll * tmpa[i] * inv[i] % MOD;
NTT(res, k << 1, -1);
for (int i = 0; i < k; ++i) res[i] = 1ll * (res[i] + tmpr[i]) % MOD * iv2 % MOD;
for (int i = k; i < k << 1; ++i) res[i] = 0;
}
for (int i = n; i < k; ++i) res[i] = 0;
}
多项式除法
题意:给定一个
这题的做法还是比较牛的。
若保证能整除,直接求逆即可,因此考虑去除
那么我们就可以多项式求逆求出
实现细节还是比较多的。代码:
void poly_div(const int *a, const int *b, int *q, int *r, int n, int m) {
static int ar[N], br[N], inv[N], tq[N];
reverse_copy(a, a + n, ar), reverse_copy(b, b + m, br);
for (int i = n - m + 1; i < m; ++i) br[i] = 0;
poly_inv(br, inv, n - m + 1);
int len = calc_len(n * 2 - m);
NTT(ar, len), NTT(inv, len);
for (int i = 0; i < len; ++i) ar[i] = 1ll * ar[i] * inv[i] % MOD;
NTT(ar, len, -1);
reverse_copy(ar, ar + n - m + 1, q), reverse_copy(ar, ar + n - m + 1, tq);
clr(ar, len), clr(br, len), clr(inv, len);
len = calc_len(n);
copy(b, b + m, br);
NTT(br, len), NTT(tq, len);
for (int i = 0; i < len; ++i) r[i] = 1ll * br[i] * tq[i] % MOD;
NTT(r, len, -1);
for (int i = 0; i < m - 1; ++i) r[i] = (a[i] - r[i] + MOD) % MOD;
clr(br, len), clr(tq, len);
}
多项式对数函数
Luogu P4725 【模板】多项式对数函数(多项式 ln)
题意:给定一个
读者可能会想到求导/积分会带来常数项的损失,不过题目钦定了
那这又导出了另一个问题:如果不保证
的对数多项式存在当且仅当 。
考虑用麦克劳林级数对
考察其常数项,即
显然该级数收敛当且仅当
代码:
void poly_diff(const int *a, int *res, int n) {
for (int i = 1; i < n; ++i) res[i - 1] = 1ll * a[i] * i % MOD;
res[n - 1] = 0;
}
void poly_intg(const int *a, int *res, int n) {
for (int i = 1; i < n; ++i) res[i] = 1ll * a[i - 1] * qpow(i, MOD - 2) % MOD;
res[0] = 0;
}
void poly_ln(const int *a, int *res, int n) {
static int df[N], inv[N];
poly_inv(a, inv, n), poly_diff(a, df, n);
int len = calc_len(n << 1);
NTT(inv, len), NTT(df, len);
for (int i = 0; i < len; ++i) df[i] = 1ll * df[i] * inv[i] % MOD;
NTT(df, len, -1), poly_intg(df, res, n);
clr(inv, len), clr(df, len);
}
多项式指数函数
Luogu P4726 【模板】多项式指数函数(多项式 exp)
题意:给定一个
这个形式直接上牛顿迭代:
倍增
和多项式对数函数类似,考虑
的指数多项式存在当且仅当 。
代码:
void poly_exp(const int *a, int *res, int n) {
static int ta[N], tr[N], b[N], lnr[N];
b[0] = 1;
for (int k = 1; k >> 1 < n; k <<= 1) {
clr(ta, k << 1), clr(tr, k << 1);
copy(a, a + min(n, k), ta), copy(b, b + min(n, k), tr);
poly_ln(tr, lnr, k);
for (int i = 0; i < k << 1; ++i) ta[i] = (ta[i] - lnr[i] + MOD) % MOD;
ta[0] = (ta[0] + 1) % MOD;
NTT(ta, k << 1), NTT(tr, k << 1);
for (int i = 0; i < k << 1; ++i) b[i] = 1ll * ta[i] * tr[i] % MOD;
NTT(b, k << 1, -1);
clr(b + k, k);
}
copy(b, b + n, res);
}
多项式幂函数
Luogu P5245 【模板】多项式快速幂 | Luogu P5273 【模板】多项式幂函数(加强版)
题意:给定一个
看到幂函数,一个思路就是两边同时取
不过
因此我们可以令
我们需要求
非加强版的代码:
void poly_pow(int *a, int *res, int n, int b) {
static int lna[N];
poly_ln(a, lna, n);
for (int i = 0; i < n; ++i) lna[i] = 1ll * lna[i] * b % MOD;
poly_exp(lna, res, n);
clr(lna, n);
}
cin >> n >> str + 1;
for (int i = 1; str[i]; ++i) k = (1ll * k * 10 + (str[i] ^ 48)) % MOD;
for (int i = 0; i < n; ++i) cin >> a[i];
poly_pow(a, ans, n, k);
for (int i = 0; i < n; ++i) cout << ans[i] << " \n"[i == n - 1];
既然上述做法要求
这就做完了吗?并没有。加强版还不保证
也就是先把系数整体左移
但还有很多细节。首先计算
需要和多项式的幂次做区分,即要存两个
其次,我们需要特判偏移量
代码:
void poly_pow(int *a, int *res, int n, int b1, int b2) {
int t = 0;
static int ta[N], tr[N], lna[N];
for (int i = 0; i < n && !a[i]; ++i, ++t);
if (1ll * t * b1 >= n) { clr(res, n); return; }
int a0 = a[t], iv = qpow(a0, MOD - 2);
for (int i = 0; i < n; ++i) ta[i] = 1ll * a[i + t] * iv % MOD;
poly_ln(ta, lna, n);
for (int i = 0; i < n; ++i) lna[i] = 1ll * lna[i] * b1 % MOD;
poly_exp(lna, tr, n);
t *= b1;
int pw = qpow(a0, b2);
clr(res, t);
for (int i = t; i < n; ++i) res[i] = 1ll * tr[i - t] * pw % MOD;
}
ios::sync_with_stdio(false), cin.tie(nullptr);
cin >> n >> str + 1;
for (int i = 1; str[i]; ++i) {
k1 = (1ll * k1 * 10 + (str[i] ^ 48)) % MOD;
k2 = (1ll * k2 * 10 + (str[i] ^ 48)) % (MOD - 1);
}
for (int i = 1; str[i] && i <= 6; ++i) k3 = k3 * 10 + (str[i] ^ 48);
for (int i = 0; i < n; ++i) cin >> a[i];
if (!a[0] && k3 >= n) {
for (int i = 0; i < n; ++i) cout << 0 << " \n"[i == n - 1];
return 0;
}
poly_pow(a, ans, n, k1, k2);
for (int i = 0; i < n; ++i) cout << ans[i] << " \n"[i == n - 1];
return 0;
多项式多点求值
题意:给定一个
挺有趣的科技啊。下面默认
方法一:多项式取模
构造多项式
令
, ,则 。
于是我们得到了一个较为显然的做法。首先容易
由于多项式取模的大常数,通常上述做法需要经过刻意卡常才能通过本题。
方法二:转置原理
咕咕咕。
代码:
void poly_mult(const int *a, const int *b, int *res, int n, int m) {
int len = calc_len(n);
static int ta[N], tb[N];
copy(a, a + n, ta), reverse_copy(b, b + m, tb);
NTT(ta, len), NTT(tb, len);
for (int i = 0; i < len; ++i) ta[i] = 1ll * ta[i] * tb[i] % MOD;
NTT(ta, len, -1);
copy(ta + m - 1, ta + n, res);
clr(ta, len), clr(tb, len);
}
namespace PolyEval {
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
int inv[N];
int len[N << 2], *g[N << 2], *q[N << 2];
void eval_build(int p, int l, int r, const int *a) {
if (l == r) {
len[p] = 1;
g[p] = new int[2], q[p] = new int[2];
g[p][0] = 1, g[p][1] = (-a[l] + MOD) % MOD;
return;
}
int mid = (l + r) >> 1;
eval_build(ls(p), l, mid, a), eval_build(rs(p), mid + 1, r, a);
len[p] = len[ls(p)] + len[rs(p)];
g[p] = new int[len[p] + 1], q[p] = new int[len[p] + 1];
poly_mul(g[ls(p)], g[rs(p)], g[p], len[ls(p)] + 1, len[rs(p)] + 1);
}
void eval_solve(int p, int l, int r, int *ans) {
if (l == r) { ans[l] = q[p][0]; return; }
int mid = (l + r) >> 1;
poly_mult(q[p], g[rs(p)], q[ls(p)], r - l + 1, len[rs(p)] + 1);
eval_solve(ls(p), l, mid, ans);
poly_mult(q[p], g[ls(p)], q[rs(p)], r - l + 1, len[ls(p)] + 1);
eval_solve(rs(p), mid + 1, r, ans);
}
void solve(const int *a, const int *b, int n, int m, int *ans) {
static int tmp[N];
eval_build(1, 1, m, b);
poly_inv(g[1], inv, m + 1), reverse(inv, inv + m + 1);
poly_mul(a, inv, tmp, n, m + 1), copy(tmp + n, tmp + n + m, q[1]);
eval_solve(1, 1, m, ans);
for (int i = 1; i <= m; ++i) ans[i] = (1ll * ans[i] * b[i] % MOD + a[0]) % MOD;
}
#undef ls
#undef rs
}
多项式快速插值
题意:给出
回顾拉格朗日插值公式:
不妨将分母提出,得到
设
而函数
于是插值公式化为
这是一个比较典的可以分治计算的形式,具体来说,令
即
把
代码:
namespace PolyInter {
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
int val[N];
int len[N << 2], *g[N << 2], *h[N << 2];
void inter_build(int p, int l, int r, const int *a) {
if (l == r) {
len[p] = 1;
g[p] = new int[2], h[p] = new int[2];
g[p][0] = -a[l] + MOD, g[p][1] = 1;
return;
}
int mid = (l + r) >> 1;
inter_build(ls(p), l, mid, a), inter_build(rs(p), mid + 1, r, a);
len[p] = len[ls(p)] + len[rs(p)];
g[p] = new int[len[p] + 1], h[p] = new int[len[p] + 1];
poly_mul(g[ls(p)], g[rs(p)], g[p], len[ls(p)] + 1, len[rs(p)] + 1);
}
void inter_solve(int p, int l, int r, const int *y) {
if (l == r) { h[p][0] = 1ll * y[l] * qpow(val[l], MOD - 2) % MOD; return; }
int mid = (l + r) >> 1;
inter_solve(ls(p), l, mid, y), inter_solve(rs(p), mid + 1, r, y);
static int tmp[N];
poly_mul(h[ls(p)], g[rs(p)], tmp, len[ls(p)], len[rs(p)] + 1);
poly_mul(h[rs(p)], g[ls(p)], h[p], len[rs(p)], len[ls(p)] + 1);
for (int i = 0; i < len[p]; ++i) h[p][i] = (h[p][i] + tmp[i]) % MOD;
clr(tmp, len[p]);
}
void solve(const int *x, const int *y, int n) {
static int df[N];
inter_build(1, 1, n, x);
poly_diff(g[1], df, n + 1);
PolyEval::solve(df, x, n + 1, n, val);
inter_solve(1, 1, n, y);
clr(df, n);
}
#undef ls
#undef rs
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)