这是一次将多项式模板的易用性与高效性结合的尝试。进度见此 pastebin
怎样有易用性?
封装,用一个 vector
来存多项式,重载各种运算,特点是用的时候比较轻松,但本身的特性导致效率必然不能达到最优。
大概开放形如这样的接口:
struct Poly {
Poly(const vector<int>& v); // init by vector
Poly(initializer_list<int> list); // init by `Poly a = {1, 2, 3};`
int* base(); // iterator
int& operator[](int index); // reference
Poly operator+(const Poly& rhs) const; // basic calculation
// operator + - * /
Poly inv() const; // basic elementary function
Poly pow(int k); // power
};
struct EvaluationHelper {
vector<int> evaluate(const vector<int>& xs, const Poly& f); // polynomial evaluation & interpolation
Poly interpolate(const vector<pair<int, int>>& points);
};
struct PolyMod {
Poly modulo;
Poly homo(const Poly& a) const; // fast modulo
};
Poly operator "" _z(unsigned long long a) { return {0, (int)a}; }
// sugar in C++11(?), you can use a_z to express a polynomial az now.
怎样有高效性?
用的时候看不见的地方,在封装的内部尽量提高效率,尽量使用较为友好的 ntt 方式,并且计算多项式带入初等函数时,使用 negiizhao 整理的 FFT 次数更少的迭代方法。
这点倒是没什么新颖的,顶多就是把位翻转能做的某些工作在初始化的时候就帮忙做一下,有时间试一下 Montgomery 约化什么的管不管用:
struct NTT {
int brev[1 << L2];
NTT() {
for (int i = 1; i < (1 << L2); ++i)
brev[i] = brev[i >> 1] >> 1 | ((i & 1) << (L2 - 1));
}
void fft(int* a, int lgn, int d = 1) {
int n = 1 << lgn;
for (int i = 0; i < n; ++i) {
int rev = (brev[i >> L2] | (brev[i & ((1 << L2) - 1)] << L2)) >> ((L2 << 1) - lgn);
if (i < rev)
swap(a[i], a[rev]);
}
// ...
}
} ntt;
经过测试,某些形式特殊的数组的 NTT 改良版本,看似省略了部分计算,实则缓存不友好,还不如直接做……
在比较普遍的做法中,我们倍增时通常会递归地计算需要的一些子部分,例如大部分初等函数中途都会用到倒数,而倍增的时候倒数内部本来是没有必要重复计算一些东西的。这时为了效率,我们必须改递归为同轮递推的倍增方法,也可以认为是一种记忆化搜索。这样一来我们调用的时候就达到了论文中真正等价的 FFT 次数。为此我搞了一个牛顿迭代的外包结构体
struct Newton {
void inv(const Poly& f, const Poly& nttf, Poly& g, const Poly& nttg, int t);
// ...
} nit;
它的外包作用就是帮助进行迭代的细节,因此我们在写别的嵌套使用的时候就可以像这样:
Poly Poly::sqrt() const {
Poly g = nt.sqrt(a[0]), h = nt.inv(g[0]), nttg = g;
for (int t = 0; (1 << t) <= deg(); ++t) {
nit.sqrt(slice((2 << t) - 1), g, nttg, h, t);
if ((2 << t) <= deg()) {
nttg = g;
ntt.fft(nttg.base(), t + 1, 1);
nit.inv(g, nttg, h, t);
}
}
return g.slice(deg());
}
求积分的时候就要这种东西。
没有必要每次都重新算一遍,设计一个倍增式预处理的结构。
struct Simple {
int n;
vector<int> fac, ifac, inv;
void build(int n) {
// calculate fac, ifac, inv
}
Simple() {
build(1);
}
void check(int k) {
int nn = n;
if (k > nn) {
while (k > nn)
nn <<= 1;
build(nn);
}
}
int gfac(int k) {
check(k);
return fac[k];
}
// ...
} simp;
还不打算整合板子,见提交。
所谓的 PolyMod
类的用法是类似实现一个 BinaryOperator
的协议。特点在于多次取模同一个多项式的时候,可以预处理这个多项式翻转后的逆元的 NTT 的值。这样能够有效节省之后重复的计算。在单次乘法消耗大的情况下,减少快速幂过程中乘法的次数是极为有效的减小常数的方法,由四毛子 (Four Russians) 方法可以做到 log 2 n + Θ ( log n log log n ) \log_2 n + \Theta\left(\frac{\log n}{\log \log n}\right) log2n+Θ(loglognlogn) 次乘法。所以封装了一个快速幂装置
template <class T, class Comp>
struct AdditionChain {
int k;
vector<T> prepare;
T t, unit;
Comp comp;
AdditionChain(const T& t, const Comp& comp, int k, const T& unit = 1) : comp(comp), t(t), unit(unit), k(k), prepare(1U << k) {
prepare[0] = unit;
for (int i = 1; i < 1 << k; ++i)
prepare[i] = comp(prepare[i - 1], t);
}
static AdditionChain fourRussians(const T &t, const Comp &comp, int lgn, const T &unit = 1) {
lgn = max(lgn, 1);
int k = 1, lglgn = 1;
while (2 << lglgn <= lgn)
++lglgn;
int w = lgn / lglgn;
while (1 << k < w)
++k;
return AdditionChain(t, comp, k, unit);
}
T pow(int n) const {
if (n < 1 << k)
return prepare[n];
int r = n & ((1 << k) - 1);
T step = pow(n >> k);
for (int rep = 0; rep < k; ++rep)
step = comp(step, step);
return comp(step, prepare[r]);
}
};