EI 的第二代多项式板子整体架构理念

这是一次将多项式模板的易用性高效性结合的尝试。进度见此 pastebin

怎样有易用性?
封装,用一个 vector<int> 来存多项式,重载各种运算,特点是用的时候比较轻松,但本身的特性导致效率必然不能达到最优。

大概开放形如这样的接口:

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 次数更少的迭代方法。

降低 NTT 的常数

这点倒是没什么新颖的,顶多就是把位翻转能做的某些工作在初始化的时候就帮忙做一下,有时间试一下 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 改良版本,看似省略了部分计算,实则缓存不友好,还不如直接做……

改良版牛顿迭代:代码长度与效率的 Balance

在比较普遍的做法中,我们倍增时通常会递归地计算需要的一些子部分,例如大部分初等函数中途都会用到倒数,而倍增的时候倒数内部本来是没有必要重复计算一些东西的。这时为了效率,我们必须改递归为同轮递推的倍增方法,也可以认为是一种记忆化搜索。这样一来我们调用的时候就达到了论文中真正等价的 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<Poly, Poly> 的协议。特点在于多次取模同一个多项式的时候,可以预处理这个多项式翻转后的逆元的 NTT 的值。这样能够有效节省之后重复的计算。在单次乘法消耗大的情况下,减少快速幂过程中乘法的次数是极为有效的减小常数的方法,由四毛子 (Four Russians) 方法可以做到 log 2 n + Θ ( log n log log n ) \log_2 n + \Theta\left(\frac{\log n}{\log \log n}\right) 次乘法。所以封装了一个快速幂装置

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]);
  }
};

猜你喜欢

转载自blog.csdn.net/EI_Captain/article/details/88628618