多项式基本运算

摘要

操纵 有限项/无限项 的多项式是 OI 数学中,尤其是生成函数中的重要内容。

以 快速傅里叶变换 为基石的多项式算法赋予了算法竞赛选手直接操纵生成函数的能力。

摘自 OI-Wiki

多项式的核心操作为多项式乘法,以此为延伸,可以进行一系列运算。例如:多项式求逆,多项式开根,多项式除法,多项式对数函数,多项式指数函数,多项式牛顿迭代,多项式幂函数等。

下面逐一剖析这些操作。

约定

多项式 f(x)f(x) 定义为 f(x)=a0+a1x+a2x2++anxnf(x)=a_{0}+a_{1}x+a_{2}x^{2}+\cdots+a_{n}x^{n} 的形式,其中 {ai}\{a_i\} 被称为系数序列,使用这种方式表示多项式被称为 系数表示法

yf(x)y\rightarrow f(x) 表示将 yy 代入多项式 f(x)f(x) 得到的结果。

由拉格朗日插值定理得到:使用 n+1n+1 个二元组 (xi,yi)(x_i,y_i),其中 xix_i 两两不同,满足 xif(x)=yix_i\rightarrow f(x)=y_i,可以唯一确定一个 nn 次多项式。将这 n+1n+1 个二元组称作为该多项式的 点值表示法

多项式乘法

朴素算法

多项式的乘法操作也被称为加法卷积操作。对给定多项式 f(x)f(x)g(x)g(x)

f(x)=a0+a1x+a2x2++anxng(x)=b0+b1x+b2x2++bmxmf(x)=a_{0}+a_{1}x+a_{2}x^{2}+\cdots+a_{n}x^{n}\\ g(x)=b_{0}+b_{1}x+b_{2}x^{2}+\cdots+b_{m}x^{m}\\

其乘法结果为

i=0nj=0maibjxi+j\sum_{i=0}^{n}\sum_{j=0}^{m}a_{i}b_{j}x^{i+j}

有时也写作

i+j=kaibjxk\sum_{i+j=k}a_ib_jx^{k}

这也是为什么其被称为加法卷积。

直接根据定义计算复杂度是 O(nm)O(nm) 的。

单位根

如果多项式环上存在 2n2^{n} 次单位根,那么可以在 O(n2n)O(n2^{n}) 的时间内计算两个 2n2^{n} 次多项式的乘积。

典型的两个例子是快速傅里叶变换和快速数论变换。复数域存在任意 2n2^{n} 次的单位根,而快速数论变换中单位根的数量取决于模数。

定义 nn 次单位根 ωni,0i<n\omega_{n}^{i},0\le i<nnn 个,具有以下性质:

  1. ωnaωnb=ωn(a+b)modn\omega_{n}^{a}\omega_{n}^{b}=\omega_{n}^{(a+b)\bmod n}

  2. (ωna)b=ωnabmodn(\omega_{n}^{a})^{b}=\omega_{n}^{ab\bmod n}

对于 2n2\vert n,有

  1. ωn2i=ωn2i\omega_{n}^{2i}=\omega_{\frac{n}{2}}^{i}

  2. ωni+n2=ωni\omega_{n}^{i+\frac{n}{2}}=-\omega_{n}^{i}

下面称 ωni\omega_{n}^{i} 为第 iinn 次单位根,在不引起混淆的情况下可能直接称为第 ii 个单位根。

单位根优化多项式乘法

如果将 nn 次单位根依次代入多项式,得到该多项式的点值表示法。容易发现,对于 nn 次多项式,点值相乘的复杂度为 O(n)O(n)。得到点值乘积后,我们可以将点值乘积转换成系数表示法。

也就是说,使用单位根进行多项式乘法有三步:

  1. 将单位根代入多项式。

  2. 将点值相乘。

  3. 用点值乘积求系数表示。

而单位根的性质使得,对于 22 的整幂 nn,我们可以在 O(nlogn)O(n\log n) 时间内,对一个 nn 项多项式(即 n1n-1 次)完成第一步和第三步。

第一步

对于多项式 f(x)=a0+a1x+a2x2++an1xn1f(x)=a_0+a_1x+a_2x^{2}+\cdots+a_{n-1}x^{n-1},其中 nn 是二的整数幂。我们定义 h(x)=a0+a2x+a4x2+an2xn22h(x)=a_0+a_2x+a_4x^{2}+\cdots a_{n-2}x^{\frac{n-2}{2}}g(x)=a1+a3x+a5x2+an1xn22g(x)=a_1+a_3x+a_5x^{2}+\cdots a_{n-1}x^{\frac{n-2}{2}}

根据定义可以自然得到:

f(x)=h(x2)=xg(x2)\begin{equation} f(x)=h(x^2)=xg(x^2) \end{equation}

