多项式牛顿迭代的分块优化
草稿。。。
半在线卷积可以多叉,牛顿迭代怎么就不能多叉呢。
在本文我们利用分块得到了 sqrt(8E),inv(10E),div(10E),exp(14E),相比以前有了一定的常数提升。
本文可以看作 Faster algorithms for the square root and reciprocal of power series 和 Faster exponentials of power series 的翻译。原论文还有 inv(8.66E) 和 exp(13E) 的算法,但是过于复杂了,我没有尝试。
初次写文,好多地方不规范,欢迎指教。
分块原理
对于两个长为 多项式 ,我们把 分成 块,分别记做
对于 类似,为了简化描述,我们记 。
其乘积有一些额外的项,拿 举例
注意
因为 是 项的,可以用循环卷积转回去。写全即是
一般的,对于 的第 块,有公式
所以我们对应的计算过程为
而 ,无需计算。即在各块的 DFT 已经计算好的情况下,我们只需要一次 IDFT 就可以算出第 块。
平方根
这个东西其实和半在线卷积很像,只不过 relax 最后归于一个点,而这里是一整块在迭代。
类似于普通牛顿迭代,假设 已经给出,那么观察缺少 所造成的影响
即得
具体的步骤是
- 给定 ,预计算 ,可以使用任何 6E 的算法。
- 计算 ,花费 2E。
-
- 假设要计算第 块。
- 计算 ,花费 2E。
- 计算 ,用前述优化只需一次 IDFT,花费 2E。
- 计算 ,花费 4E。
总之,根据论文估计,若我们通过分成 块计算,大概开销是 8E。
Poly Poly::sqrt() const {
if (deg() == 1) {
return {front().sqrt()};
}
const int R = 16, iv2 = qpow(2);
int m = get_lim((deg() - 1) / R + 1);
Poly x = cut(m).sqrt(), h = x.inv().ntt(m * 2);
vector<Poly> ng(R);
for (int k = 1; x.deg() < deg(); k++) {
ng[k - 1] = x.cut(m, (k - 1) * m).ntt(m * 2);
Poly psi(m * 2);
for (int j = 0; j < k; j++) {
if (j >= 1) {
for (int i = 0; i < m; i++)
psi[i] -= ng[j][i] * (ng[k - j][i] + ng[k - 1 - j][i]);
for (int i = m; i < m * 2; i++)
psi[i] -= ng[j][i] * (ng[k - j][i] - ng[k - 1 - j][i]);
} else {
for (int i = 0; i < m; i++)
psi[i] -= ng[j][i] * ng[k - 1 - j][i];
for (int i = m; i < m * 2; i++)
psi[i] += ng[j][i] * ng[k - 1 - j][i];
}
}
psi.intt(m * 2).fillZeroH(m * 2);
for (int j = 0; j < min(m, deg() - m * k); j++)
psi[j] += (*this)[m * k + j];
mul(psi, h, m * 2);
x.redeg((k + 1) * m);
for (int i = 0; i < m; i++)
x[m * k + i] = psi[i] * iv2;
}
return x.cut(deg());
}
实际测试发现,迭代写法常数对 敏感,猜测是 inv 开销过大,算足 次才有优势,递归在划分上比迭代更好。块数 可以不是 的幂。
代码请看 提交。
倒数
其分块形式是
具体的步骤是
- 给定 ,而 ,不用计算。
-
- 假设要计算第 块。
- 计算 和 ,花费 4E。
- 计算 ,花费 2E。
- 计算 ,花费 4E。
Poly Poly::inv() const { // 10E
if (deg() == 1) {
return {front().inv()};
}
const int R = 16;
int m = get_lim((deg() - 1) / R + 1);
Poly x = cut(m).inv();
vector<Poly> nf(R), ng(R);
nf[0] = cut(m).ntt(m * 2);
for (int k = 1; x.deg() < deg(); k++) {
nf[k] = cut(m, k * m).ntt(m * 2); // 2E
ng[k - 1] = x.cut(m, (k - 1) * m).ntt(m * 2); // 2E
Poly psi(m * 2);
for (int j = 0; j < k; j++) {
for (int i = 0; i < m; i++)
psi[i] -= ng[j][i] * (nf[k - j][i] + nf[k - 1 - j][i]);
for (int i = m; i < m * 2; i++)
psi[i] -= ng[j][i] * (nf[k - j][i] - nf[k - 1 - j][i]);
}
psi.intt(m * 2).fillZeroH(m * 2); // 2E
mul(psi, ng[0], m * 2); // 4E
x.redeg((k + 1) * m);
for (int i = 0; i < m; i++)
x[m * k + i] = psi[i];
}
return x.cut(deg());
}
当 时,开销大概是 10E。代码请看 提交。
原论文中分成 块,前面一块方法相同,后面两块通过更优的复用做到了 。我还没有阅读,感兴趣的可以尝试。
商数
论文里没有这个,我觉得右边加个 没啥影响,试了试的确可以。
因为 不需要做 DFT,故与倒数的方法基本相同,这里不重复了。
Poly Poly::div(Poly f) const { // 10E
if (deg() == 1) {
return {front() * f[0].inv()};
}
f.redeg(deg());
const int R = 16;
int m = get_lim((deg() - 1) / R + 1);
Poly x = cut(m).div(f), h = f.cut(m).inv().ntt(m * 2);
vector<Poly> nf(R), ng(R);
nf[0] = f.cut(m).ntt(m * 2);
for (int k = 1; x.deg() < deg(); k++) {
nf[k] = f.cut(m, k * m).ntt(m * 2);
ng[k - 1] = x.cut(m, (k - 1) * m).ntt(m * 2);
Poly psi(m * 2);
for (int j = 0; j < k; j++) {
for (int i = 0; i < m; i++)
psi[i] -= ng[j][i] * (nf[k - j][i] + nf[k - 1 - j][i]);
for (int i = m; i < m * 2; i++)
psi[i] -= ng[j][i] * (nf[k - j][i] - nf[k - 1 - j][i]);
}
psi.intt(m * 2).fillZeroH(m * 2);
for (int j = 0; j < min(m, deg() - m * k); j++)
psi[j] += (*this)[m * k + j];
mul(psi, h, m * 2);
x.redeg((k + 1) * m);
for (int i = 0; i < m; i++)
x[m * k + i] = psi[i];
}
return x.cut(deg());
}
当 时,开销大概是 10E。代码请看 提交。
指数
指数与前者不同,其微分方程 左右都有 ,不能直接算。
类似的,写作分块形式
为了方便起见,我们记 ,有
令 ,注意到
因此
如此就可以更快的取出 了。
具体的计算过程:
- 给定 。
- 计算 和 ,花费 6E + 2E
- 计算 ,花费 2E。
-
- 假设要计算第 块
- 计算 和 ,花费 2E + 2E。
- 计算 ,花费 2E。
- 计算 ,花费 4E。
- 计算 ,花费 4E。
当 时,开销大概是 14E。代码请看 提交。
Poly Poly::exp() const { // 14E
if (deg() == 1) {
return {1};
}
const int S = 16;
int m = get_lim((deg() - 1) / S + 1);
Poly x = cut(m).exp(), u = x.inv();
vector<Poly> nf(S), ng(S);
Poly df = *this;
for (int i = 0; i < df.deg(); i++)
df[i] *= i;
u.ntt(m * 2);
nf[0] = df.cut(m).ntt(m * 2);
for (int k = 1; x.deg() < deg(); k++) {
nf[k] = df.cut(m, k * m).ntt(m * 2);
ng[k - 1] = x.cut(m, m * (k - 1)).ntt(m * 2);
Poly psi(m * 2);
for (int j = 0; j < k; j++) {
for (int i = 0; i < m; i++)
psi[i] += ng[j][i] * (nf[k - j][i] + nf[k - 1 - j][i]);
for (int i = m; i < m * 2; i++)
psi[i] += ng[j][i] * (nf[k - j][i] - nf[k - 1 - j][i]);
}
psi.intt(m * 2).fillZeroH(m * 2);
mul(psi, u, m * 2).fillZeroH(m * 2);
pre_inv(m * (k + 2));
for (int i = 0; i < m * 2; i++)
psi[i] *= Inv[m * k + i];
mul(psi, ng[0], m * 2).fillZeroH(m * 2);
x.redeg((k + 1) * m);
for (int i = 0; i < m; i++)
x[m * k + i] = psi[i];
}
return x.cut(deg());
}
原论文中分成 块,做到了 13E。我还没有阅读,感兴趣的可以尝试。
Refence
https://arxiv.org/abs/0910.1926
https://arxiv.org/abs/0911.3110
https://negiizhao.blog.uoj.ac/blog/4671
https://hly1204.github.io/library/math/formal_power_series/formal_power_series.hpp