ケロシのポリ - 多项式学习笔记
ケロシのポリ - 多项式学习笔记
持续更新中……
多项式乘法
给出 和 ,求出 。
考虑用 NTT 求出点值,而点值可以直接相乘。
然后对乘出来的点值做一遍逆变换即可。
时间复杂度 。
ケロシの代码
void solve() {
cin >> n >> m;
FOR(i, 0, n) cin >> a[i];
FOR(i, 0, m) cin >> b[i];
int lim = 1, l = 0;
while(lim <= (n + m)) lim <<= 1, l++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
NTT(a, lim, 1);
NTT(b, lim, 1);
REP(i, lim) Mul(a[i], b[i]);
NTT(a, lim, -1);
FOR(i, 0, n + m) cout << a[i] << " ";
cout << endl;
}
多项式乘法逆
给出 ,求 使得 。
首先 ,然后考虑倍增,用 的答案 推 的答案:
需要用到 的多项式乘法,计算一下复杂度:
所以复杂度 。
ケロシの代码
int c[N];
void invs(int * a, int * b, int d) {
if(d == 1) {
b[0] = fp(a[0], P - 2);
return;
}
invs(a, b, (d + 1) >> 1);
int lim = 1, l = 0;
while(lim < (d << 1)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
REP(i, d) c[i] = a[i];
FOR(i, d, lim - 1) c[i] = 0;
NTT(b, lim, 1);
NTT(c, lim, 1);
REP(i, lim) Mul(b[i], sub(2, mul(b[i], c[i])));
NTT(b, lim, -1);
FOR(i, d, lim - 1) b[i] = 0;
}
多项式开根
给出 ,求 使得 。
先考虑常数项,那么 就是 的一个二次剩余。
然后还是考虑倍增,用 的答案 推 的答案:
时间复杂度 。
多项式在 和 上的意义
一般来说, 和 都是对于实数的,那么要计算多项式等形式的 和 需要用到泰勒公式进行展开:
我们把式子中的 替换成多项式即可使运算有意义。
为了使其不无限循环下去,式子中的多项式的 常数项必须为 ,使得 及以后 都等于 。
所以当多项式 满足 时 有意义,
当多项式 满足 时 有意义。
多项式对数函数
给出 ,求 使得 。
对于 ,注意到其求导后即为 ,比较好算,所以考虑两边求导。
对于复合函数 ,其求导需要遵循链式法则:。
需要多项式求逆和多项式乘法,时间复杂度 。
ケロシの代码
int p[N], q[N];
void lns(int * a, int * b, int d) {
int lim = 1, l = 0;
while(lim < (d << 1)) lim <<= 1, l++;
REP(i, lim) q[i] = 0;
invs(a, q, d);
FOR(i, 1, d - 1) p[i - 1] = mul(i, a[i]);
FOR(i, d - 1, lim - 1) p[i] = 0;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
NTT(p, lim, 1);
NTT(q, lim, 1);
REP(i, lim) Mul(p[i], q[i]);
NTT(p, lim, - 1);
FOR(i, 1, d) b[i] = mul(p[i - 1], inv[i]);
b[0] = 0;
}
多项式牛顿迭代
给出多项式 ,求 的零点 ,即满足 。
考虑使用牛顿迭代来进行倍增。
先单独考虑常数项 。
接下来使用牛顿迭代,用 的答案 推 的答案。
考虑泰勒展开公式:
考虑将泰勒展开公式中的 和 分别代入 和 ,那么 及以后的项 都等于 ,所以直接代入泰勒展开公式:
因为
所以可以推出 :
多项式指数函数
给出 ,求 使得 。
考虑将求 转换成关于 的零点,
那么 就是函数 的零点。
接下来使用牛顿迭代,代入公式。
但是需要求得 ,因为自变量 是多项式, 也是多项式,所以对于 来说 就是常数项,所以
然后直接代入公式即可:
需要用到多项式乘法,多项式乘法逆和多项式对数函数 。
时间复杂度 。
ケロシの代码
int h[N];
void exps(int * a, int * b, int d) {
if(d == 1) {
b[0] = 1;
return;
}
exps(a, b, (d + 1) >> 1);
REP(i, d) h[i] = 0;
lns(b, h, d);
REP(i, d) h[i] = sub(a[i], h[i]);
Add(h[0], 1);
int lim = 1, l = 0;
while(lim < (d << 1)) lim <<= 1, l++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
FOR(i, d, lim - 1) h[i] = 0;
NTT(b, lim, 1);
NTT(h, lim, 1);
REP(i, lim) Mul(b[i], h[i]);
NTT(b, lim, - 1);
FOR(i, d, lim - 1) b[i] = 0;
}
多项式快速幂
给出 和 ,求 使得 ,保证 。
考虑两边同时取对数下放指数:
时间复杂度 。
多项式连续点值平移
给定不超过 次多项式的 个点值:,并且给定 ,满足 ,求出 。
考虑拉格朗日插值求出 的点值:
考虑卷积优化,把式子拆成关于 , 和 的部分:
为了方便卷积,对齐下标:
拆成两个函数,注意下标必须要正的,所以将 改成 即可:
然后卷起来即可:
时间复杂度 。
ケロシの代码
int n, m, a[N], F[N], G[N];
int fac[N], finv[N];
void solve() {
cin >> n >> m;
FOR(i, 0, n) cin >> a[i];
fac[0] = 1;
FOR(i, 1, n) fac[i] = mul(fac[i - 1], i);
finv[n] = fp(fac[n], P - 2);
ROF(i, n - 1, 0) finv[i] = mul(finv[i + 1], i + 1);
FOR(i, 0, n) F[i] = mul({a[i], finv[i], finv[n - i], sgn(n - i)});
FOR(i, 0, n << 1) G[i] = fp(m - n + i, P - 2);
int lim = 1, l = 0;
while(lim <= (n << 1)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) Mul(F[i], G[i]);
NTT(F, lim, - 1);
int res = 1;
FOR(i, 0, n) Mul(res, m - i);
FOR(i, 0, n) {
cout << mul(F[n + i], res) << " ";
Mul(res, fp(m + i - n, P - 2));
Mul(res, m + i + 1);
}
cout << endl;
}
Chirp Z-Transform
给定一个 次多项式 ,给定 和 ,求出 。
考虑:
我们希望用卷积加速这个式子,但是有有关 的项,不好处理。
注意到 是在指数上,拆成加减后会变成乘法,比较好做,所以考虑拆解 :
但是这个式子的指数有除以二,需要二次剩余,不好做。
考虑另外一种拆法:
这样就都是整数了,代入式子:
现在就是一部分只和 有关,另一部分之和 有关,变成卷积形式了,直接卷积即可。
假设 与 同阶,时间复杂度 。
ケロシの代码
int n, c, m;
int a[N], pw1[B + 1], pw2[B + 1];
int F[N], G[N];
inline int pw(int x) {
return mul(pw1[x % B], pw2[x / B]);
}
inline int C2(int x) {
return (1ll * x * (x - 1) / 2) % (P - 1);
}
void solve() {
cin >> n >> c >> m;
REP(i, n) cin >> a[i];
pw1[0] = pw2[0] = 1;
FOR(i, 1, B) pw1[i] = mul(pw1[i - 1], c);
FOR(i, 1, B) pw2[i] = mul(pw2[i - 1], pw1[B]);
REP(i, n + m) F[i] = pw(C2(i));
REP(i, n) G[i] = mul(a[i], pw(P - 1 - C2(i)));
reverse(G, G + n);
int lim = 1, l = 0;
while(lim <= (n + m)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) Mul(F[i], G[i]);
NTT(F, lim, - 1);
REP(i, m) cout << mul(F[i + n - 1], pw(P - 1 - C2(i))) << " "; cout << endl;
}
任意模数多项式乘法
能做 NTT 的模数需要大量单位根,所以不是所有的数都能做 NTT 的模数。
考虑使用三个原根都是 的模数:。
对于每个模数都做多项式乘法,然后使用 CRT 把答案解出来即可。
时间复杂度 。
ケロシの代码
const int N = 1e6 + 5;
const int P1 = 998244353;
const int P2 = 469762049;
const int P3 = 1004535809;
const int I1 = 208783132;
const int I2 = 395249030;
inline int add1(int x, int y) { return (x + y < P1 ? x + y : x + y - P1); }
inline void Add1(int & x, int y) { x = (x + y < P1 ? x + y : x + y - P1); }
inline int sub1(int x, int y) { return (x < y ? x - y + P1 : x - y); }
inline void Sub1(int & x, int y) { x = (x < y ? x - y + P1 : x - y); }
inline int mul1(int x, int y) { return (1ll * x * y) % P1; }
inline void Mul1(int & x, int y) { x = (1ll * x * y) % P1; }
inline int add2(int x, int y) { return (x + y < P2 ? x + y : x + y - P2); }
inline void Add2(int & x, int y) { x = (x + y < P2 ? x + y : x + y - P2); }
inline int sub2(int x, int y) { return (x < y ? x - y + P2 : x - y); }
inline void Sub2(int & x, int y) { x = (x < y ? x - y + P2 : x - y); }
inline int mul2(int x, int y) { return (1ll * x * y) % P2; }
inline void Mul2(int & x, int y) { x = (1ll * x * y) % P2; }
inline int add3(int x, int y) { return (x + y < P3 ? x + y : x + y - P3); }
inline void Add3(int & x, int y) { x = (x + y < P3 ? x + y : x + y - P3); }
inline int sub3(int x, int y) { return (x < y ? x - y + P3 : x - y); }
inline void Sub3(int & x, int y) { x = (x < y ? x - y + P3 : x - y); }
inline int mul3(int x, int y) { return (1ll * x * y) % P3; }
inline void Mul3(int & x, int y) { x = (1ll * x * y) % P3; }
int fp1(int x, int y) {
int res = 1;
for(; y; y >>= 1) {
if(y & 1) Mul1(res, x);
Mul1(x, x);
}
return res;
}
int fp2(int x, int y) {
int res = 1;
for(; y; y >>= 1) {
if(y & 1) Mul2(res, x);
Mul2(x, x);
}
return res;
}
int fp3(int x, int y) {
int res = 1;
for(; y; y >>= 1) {
if(y & 1) Mul3(res, x);
Mul3(x, x);
}
return res;
}
struct cint {
int x1, x2, x3;
cint operator + (const cint & A) {
return {add1(x1, A.x1), add2(x2, A.x2), add3(x3, A.x3)};
}
cint operator - (const cint & A) {
return {sub1(x1, A.x1), sub2(x2, A.x2), sub3(x3, A.x3)};
}
cint operator * (const cint & A) {
return {mul1(x1, A.x1), mul2(x2, A.x2), mul3(x3, A.x3)};
}
cint & operator += (const cint & A) {
Add1(x1, A.x1); Add2(x2, A.x2); Add3(x3, A.x3);
return * this;
}
cint & operator -= (const cint & A) {
Sub1(x1, A.x1); Sub2(x2, A.x2); Sub3(x3, A.x3);
return * this;
}
cint & operator *= (const cint & A) {
Mul1(x1, A.x1); Mul2(x2, A.x2); Mul3(x3, A.x3);
return * this;
}
int crt(int P) {
ll x4 = 1ll * P1 * mul2(sub2(x2, x1 % P2), I1) + x1;
int k4 = mul3(sub3(x3, x4 % P3), I2);
return (x4 + 1ll * k4 * (1ll * P1 * P2 % P)) % P;
}
};
int rev[N];
void NTT(cint * a, int lim, int o) {
REP(i, lim) if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int i = 1; i < lim; i <<= 1) {
cint wn = {
fp1(o == 1 ? 3 : (P1 + 1) / 3, (P1 - 1) / (i << 1)),
fp2(o == 1 ? 3 : (P2 + 1) / 3, (P2 - 1) / (i << 1)),
fp3(o == 1 ? 3 : (P3 + 1) / 3, (P3 - 1) / (i << 1))
};
for(int j = 0; j < lim; j += (i << 1)) {
cint w = {1, 1, 1};
for(int k = 0; k < i; k ++, w *= wn) {
cint x = a[j + k];
cint y = a[j + k + i] * w;
a[j + k] = x + y;
a[j + k + i] = x - y;
}
}
}
if(o == 1) return;
cint inv = {fp1(lim, P1 - 2), fp2(lim, P2 - 2), fp3(lim, P3 - 2)};
REP(i, lim) a[i] *= inv;
}
int n, m, P; cint F[N], G[N];
void solve() {
cin >> n >> m >> P;
FOR(i, 0, n) {
int x; cin >> x; x %= P;
F[i] = {x, x, x};
}
FOR(i, 0, m) {
int x; cin >> x; x %= P;
G[i] = {x, x, x};
}
int lim = 1, l = 0;
while(lim <= (n + m)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) F[i] *= G[i];
NTT(F, lim, - 1);
FOR(i, 0, n + m) cout << F[i].crt(P) << " "; cout << endl;
}
快速阶乘
给定 和 ,求 。
考虑分块,设快长 ,并考虑求解一段数连乘的值,考虑设多项式 。
那么 ,也就是求出 即可。
考虑求解 ,但是在 需要的下标都是 ,这启发直接计算点值。
考虑倍增求解点值,考虑 如何转移到 ,发现 。
所以直接将 进行连续点值平移到 也就是 即可。
但这样是需要两只 。
考虑缩减范围, 就是一个 次多项式,只保留 一共 即可。
倍增到 的时候再做一次点值平移得出 即可。
这样时间复杂度就是 。
ケロシの代码
int n, P, B;
int a[N], b[N];
cint F[N], G[N];
int fac[N], finv[N];
void moves(int * a, int * b, int n, int m) {
int lim = 1, l = 0;
while(lim <= (n << 1)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
REP(i, lim) F[i] = G[i] = Cint(0);
FOR(i, 0, n) F[i] = Cint(mul({a[i], finv[i], finv[n - i], sgn(n - i)}));
FOR(i, 0, n << 1) G[i] = Cint(inv(sub(m, n - i)));
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) F[i] *= G[i];
NTT(F, lim, - 1);
int res = 1;
FOR(i, 0, n) Mul(res, sub(m, i));
FOR(i, 0, n) {
b[i] = mul(F[n + i].crt(P), res);
Mul(res, inv(sub(m, n - i)));
Mul(res, add(m, i + 1));
}
}
void facs(int d) {
if(d == 1) {
a[0] = 1; a[1] = B + 1;
return;
}
facs(d >> 1);
moves(a, b, d >> 1, (d >> 1) + 1);
REP(i, d) a[(d >> 1) + i + 1] = b[i];
moves(a, b, (d >> 1) << 1, mul(d >> 1, inv(B)));
FOR(i, 0, (d >> 1) << 1) Mul(a[i], b[i]);
if(d & 1) {
REP(i, d) Mul(a[i], i * B + d);
a[d] = 1;
FOR(i, 1, d) Mul(a[d], d * B + i);
}
}
void solve() {
cin >> n >> P;
B = sqrt(n);
fac[0] = 1;
FOR(i, 1, B) fac[i] = mul(fac[i - 1], i);
finv[B] = fp(fac[B], P - 2);
ROF(i, B - 1, 0) finv[i] = mul(finv[i + 1], i + 1);
facs(B);
int ans = 1;
REP(i, B) Mul(ans, a[i]);
FOR(i, B * B + 1, n) Mul(ans, i);
cout << ans << endl;
}
第二类斯特林数行
给出 ,对于 ,求出 。
考虑第二类斯特林数的通项公式:
直接设多项式 ,,直接卷积求解即可。
时间复杂度 。
ケロシの代码
int n;
int fac[N], finv[N];
int F[N], G[N];
void init(int n) {
fac[0] = 1;
FOR(i, 1, n) fac[i] = mul(fac[i - 1], i);
finv[n] = fp(fac[n], P - 2);
ROF(i, n - 1, 0) finv[i] = mul(finv[i + 1], i + 1);
}
void solve() {
cin >> n;
init(n);
int lim = 1, l = 0;
while(lim <= (n << 1)) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
FOR(i, 0, n) F[i] = mul(fp(i, n), finv[i]);
FOR(i, 0, n) G[i] = mul(sgn(i), finv[i]);
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) Mul(F[i], G[i]);
NTT(F, lim, - 1);
FOR(i, 0, n) cout << F[i] << " "; cout << endl;
}
第二类斯特林数列
给出 和 ,对于 ,求出 。
考虑组合意义,先乘上 变成 个有标号球放进 个有标号非空盒子里,考虑每次加一个盒子,相当于选择放多少个球再分配标号,不难发现 EGF 的乘法可以做这个。
所以设只放一个盒子的 EGF,除了放 个都有 的方案,所以设 EGF 。
所以答案的 EGF 即为 。
使用多项式快速幂即可。
时间复杂度 。
ケロシの代码
int n, k;
int F[N], G[N];
void solve() {
cin >> n >> k;
if(n < k) {
REP(_, n + 1) cout << 0 << " ";
return;
}
init(n);
int len = n - k + 1;
REP(i, len) F[i] = finv[i + 1];
lns(F, G, len);
REP(i, len) Mul(G[i], k);
exps(G, F, len);
REP(_, k) cout << 0 << " ";
REP(i, len) cout << mul({F[i], fac[i + k], finv[k]}) << " "; cout << endl;
}
第一类斯特林数行
给出 ,对于 ,求出 。
所以只要求 即可,考虑倍增:
然后考虑 怎么求:
右边很明显是一个卷积形式,设 和 ,直接卷即可。
时间复杂度 。
ケロシの代码
int n, a[N], b[N];
int fac[N], finv[N];
int F[N], G[N];
void init(int n) {
fac[0] = 1;
FOR(i, 1, n) fac[i] = mul(fac[i - 1], i);
finv[n] = fp(fac[n], P - 2);
ROF(i, n - 1, 0) finv[i] = mul(finv[i + 1], i + 1);
}
void slv(int d) {
if(d == 1) {
a[1] = 1;
return;
}
if(d & 1) {
slv(d - 1);
FOR(i, 0, d) F[i] = 0;
REP(i, d) Add(F[i + 1], a[i]);
REP(i, d) Add(F[i], mul(a[i], d - 1));
FOR(i, 0, d) a[i] = F[i];
return;
}
slv(d >> 1);
int len = d >> 1;
int lim = 1, l = 0;
while(lim <= d) lim <<= 1, l ++;
REP(i, lim) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1));
REP(i, lim) F[i] = G[i] = 0;
FOR(i, 0, len) F[i] = mul(fac[i], a[i]);
FOR(i, 0, len) G[len - i] = mul(finv[i], fp(len, i));
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) Mul(F[i], G[i]);
NTT(F, lim, - 1);
REP(i, lim) b[i] = 0;
FOR(i, 0, len) b[i] = mul(finv[i], F[len + i]);
NTT(a, lim, 1);
NTT(b, lim, 1);
REP(i, lim) Mul(a[i], b[i]);
NTT(a, lim, - 1);
}
void solve() {
cin >> n;
init(n);
slv(n);
FOR(i, 0, n) cout << a[i] << " "; cout << endl;
}
第一类斯特林数列
给出 和 ,对于 ,求出 。
考虑使用与第二类斯特林数列相同的做法,但是注意单个环的方案是 ,所以 EGF 。
时间复杂度 。
ケロシの代码
int n, k;
int F[N], G[N];
void solve() {
cin >> n >> k;
if(n < k) {
REP(_, n + 1) cout << 0 << " ";
return;
}
init(n);
int len = n - k + 1;
REP(i, len) F[i] = inv[i + 1];
lns(F, G, len);
REP(i, len) Mul(G[i], k);
exps(G, F, len);
REP(_, k) cout << 0 << " ";
REP(i, len) cout << mul({F[i], fac[i + k], finv[k]}) << " "; cout << endl;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App