换句话说,我们提取 f(x)f(x) 的系数,按照次数的奇偶性分类,得到两个 n2\frac{n}{2} 项式。

ωnif(x)\omega_{n}^{i}\rightarrow f(x),其中 i<n2i<\frac{n}{2}。由 (1) 式得到

ωnif(x)=(ωni)2h(x)+ωni[(ωni)2g(x)]\omega_{n}^{i}\rightarrow f(x)=(\omega_{n}^{i})^2\rightarrow h(x)+\omega_{n}^{i}[(\omega_{n}^{i})^2\rightarrow g(x)]

根据单位根性质进行变换,得到 (ωni)2=ωn2i=ωn2i(\omega_{n}^{i})^2=\omega_{n}^{2i}=\omega_{\frac{n}{2}}^{i},也就是说,我们有

ωnif(x)=ωn2ih(x)+ωni(ωn2ig(x))\begin{equation} \omega_{n}^{i}\rightarrow f(x)=\omega_{\frac{n}{2}}^{i}\rightarrow h(x)+\omega_{n}^{i}(\omega_{\frac{n}{2}}^{i}\rightarrow g(x)) \end{equation}

ωni+n2f(x)\omega_{n}^{i+\frac{n}{2}}\rightarrow f(x),其中 i<n2i<\frac{n}{2}。由 (1) 式得到

ωni+n2f(x)=(ωni+n2)2h(x)+ωni+n2[(ωni+n2)2g(x)]\omega_{n}^{i+\frac{n}{2}}\rightarrow f(x)=(\omega_{n}^{i+\frac{n}{2}})^2\rightarrow h(x)+\omega_{n}^{i+\frac{n}{2}}[(\omega_{n}^{i+\frac{n}{2}})^2\rightarrow g(x)]

根据单位根性质进行变换,得到 (ωni+n2)2=ωn2i+n=ωn2i(\omega_{n}^{i+\frac{n}{2}})^2=\omega_{n}^{2i+n}=\omega_{\frac{n}{2}}^{i},并且 ωni+n2=ωni\omega_{n}^{i+\frac{n}{2}}=-\omega_{n}^{i} 也就是说,我们有

ωni+n2f(x)=ωn2ih(x)ωni(ωn2ig(x))\begin{equation} \omega_{n}^{i+\frac{n}{2}}\rightarrow f(x)=\omega_{\frac{n}{2}}^{i}\rightarrow h(x)-\omega_{n}^{i}(\omega_{\frac{n}{2}}^{i}\rightarrow g(x)) \end{equation}

观察 (2) 式与 (3) 式,可以发现,两式只有一个符号的区别。也就是说,只要知道 ωn2ih(x)\omega_{\frac{n}{2}}^{i}\rightarrow h(x)ωn2ig(x)\omega_{\frac{n}{2}}^{i}\rightarrow g(x),就能够在 O(n)O(n) 的时间内得到 ωnif(x)\omega_{n}^{i}\rightarrow f(x)。而求上述两者是一个规模更小的子问题,可以递归解决。

至此我们在 O(nlogn)O(n\log n) 的时间内解决了第一步。

第三步

fi=ωnif(x)f_i=\omega_{n}^{i}\rightarrow f(x),记多项式 g(x)=f0+f1x+f2x2++fn1xn1g(x)=f_0+f_1x+f_2x^{2}+\cdots+f_{n-1}x^{n-1},那么 f(x)f(x) 的系数满足 ai=ωnig(x)na_i=\dfrac{\omega_{n}^{-i}\rightarrow g(x)}{n}

证明

fif_i 的定义得

fi=a0+a1ωni+a2ωn2i+ωni(n1)f_i=a_0+a_1\omega_{n}^{i}+a_2\omega_{n}^{2i}+\cdots\omega_{n}^{i(n-1)}

ωnjg(x)\omega_{n}^{-j}\rightarrow g(x)fixif_ix^{i} 项的值为 ωijfi\omega^{-ij}f_i

展开 fif_i 得到:

a0ωi(0j)+a1ωni(1j)+a2ωni(2j)+ωni(n1j)a_0\omega^{i(0-j)}+a_1\omega_{n}^{i(1-j)}+a_2\omega_{n}^{i(2-j)}+\cdots\omega_{n}^{i(n-1-j)}

对于其中一项 aka_k 考虑,其贡献为 akωi(kj)a_k\omega^{i(k-j)}

k=jk=j 时,其贡献为 nakna_{k}

kjk\not=j 时,i(kj)i(k-j)nn 取模的剩余系取遍 [0,n)[0,n),所以总贡献为 i=0n1ωniak\sum_{i=0}^{n-1}\omega_{n}^{i}a_k,这是等比数列求和的形式,代入公式,得到总贡献为 ak(1ωnn)1ωn1=0\dfrac{a_k(1-\omega_n^{n})}{1-\omega_{n}^{1}}=0

故对于 aka_k,只有代入 ωnj,j=k\omega_{n}^{j},j=k 时,结果为 nakna_k,其它时候为 00。于是我们成功证明了上述结论。

所以再做一次类似第一步的操作即可还原系数。

快速傅里叶变换

对于 2n2^{n} 次多项式 f(x)f(x)g(x)g(x),快速傅里叶变换可以在 O(n2n)O(n2^{n}) 的复杂度内计算定义在复数域的多项式乘法。

在快速傅里叶变换中,取 ωni=cos(inπ)+sin(inπ)ı\omega_{n}^{i}=\cos(\frac{i}{n}\pi)+\sin(\frac{i}{n}\pi)\imath,其中 ı\imath 表示虚数单位根。

将第 ii 个单位根绘制到复数坐标系上,可以发现这些单位根位于 nn 等分单位圆的点上。可以验证这些单位根满足上述要求的性质。

如此我们得到了快速傅里叶变换算法。

实现上,可以直接分治,但是常数较大,且不容易实现。推荐使用倍增法进行实现。

具体来说,先使用被称为 蝶形运算优化 的技巧来合并减小不必要的内存拷贝。观察系数 aia_i 在分治到规模为 11 的问题时,其位置恰好为 ii 在二进制下的反转。为什么呢?每一次分治,相当于是将二进制最低位为 00 的放到序列左侧,将最低位为 11 的放到右侧。那么新序列最高位是 00 的,即左侧,原来最低位为 00。也就是说,分治的过程相当于翻转了二进制位。

f[i] 表示 i 的二进制翻转后的结果。则可以递推得到,f[i]=f[i>>1]|((i&1)?(1<<s):0),其中 ssnn 的二进制最高位的位数。

下面给出 洛谷模板题 的代码。

Sample Code(c++)
c++
#include <bits/stdc++.h>
using namespace std;

const int N = 1 << 21;
const int mod = 998244353, RT = 3;

using ll = long long;
ll qpow(ll a, ll b) {
  ll res = 1;
  for (; b; b >>= 1, a = a * a % mod)
    if (b & 1) res = res * a % mod;
  return res;
}
void ntt(int *a, int n, bool inv) {
  static int G[N], invG[N], rev[N];
  static int lst_n = 0;
  if (n != lst_n) {
    lst_n = n;
    for (int i = 0; i < n; ++i)
      rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (n >> 1) : 0);
    for (int i = 1; i < n; i <<= 1) {
      int g1 = qpow(RT, (mod - 1) / (i << 1)), ig1 = qpow(g1, mod - 2);
      int g = 1, ig = 1;
      for (int j = i; j < i + i; ++j) {
        G[j] = g, invG[j] = ig;
        g = 1ull * g * g1 % mod, ig = 1ull * ig * ig1 % mod;
      }
    }
  }
  for (int i = 0; i < n; ++i)
    if (rev[i] < i) swap(a[rev[i]], a[i]);
  for (int i = 1, x; i < n; i <<= 1) {
    for (int *j = a; j < a + n; j += (i << 1)) {
      for (int *k = j, *buf = (inv ? invG : G) + i; k < j + i; ++k, ++buf) {
        x = 1ull * k[i] * *buf % mod;
        if ((k[i] = *k + mod - x) >= mod) k[i] -= mod;
        if ((*k += x) >= mod) *k -= mod;
      }
    }
  }
  if (inv) {
    int invn = qpow(n, mod - 2);
    for (int i = 0; i < n; ++i)
      a[i] = 1ull * a[i] * invn % mod;
  }
}
void mul(int *a, int *b, int l1, int l2) {
  int n = 1;
  while (n <= l1 + l2) n <<= 1;
  ntt(a, n, false), ntt(b, n, false);
  for (int i = 0; i < n; ++i) a[i] = 1ull * a[i] * b[i] % mod;
  ntt(a, n, true);
}

int a[N], b[N];
int main() {
  cin.tie(0)->sync_with_stdio(false);
  int n, m;
  cin >> n >> m;
  for (int i = 0; i <= n; ++i) cin >> a[i];
  for (int i = 0; i <= m; ++i) cin >> b[i];
  mul(a, b, n, m);
  for (int i = 0; i <= n + m; ++i)
    cout << a[i] << ' ';
  cout << '\n';
}

快速数论变换

由于快速傅里叶变换使用了复数,不可避免的引起了精度误差问题,并且实数运算是低效的,我们使用原根的一些性质来替代单位根,使得运算更为准确快捷。

参见:原根与快速数论变换

快速沃尔什变换
随机数与随机函数