多项式学习笔记(Updating)

1. 复数,单位根,原根

1.1 复数的表示

  1. 代数表示:\(z = a+b_i\),记 \(\overline z\) 的共轭为 \(\overline z = a-bi\)
  2. 三角表示:记复数的模为 \(|z| = r = \sqrt{a^2+b^2}\),复数的辐角为 \(\mathrm{Arg}\space z = \theta = \frac b a\)\(\theta \in (-\pi, \pi]\) 的辐角称作辐角主值,记作 \(\arg z = \arctan \frac b a\),那么 \(z = r(\cos\theta+i\sin\theta)\)
  3. 用欧拉公式表示:\(z=re^{i\theta}\)

1.2 复数的运算

\[(a+bi)\pm(c+di)=(a\pm c)+(b\pm d)i \]

\[(a+bi)(c+di)=(ac-bd)+(ad+bc)i \]

\[\dfrac{a+bi}{c+di}=\dfrac{(a+bi)(c-di)}{c^2+d^2}=\dfrac{ac+bd}{c^2+d^2}+\dfrac{bc-ad}{c^2+d^2}i \]

\[z_1z_2=r_1r_2(\cos(\theta_1+\theta_2)+i\sin(\theta_1+\theta_2)) \]

1.3 单位根

\(r = 1\) 的复数在复平面的单位圆上,其 \(n\) 次幂为 \(\cos n\theta + i\sin n\theta\),即在单位圆上从 \((1, 0)\) 开始以 \(\theta\) 的角度顺时针旋转了 \(n\) 次。
考虑 \(n\) 等分单位圆,记逆时针第 \(k\) 个(从 \(0\) 开始标号)等分点为 \(\omega_n^k = \cos(\frac{2k\pi} n)+i\sin(\frac{2k\pi}n)\)。所有的 \(\omega_n^k\) 记为 \(n\) 次单位根。\(\omega_n^1\) 简记为 \(\omega_n\)。显然 \((\omega_n^k)^n=1\)。若 \(\gcd(n, k) = 1\),称 \(\omega_n^k\) 为本源单位根,所有本源单位根的 \(0 \sim n - 1\) 次幂是互不相同的。
另外,根据欧拉公式,我们有 \(\omega_n^k=e^{\frac{2k\pi}ni}\)
性质:

  1. \(\omega_n^k = \omega_n^{k+tn}, t\in \mathbb Z\)
  2. \(\omega_n^k=\omega_{tn}^{tk}, t \in \mathbb Z\)。由定义直接得出。
  3. \(n\) 为偶数时,\(-\omega_n^k=\omega_n^{k\pm\frac n2}\)。相当于在单位圆上顺时针或逆时针转半圈。
  4. \(\sum_{i=0}^{n-1}\omega_n^i=0\),等比数列求和可得。

1.4 原根

原根可以看做是模 \(p\) 意义下的单位根,设 \(n = \varphi(p)\),若 \(\delta_p(g)=n\),则 \(g\) 为模 \(p\) 的原根,如果模 \(p\) 的原根存在,则 \(g^i \bmod p, i \in [0, n - 1]\) 两两不同,这和单位根的性质极其相似,模 \(p\) 意义下我们可以直接用 \(g\) 代替 \(n\) 次单位根,对于 \(d\mid n\),也可以用 \(g^{\frac n d}\) 代替 \(d\) 次单位根(可以用 \(\omega_d=\omega_n^{\frac n d}\) 理解)。

2. 多项式

形如 \(\sum_{i = 0}^n a_ix^i\) 的和式称为多项式,记为 \(f(x) = \sum_{i=0}^n a_ix^i\),其最高系数非零项记作 \(\deg f\),若 \(a_n \neq 0\)\(\deg f = n\)。所有 \(f(x) = 0\)\(x\) 称为多项式的根,一个\(n\) 次多项式有 \(n\) 个复数根,这些根可能有重合。也可以用 \(f_i\) 表示 \(f\)\(i\) 次项系数。

2.1 表示方法

系数表示

\(a_i\) 组成的序列来表示。

点值表示

我们有 \(n\) 个点值 \((x_0, y_0), (x_1, y_1), \dots, (x_{n - 1}, y_{n - 1})\),若 \(x_i\) 互不相同,则这 \(n\) 个点值可以唯一确定一个最高次数为 \(n - 1\) 的多项式。

从系数表示法转为点值表示法称为求值。
从点值表示法转为系数表示法称为插值。

2.2 运算

\(f(x) = \sum_{i=0}^n a_ix^i, g(x) = \sum_{i=0}^m b_ix_i\),则

\[f(x) \pm g(x) = (f\pm g)(x) = \sum_{i=0}^{\max(n, m)} (a_i\pm b_i)x_i \]

\(\deg(f+g) = \max(\deg f, \deg g)\)

\[f(x)g(x) = (f * g)(x) = \sum_{i=0}^{n+m}(\sum_{j=0}^i a_jb_{i-j}) x^i \]

\(\deg(f* g)=\deg f + \deg g\)
\(f* g\)\(fg\) 为加法卷积,而 \(f \cdot g\) 表示点乘,两者系数相乘。

计算两个多项式相加,在系数表示法下时间复杂度为 \(\mathcal{O}(n + m)\),点值表示法下要 \(\max(n, m) + 1\) 个点值才能确定,时间复杂度 \(\mathcal{O}(n + m)\)。计算两个多项式相乘,在系数表示法下时间复杂度为 \(\mathcal{O}(nm)\),点值表示法下要 \(n + m + 1\) 个点值才能确定,时间复杂度 \(\mathcal{O}(n + m)\)

2.3 次数上界

我们用 \(\pmod {x^n}\) 来截断一个多项式,只保留 \(0 \sim n - 1\) 次项,\(F(x) \equiv G(x) \pmod {x^n}\)\(G(x) = \sum_{i=0}^{n-1}F_ix^i\)

3. 拉格朗日插值

3.1 算法介绍

我们有 \(n + 1\) 个点 \((x_0, y_0), (x_1, y_1), \dots, (x_n, y_n)\),要求出一个 \(n\) 次多项式 \(f(x)\) 满足 \(f(x_i) = y_i\)

3.1.1 常规形式

我们构造 \(n\) 个函数 \(f_1(x), \dots, f_n(x)\)\(f_i(x)\) 的图象过 \(\begin{cases} (x_j, 0), & (j\neq i) \\ (x_j, y_j), & (j = i) \end{cases}\),那么 \(f(x) = \sum_{i=1}^n f_i(x)\)

我们设 \(f_i(x) = a\prod_{j\neq i}(x - x_j)\),带入 \((x_i, y_i)\)\(a = \frac{y_i}{\prod_{j\neq i} (x_i-x_j)}\)。即

\[f_i(x) = y_i \cdot \prod\limits_{j \neq i} \frac{x-x_j}{x_i-x_j} \]

\[f(x) = \sum\limits_{i=1}^ny_i \cdot \prod\limits_{j \neq i} \frac{x-x_j}{x_i-x_j} \]

时间复杂度 \(\mathcal{O}(n^2)\)。但是可以优化到 \(\mathcal{O}(n \log^2 n)\),具体见 5.?.?,我还没写。

为了求出 \(f(x)\) 的系数,考虑预处理 \(M = \prod\limits_{i=1}^n (x - x_i)\) 的各项系数,\(px_i = \prod\limits_{j\neq i}(x_i - x_j)\),然后就好求了。
洛谷模版题代码,0-indexed:

namespace Loop1st {
int n, k;
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;
}
vector<ll> lagrange(const vector<ll> &x, const vector<ll> &y) {
    vector<ll> M(n + 1), px(n, 1), f(n);
    M[0] = 1;
    for (int i = 0; i < n; i++) {
        for (int j = i; j >= 0; j--) {
            M[j + 1] = (M[j + 1] + M[j]) % mod;
            M[j] = M[j] * (mod - x[i]) % mod;
        }
    }
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) if (i != j) {
            px[i] = px[i] * (x[i] + mod - x[j]) % mod;
        }
    }
    for (int i = 0; i < n; i++) {
        ll t = y[i] * qpow(px[i], mod - 2) % mod, now = M[n];
        for (int j = n - 1; j >= 0; j--) {
            f[j] = (f[j] + now * t) % mod;
            now = (now * x[i] + M[j]) % mod;
        }
    }
    return f;
}
void Main(int tc) {
    cin >> n >> k;
    vector<ll>x(n), y(n);
    for (int i = 0; i < n; i++) cin >> x[i] >> y[i];
    const auto f = lagrange(x, y);
    ll pw = 1, ans = 0;
    for (int i = 0; i < n; i++) ans = (ans + pw * f[i]) % mod, pw = pw * k % mod;
    cout << ans << '\n';
}

}

3.1.2 取值连续的形式

即给出 \(n\) 个点,但是 \(x_i\) 是从小到大连续的 \(n\) 个数。
则公式简化为

\[f(x) = \sum\limits_{i=1}^ny_i \cdot \prod\limits_{j \neq i} \frac{x-x_j}{i-j} \]

对于分子容易通过前后缀预处理得到,分母考虑对 \(i > j\)\(i < j\) 分讨,\(i < j\) 的时候可能是负的,讨论一下得到

\[f(x) = \sum\limits_{i=1}^ny_i \cdot\frac{1}{(-1)^{n-i}(i-1)!(n-i)!}\prod\limits_{j < i} (x-x_j) \prod\limits_{j > i} (x-x_j) \]

于是可以 \(\mathcal{O}(n)\) 计算单点的值。但是似乎不能 \(\mathcal{O}(n)\) 计算出所有系数。
代码见例题。

3.1.3 动态问题

我们插入一个点值时,容易 \(\mathcal{O}(n)\) 重新插值,可以 \(\mathcal{O}(n)\) 求任意点值,但是求系数似乎还是要 \(\mathcal{O}(n^2)\)

3.2 例题

CF622F

考虑到 \(f_n = \sum\limits_{i=1}^n i^k\),则 \(f_n\) 的差分为 \(n^k\),是一个 \(k\) 次多项式,所以 \(f_n\) 是一个 \(k + 1\) 次多项式,所以找 \(k + 2\) 个连续的点插值,最后求 \(f_n\) 即可。时间复杂度 \(\mathcal{O}(k)\)

namespace Loop1st {
int n, k, b[N], p[N], tot;
ll f[N], pre[N], suf[N], fac[N], ifac[N];
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 Main(int tc) {
    cin >> n >> k;
    f[1] = 1;
    for (int i = 2; i <= k + 2; i++) {
        if (!b[i]) { p[++tot] = i; f[i] = qpow(i, k); }
        for (int j = 1; j <= tot && i * p[j] <= k + 2; j++) {
            b[i * p[j]] = 1;
            f[i * p[j]] = f[i] * f[p[j]] % mod;
            if (i % p[j] == 0) break;
        }
        f[i] = qpow(i, k);
    }
    for (int i = 2; i <= k + 2; i++) f[i] = (f[i] + f[i - 1]) % mod;
    if (n <= k + 2) { cout << f[n] << '\n'; return ; }
    pre[0] = suf[k + 3] = 1;
    for (int i = 1; i <= k + 2; i++) pre[i] = pre[i - 1] * (n - i) % mod;
    for (int i = k + 2; i; i--) suf[i] = suf[i + 1] * (n - i) % mod;
    fac[0] = ifac[0] = 1;
    for (int i = 1; i <= k + 2; i++) fac[i] = fac[i - 1] * i % mod;
    ifac[k + 2] = qpow(fac[k + 2], mod - 2);
    for (int i = k + 1; i; i--) ifac[i] = ifac[i + 1] * (i + 1) % mod;
    ll ans = 0;
    for (int i = 1; i <= k + 2; i++) {
        ll sgn = (k + 2 - i) & 1 ? mod - 1 : 1;
        ans = (ans + sgn * ifac[i - 1] % mod * ifac[k + 2 - i] % mod * pre[i - 1] % mod * suf[i + 1] % mod * f[i] % mod) % mod;
    }
    cout << ans << '\n';
}

}

calc

我们不考虑顺序,从小到大填数,设 \(f_{i, j}\) 为考虑到 \([1, i]\),填了 \(j\) 个数的答案,则 \(f_{i, j} = f_{i - 1, j} + if_{i - 1, j - 1}\),答案即 \(n!f_{n, k}\),把 \(f_{i, j}\) 当成关于 \(i\) 的多项式,差分一下,\(f_j(i) - f_j(i - 1) = if_{j - 1}(i - 1)\),所以 \(f_j(i)\) 的差分的次数是 \(f_{j-1}(i-1)\) 的次数 \(+1\)\(f_j(i)\) 的次数是 \(f_{j-1}(i-1)\) 的次数 \(+2\),而 \(f_0(i)\) 的次数是 \(0\),所以 \(f_n(k)\)\(2n\) 次多项式。带入 \(2n + 1\) 个点值即可,时间复杂度 \(\mathcal{O}(n^2)\)

namespace Loop1st {
int n, m, mod, b[N], p[N], tot;
ll k, f[N][N], pre[N], suf[N], fac[N], ifac[N];
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 Main(int tc) {
    cin >> k >> n >> mod;
    m = n << 1 | 1;
    for (int i = 0; i <= m; i++) f[i][0] = 1;
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            f[i][j] = (f[i - 1][j] + f[i - 1][j - 1] * i) % mod;
        }
    }
    fac[0] = ifac[0] = 1;
    for (int i = 1; i <= m; i++) fac[i] = fac[i - 1] * i % mod;
    ifac[m] = qpow(fac[m], mod - 2);
    for (int i = m - 1; i; i--) ifac[i] = ifac[i + 1] * (i + 1) % mod;
    if (k <= m) { cout << fac[n] * f[k][n] % mod << '\n'; return ; }
    pre[0] = suf[m + 1] = 1;
    for (int i = 1; i <= m; i++) pre[i] = pre[i - 1] * (k - i) % mod;
    for (int i = m; i; i--) suf[i] = suf[i + 1] * (k - i) % mod;
    ll ans = 0;
    for (int i = 1; i <= m; i++) {
        ll sgn = (m - i) & 1 ? mod - 1 : 1;
        ans = (ans + sgn * ifac[i - 1] % mod * ifac[m - i] % mod * pre[i - 1] % mod * suf[i + 1] % mod * f[i][n] % mod) % mod;
    }
    cout << fac[n] * ans % mod << '\n';
}

}

4. FFT & NTT

这个部分的代码参考了 Alex_Wei。

4.1 点值、系数表示的转化

\(f(x) = \sum_{i=0}^{n-1} a_ix^i\),我们考虑现在给定互不相同的 \(m - 1\) 个位置 \(x_0, x_1, x_2, \dots, x_{m - 1}\),需要求 \(f(x_0), f(x_1), f(x_2), \dots, f(x_{m - 1})\) 的值,用矩阵乘法的形式描述问题:

\[\begin{bmatrix} x_0^0 & x_0^1 & \dots & x_0^{n-1}\\ x_1^0 & x_1^1 & \dots & x_1^{n-1}\\ \vdots & \vdots & \ddots & \vdots\\ x_{m-1}^0 & x_{m-1}^1 & \dots & x_{m-1}^{n-1} \end{bmatrix} \times \begin{bmatrix} a_0\\ a_1\\ \vdots \\ a_{n-1} \end{bmatrix}=\begin{bmatrix} f(x_0)\\ f(x_1)\\ \vdots \\ f(x_{n-1}) \end{bmatrix}\]

左侧矩阵叫做范德蒙德矩阵(\(n = m\) 时称作范德蒙德方阵),\(m < n\) 时无法求逆,\(m \ge n\) 时可以选出 \(x_i\) 互不相同的 \(n\) 行求逆,通过点值乘上范德蒙德矩阵的逆还原系数。

朴素求值的复杂度为 \(\mathcal{O}(nm)\)

4.2 DFT

DFT 就是把一个复数序列 \(x_0, x_1, \dots, x_{N - 1}\) 用如下公式转化为另一个复数序列 \(X_0, X_1, \dots, X_{N-1}\)

\[X_k = \sum\limits_{n=0}^{N-1} x_n\omega_N^{-kn} \]

\(f(x) = \sum_{i=0}^{N-1}a_ix^i\),则

\[X_k=f(\omega_N^{-k}) \]

即可以看成 \(f(x)\)\(N\) 个单位根处求值。

4.3 FFT

朴素地求出 \(n^2\) 个点值仍然是 \(\mathcal{O}(n^2)\) 的。我们现在的目标是给定一个 \(n - 1\) 次多项式 \(f(x) = \sum_{i=0}^{n-1}a_ix^i\),求出其至少 \(n\) 个点值。但 FFT 所实现的变换又和 DFT 有着细微的差别,具体体现在两者的范德蒙德矩阵带入单位根的顺序是相反的,且 DFT 不要求长度为 \(2\) 的整幂。

4.3.1 朴素的 FFT

一个多项式函数可以表示为一个奇函数和一个偶函数的和。奇函数的偶次项系数都为 \(0\),偶函数的奇次项系数都为 \(0\)

不妨令 \(n\) 变为 \(2^{\left\lceil\log n\right\rceil}\)\(a_i = 0(i \ge n)\)。这样让问题变得更加方便。

考虑将 \(f(x)\) 拆成偶函数 \(f_e(x)\) 和奇函数 \(f_o(x)\) 之和,则

\[f(x) = f_e(x)+f_o(x), f(-x)=f_e(x)-f_o(x) \]

我们现在选取 \(n\) 个位置 \(x_0, x_1, \dots, x_{n-1}\),并且 \(x_{i+\frac n2}=-x_i(i\in [0, \frac n2-1])\),求解 \(f_e(x_i)\)\(f_o(x_i)(i \in [0, \frac n2-1])\) 就可以求出 \(f(x)\) 的至少 \(n\) 个点值。由于 \(f_e(x)\) 只有偶次项,\(f_o(x)\) 只有奇次项,我们可以把 \(f_e(x)\)\(f_o(x)\) 变为 \(\frac n2\) 次项,即 $$g(x) = a_0+a_2x+a_4x2+\dots+a_{n-2}x=\sum_{i=0}^{\frac n2-1}a_{2i}x^i$$

\[h(x)=a_1+a_3x+a_5x^2+\dots+a_{n-1}x^{\frac n2-1}=\sum_{i=0}^{\frac n2-1}a_{2i+1}x^i \]

那么 \(f(x) = g(x) + xh(x), f(-x)=g(x)-xh(x)\)
这样我们就把规模减半了。

如果我们选取的 \(x_i\) 满足前 \(\frac n2\) 个数和后 \(\frac n2\) 个数互为相反数,并且前 \(\frac n2\) 个数平方后相反数仍然满足前 \(\frac n4\) 个数和后 \(\frac n4\) 个数互为相反数,依次类推,即前 \(\frac n{2^i}\) 个数前 \(\frac n{2^{i+1}}\) 个数的 \(2^i\) 次方和后 \(\frac n{2^{i+1}}\) 个数的 \(2^i\) 次方互为相反数,那么我们就可以层层递归下去,递归 \(\log n\) 层后求出所有点值。

我们发现 \(x_i = \omega_n^i\) 恰好满足这个需求,用 \(\omega_n^k=-\omega_n^{k+\frac n2}\) 这个性质即可证明。

然后我们再尝试化简一下,

\[f(\omega_n^k)=g(\omega_{\frac n2}^k)+\omega_n^kh(\omega_{\frac n2}^k) \]

\[f(\omega_n^{k+\frac n2})=g(\omega_{\frac n2}^k)-\omega_n^kh(\omega_{\frac n2}^k) \]

然后递归下去求解即可,时间复杂度 \(\mathcal{O}(n \log n)\)

递归的常数比较大,下面介绍非递归的写法。

4.3.2 蝴蝶变换

考虑到递归的最后一层,若当前所处理的位置的系数为 \(a_p\),则这次递归处理的 \(f(x) = a_p\),我们如果知道最后一层 \(a_p\) 的顺序,就可以递推地进行 FFT 了,比如,初始系数为 \((a_0, a_1, a_2, a_3, a_4, a_5, a_6, a_7)\),递归第一次,左边 \(g(x)\) 的系数为 \((a_0, a_2, a_4, a_6)\),右边 \(h(x)\) 的系数为 \((a_1, a_3, a_5, a_7)\),递归第二次,变为 \((a_0, a_4), (a_2, a_6), (a_1, a_3), (a_5, a_7)\)。在第 \(k\) 层向下递归,\(a_i\) 被分到左边还是右边(\(g\) 还是 \(h\)),取决于 \(\left\lfloor \frac i{2^{k-1}} \right\rfloor\) 的奇偶性,即 \(i\) 从低到高第 \(k\) 位是 \(0\) 还是 \(1\)。即考虑令 \(r_i\) 表示 \(i\) 二进制表示的前 \(\log n\) 位(不足用 \(0\) 补齐)翻转后的数,如 \(r_{11}=(\overline{1011})_2=(1101)_2=13\)。那么最后 \(a_p\) 的顺序即 \(r_p\) 从小到大的顺序。\(r_i\) 容易 \(\mathcal{O}(n)\) 递推得到。

有了最后一层的顺序我们就可以自底向上递推了,先同时让 \(a_{r_i} \gets a_i\),由于 \(r_{r_i}=i\),也要让 \(a_i \gets a_{r_i}\),这可以通过交换所有 \(a_i, a_{r_i}(i < r_i)\) 来实现。然后从小到大枚举问题规模 \(2d\),每个子问题为 \([i, i + 2d)\),其中 \(i = 2kd\)\([i, i + d)\)\([i + d, i + 2d)\) 都已在之前处理过,那么直接按照上述递归 FFT 的公式,枚举一个 \(j \in [0, d)\),令 \(x = i + j, y = i + j + d\)\(f'_k\) 表示当前规模下的 \(f(\omega_{2d}^k)\)\(f_k\) 表示上一规模下的 \(f(\omega_d^k)\),则

\[f'_x = f_x+\omega_{2d}^jf_y,f'_y=f_x-\omega_{2d}^jf_y \]

这个过程就是 FFT。

4.4 IFFT

如何将点值转化为系数?范德蒙德矩阵的本质是系数转化为点值,而范德蒙德矩阵的逆就是点值转化为系数,即插值。
设范德蒙德矩阵为 \(F\),其逆为 \(F^{-1}\)

\[\begin{bmatrix} x_0^0 & x_0^1 & \dots & x_0^{n-1}\\ x_1^0 & x_1^1 & \dots & x_1^{n-1}\\ \vdots & \vdots & \ddots & \vdots\\ x_{m-1}^0 & x_{m-1}^1 & \dots & x_{m-1}^{n-1} \end{bmatrix} \times \begin{bmatrix} a_0\\ a_1\\ \vdots \\ a_{n-1} \end{bmatrix}=\begin{bmatrix} f(x_0)\\ f(x_1)\\ \vdots \\ f(x_{n-1}) \end{bmatrix}\]

两边左乘 \(F^{-1}\),则 \(f(x_j)\)\(a_i\) 的贡献为 \((F^{-1})_{i, j}\)。将 \(x_{i} = \omega_n^i\) 带入拉格朗日插值的式子:

\[f(x)=\sum\limits_{i=0}^{n-1} f(x_i)\prod\limits_{j\neq i} \dfrac{x-\omega_n^j}{\omega_n^i-\omega_n^j} \]

所以 \((F^{-1})_{i, j} = [x^i]\prod\limits_{k \neq j}\frac{x-\omega_n^k}{\omega_n^j-\omega_n^k}\)。考虑令

\[g_j(x) = \frac{\prod\limits_{k=0}^nx-\omega_n^k}{x-\omega_n^j}=\frac{x^n-1}{x-\omega_n^j}=\sum\limits_{k=0}^{n-1}\omega_n^{jk}x^{n-1-k} \]

\[(F^{-1})_{i, j}=\frac{[x^i]g_j(x)}{g_j(\omega_n^j)}=\frac{\omega_n^{j(n-1-i)}}{n\omega_n^{j(n-1)}}=\frac{\omega_n^{-ij}}{n} \]

\(F\)\(F^{-1}\) 对照一下:

\[F=\begin{bmatrix} (\omega_n^0)^0 & (\omega_n^0)^1 & \dots & (\omega_n^0)^{n-1}\\ (\omega_n^1)^0 & (\omega_n^1)^1 & \dots & (\omega_n^1)^{n-1}\\ \vdots & \vdots & \ddots & \vdots\\ (\omega_n^{n-1})^0 & (\omega_n^{n-1})^1 & \dots & (\omega_n^{n-1})^{n-1} \end{bmatrix} \]

\[F^{-1}=\dfrac1n\begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \dots & (\omega_n^{-0})^{n-1}\\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \dots & (\omega_n^{-1})^{n-1}\\ \vdots & \vdots & \ddots & \vdots\\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \dots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix} \]

我们发现,\(F^{-1}\) 就是 \(F\) 的所有单位根次数取反,再将系数除以 \(n\)
所以,IFFT 的过程就是 FFT 的过程上进行一些简单的修改。

4.5 多项式乘法

4.5.1 朴素实现

现在,给定一个 \(n - 1\) 次多项式和一个 \(m - 1\) 次多项式 \(a, b\),要求出 \(c = a \times b\)\(c\)\(n + m - 2\) 次多项式,我们先将 \(n + m - 2\) 补全到 \(2\) 的幂次 \(L\),然后对 \(a, b\) 分别做长度为 \(L\) 的 FFT,点值相乘得到 \(\hat{c}\),再对 \(\hat{c}\) 做 IFFT 得到 \(c\)。时间复杂度 \(\mathcal{O}(n \log n)\)。下面给出 P3803 的代码。

namespace Loop1st {
int n, m, r[N];
struct Complex {
    double a, b;
    Complex(double _a = 0, double _b = 0): a(_a), b(_b) {}
    Complex operator + (const Complex &A) const { return {a + A.a, b + A.b}; }
    Complex operator - (const Complex &A) const { return {a - A.a, b - A.b}; }
    Complex operator * (const Complex &A) const { return {a * A.a - b * A.b, a * A.b + b * A.a}; }
} f[N], g[N], h[N];
void FFT(int L, Complex *f, bool type) { // type = 1: FFT type = 0: IFFT
    for (int i = 1; i < L; i++) {
        r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
        if (i < r[i]) swap(f[i], f[r[i]]);
    }
    for (int d = 1; d < L; d <<= 1) {
        Complex w1(cos(pi / d), sin(pi / d));
        if (!type) w1.b = -w1.b; // 指数取反相当于取倒数,对单位根来说相当于取共轭
        static Complex w[N];
        w[0] = {1, 0};
        for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1;
        for (int i = 0; i < L; i += (d << 1)) {
            for (int j = 0; j < d; j++) {
                Complex x = f[i | j], y = w[j] * f[i | j | d];
                f[i | j] = x + y, f[i | j | d] = x - y;
            }
        }
    }
    if (!type) for (int i = 0; i < L; i++) f[i].a /= L; // IFFT 最后只要输出实部就只除实部
}
void Main(int tc) {
    cin >> n >> m;
    for (int i = 0; i <= n; i++) cin >> f[i].a;
    for (int i = 0; i <= m; i++) cin >> g[i].a;
    int L = 1;
    while (L <= n + m) L <<= 1;
    FFT(L, f, 1); FFT(L, g, 1);
    for (int i = 0; i < L; i++) h[i] = f[i] * g[i];
    FFT(L, h, 0);
    for (int i = 0; i <= n + m; i++) cout << (int)(h[i].a + 0.5) << " \n"[i == n + m];
}

}

4.5.2 三次变两次优化

对于 \(a, b\) 系数为实数的情况下,根据 \((a + bi)^2 = (a^2 - b^2) +2abi\),我们将 \(a + bi\) 这个复系数多项式平方后取出虚部,除以 \(2\) 即可,进一步减小了常数。

namespace Loop1st {
int n, m, r[N];
struct Complex {
    double a, b;
    Complex(double _a = 0, double _b = 0): a(_a), b(_b) {}
    Complex operator + (const Complex &A) const { return {a + A.a, b + A.b}; }
    Complex operator - (const Complex &A) const { return {a - A.a, b - A.b}; }
    Complex operator * (const Complex &A) const { return {a * A.a - b * A.b, a * A.b + b * A.a}; }
} f[N];
void FFT(int L, Complex *f, bool type) {
    for (int i = 1; i < L; i++) {
        r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
        if (i < r[i]) swap(f[i], f[r[i]]);
    }
    for (int d = 1; d < L; d <<= 1) {
        Complex w1(cos(pi / d), sin(pi / d));
        if (!type) w1.b = -w1.b;
        static Complex w[N];
        w[0] = {1, 0};
        for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1;
        for (int i = 0; i < L; i += (d << 1)) {
            for (int j = 0; j < d; j++) {
                Complex x = f[i | j], y = w[j] * f[i | j | d];
                f[i | j] = x + y, f[i | j | d] = x - y;
            }
        }
    }
    if (!type) for (int i = 0; i < L; i++) f[i].b /= L;
}
void Main(int tc) {
    cin >> n >> m;
    for (int i = 0; i <= n; i++) cin >> f[i].a;
    for (int i = 0; i <= m; i++) cin >> f[i].b;
    int L = 1;
    while (L <= n + m) L <<= 1;
    FFT(L, f, 1);
    for (int i = 0; i < L; i++) f[i] = f[i] * f[i];
    FFT(L, f, 0);
    for (int i = 0; i <= n + m; i++) cout << (int)(f[i].b / 2 + 0.5) << " \n"[i == n + m];
}

}

4.5.3 合并两次实系数 FFT/IFFT

鸽。想学的请见 Alex_Wei 老师的 blog

4.5.4 拆系数 FFT

用于解决模 \(p\) 意义下的多项式乘法,且 \(p\) 不为 NTT 模数。
我们取 \(B = \lfloor\sqrt p\rfloor\),将系数拆成 \(f = f_0B + f_1, g = g_0B + g_1\) 的形式,其中 \(f_1, g_1\) 的所有系数 \(<B\),两两相乘得到 \(fg = (f_0g_0)B^2 + (f_0g_1+f_1g_0)B + f_1g_1\),然后对 \(f_0, f_1, g_0, g_1\) 进行 FFT,\(\hat{f_0}\hat{g_0}, \hat{f_0}\hat{g_1}+\hat{f_1}\hat{g_0},\hat{f_1}\hat{g_1}\) 进行 IFFT,得到一个 \(7\) 次 FFT 的做法,用三次变两次的优化可以变成 \(5\) 次 FFT。系数的值域为 \(nB^2 = np\),一般来说可以接受,但是卡精度要 long double。如果用 4.5.3 的技巧可以优化到 \(2\) 次 FFT 和 \(2\) 次 IFFT,效率会更优秀,我没有写代码。

4.6 NTT

4.6.1 朴素的 NTT

事实上,算法竞赛中的多项式题常常需要对一个固定模数 \(p\) 取模,这时我们就要引入 NTT 了。NTT 的本质是在模 \(p\) 意义下进行的 FFT。一般情况下我们也不写 FFT 只写 NTT。

设变换的长度为 \(n\),我们需要存在一个用于代替单位根 \(\omega_n\) 的数 \(a\) 满足 \(\delta_p(a) = n\),对于 \(p\) 为质数的情况,我们有 \(\delta_p(g) = p - 1\),我们发现 \(g^k\)\(\omega_{p-1}^k\) 是等价的。那么,\(q = g^{\frac{p-1}n}\) 等价于 \(\omega_{p-1}^{\frac{p-1}n}=\omega_n\),换句话说,\(q\) 满足以下性质:

  1. \(q^0 \sim q^{n-1}\) 互不相同
  2. \(q^k=q^{k\bmod n}\)
  3. \((g^{\frac{p-1}{2n}})^2=g^{\frac{p-1}n}\)
    这也隐性要求了 NTT 的模数 \(p\) 满足 \(p - 1\)\(n\) 的倍数,而 \(n\)\(2\) 的整幂,所以 \(2^N \mid p - 1\),其中 \(N \ge \log n\)

常见的 NTT 模数:

  • \(p=469762049 = 7\times 2^{26}+1\)
  • \(\color{red}{p=998244353=7\times 17 \times 2^{23} + 1}\)
  • \(p=1004535809=479\times 2^{21}+1\)
    这三个模数的最小原根都是 \(3\)
    如果不是常见模数,但是 \(p\) 形如 \(q2^N+1\) 的形式,且 \(N \ge \log n\),则也可以找一个原根然后 NTT。如果 \(p\) 是一般模数,请参见 4.5.4 的拆系数 FFT 或 4.6.2 三模 NTT。

当然其实如果存在 \(\delta_p(a) = n\) 的单位根 \(a\)\(n\) 的逆元 \(n^{-1}\),也是可以做 NTT 的。
下面给出多项式乘法的代码实现:

namespace Loop1st {
int n, m, r[N], f[N], g[N], h[N];
int qpow(int a, int b) {
    int res = 1;
    for (; b; b >>= 1, a = (ll)a * a % mod) if (b & 1) res = (ll)res * a % mod;
    return res;
}
void NTT(int L, int *a, bool type) {
    static ull w[N], f[N]; // 用 ull 减小常数
    w[0] = 1;
    for (int i = 0; i < L; i++) {
        r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
        f[i] = a[r[i]];
    }
    for (int d = 1; d < L; d <<= 1) {
        int w1 = qpow(type ? 3 : iv3, ((mod - 1) / d) >> 1);
        for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1 % mod;
        for (int i = 0; i < L; i += (d << 1)) {
            for (int j = 0; j < d; j++) {
                int y = w[j] * f[i | j | d] % mod;
                f[i | j | d] = f[i | j] + mod - y;
                f[i | j] += y; // 注意顺序
            }
        }
        if (d == (1 << 16)) for (int i = 0; i < L; i++) f[i] %= mod; // 16 次之后 y 最大是 18mod * mod, 还在 ull 范围内
    }
    int iv = qpow(L, mod - 2);
    for (int i = 0; i < L; i++) a[i] = f[i] % mod * (type ? 1 : iv) % mod;
}
void Main(int tc) {
    cin >> n >> m;
    for (int i = 0; i <= n; i++) cin >> f[i];
    for (int i = 0; i <= m; i++) cin >> g[i];
    int L = 1;
    while (L <= n + m) L <<= 1;
    NTT(L, f, 1); NTT(L, g, 1);
    for (int i = 0; i < L; i++) h[i] = (ll)f[i] * g[i] % mod;
    NTT(L, h, 0);
    for (int i = 0; i <= n + m; i++) cout << h[i] << " \n"[i == n + m];
}

}

4.6.2 任意模 NTT

这被称为 MTT。

4.6.2.1 三模 NTT

我们取 \(p_1 = 469762049, p_2 = 998244353, p_3 = 1004535809\),先分别计算出结果模 \(p_1, p_2, p_3\) 的值,再用 exCRT 合并,我们答案模 \(p_1, p_2, p_3\) 的值分别为 \(h_1, h_2, h_3\),先合并前两个得到答案模 \(p_1p_2\) 的值为 \(x\),然后根据第三个求出答案为 \(x+yp_1p_2\),注意此时我们可以边乘边取模,所以不需要 __int128,如果用普通 CRT 是需要的。注意此处模数互质,可以直接设 \(h_1 + kp_1 \equiv h_2 \pmod {p_2}\),这样就可以直接移项然后求逆,而不需要 exgcd。

最后会用到 \(9\) 次 NTT,还是挺慢的。

下面给出 P4245\(9\) 次 NTT 代码:

namespace Loop1st {
int n, m, p, r[N], f[N], g[N], h1[N], h2[N], h3[N];
int qpow(int a, int b, int mod) {
    int res = 1;
    for (; b; b >>= 1, a = (ll)a * a % mod) if (b & 1) res = (ll)res * a % mod;
    return res;
}
void NTT(int L, int *a, bool type, int mod) {
    int iv3 = qpow(3, mod - 2, mod);
    static ull w[N], f[N]; // 用 ull 减小常数
    w[0] = 1;
    for (int i = 0; i < L; i++) {
        r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
        f[i] = a[r[i]] % mod;
    }
    for (int d = 1; d < L; d <<= 1) {
        int w1 = qpow(type ? 3 : iv3, ((mod - 1) / d) >> 1, mod);
        for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1 % mod;
        for (int i = 0; i < L; i += (d << 1)) {
            for (int j = 0; j < d; j++) {
                int y = w[j] * f[i | j | d] % mod;
                f[i | j | d] = f[i | j] + mod - y;
                f[i | j] += y; // 注意顺序
            }
        }
        if (d == (1 << 16)) for (int i = 0; i < L; i++) f[i] %= mod; // 16 次之后 y 最大是 18mod * mod, 还在 ull 范围内
    }
    int iv = qpow(L, mod - 2, mod);
    for (int i = 0; i < L; i++) a[i] = f[i] % mod * (type ? 1 : iv) % mod;
}
void mul(int L, int *f, int *g, int *h, int mod) {
    static int a[N], b[N];
    memcpy(a, f, sizeof a); memcpy(b, g, sizeof b);
    NTT(L, a, 1, mod); NTT(L, b, 1, mod);
    for (int i = 0; i < L; i++) h[i] = (ll)a[i] * b[i] % mod;
    NTT(L, h, 0, mod);
}
void Main(int tc) {
    cin >> n >> m >> p;
    for (int i = 0; i <= n; i++) cin >> f[i];
    for (int i = 0; i <= m; i++) cin >> g[i];
    int L = 1;
    while (L <= n + m) L <<= 1;
    mul(L, f, g, h1, p1);
    mul(L, f, g, h2, p2);
    mul(L, f, g, h3, p3);
    int iv1 = qpow(p1, p2 - 2, p2), iv12 = qpow((ll)p1 * p2 % p3, p3 - 2, p3);
    for (int i = 0; i <= n + m; i++) {
        ll x = (ll)(h2[i] - h1[i] % p2 + p2) * iv1 % p2 * p1 + h1[i];
        ll y = (ll)(h3[i] - x % p3 + p3) * iv12 % p3;
        cout << (y * p1 % p * p2 + x) % p << " \n"[i == n + m];
    }
}

}

4.6.2.2 用单个大模数的 NTT

如果我们欣然接受 __int128,可以尝试找一个大一点的质数,这样就可以优化到 \(6\) 次甚至 \(3\) 次 NTT,比如我们用模数 \(25\times 2^{78}+1\),原根是 \(3\),不过这时取模是显然需要优化的,可以用 Montgomery 模乘。不过相信编译器的优化也是卡常的一环。代码以后补吧。

4.7 分治 NTT

不同于寻常 \(h_i = \sum_{j=0}^{i} f_jg_{i-j}\) 的卷积,现在我们需要求 \(f_i = \sum_{j=0}^{i-1}f_jg_{i-j}\),即给定 \(f_0\)\(g\),求出 \(f\) 的所有系数。这被称为半在线卷积。
直接 NTT 是不可行的,因为 \(f_i\) 的值和 \(f_0, \dots, f_{i - 1}\) 有关。我们考虑(CDQ)分治,设当前分治区间为 \([l, r]\),我们先递归计算 \(f_l, \dots, f_{mid}\) 的值,再计算 \(f_l, \dots, f_{mid}\)\(f_{mid + 1}, \dots, f_r\) 的值,由于此时 \(f_l, \dots, f_{mid}\) 已知,我们可以直接用多项式乘法算出贡献,然后再递归处理 \(f_{mid + 1}, \dots, r\) 的值即可,时间复杂度 \(\mathcal{O}(n \log^2 n)\)

下面给出 P4721 的代码。

namespace Loop1st {
int n, r[N];
int qpow(int a, int b) {
    int res = 1;
    for (; b; b >>= 1, a = (ll)a * a % mod) if (b & 1) res = (ll)res * a % mod;
    return res;
}
void NTT(int L, int *a, bool type) {
    static ull w[N], f[N]; // 用 ull 减小常数
    w[0] = 1;
    for (int i = 0; i < L; i++) {
        r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
        f[i] = a[r[i]];
    }
    for (int d = 1; d < L; d <<= 1) {
        int w1 = qpow(type ? 3 : iv3, ((mod - 1) / d) >> 1);
        for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1 % mod;
        for (int i = 0; i < L; i += (d << 1)) {
            for (int j = 0; j < d; j++) {
                int y = w[j] * f[i | j | d] % mod;
                f[i | j | d] = f[i | j] + mod - y;
                f[i | j] += y; // 注意顺序
            }
        }
        if (d == (1 << 16)) for (int i = 0; i < L; i++) f[i] %= mod; // 16 次之后 y 最大是 18mod * mod, 还在 ull 范围内
    }
    int iv = qpow(L, mod - 2);
    for (int i = 0; i < L; i++) a[i] = f[i] % mod * (type ? 1 : iv) % mod;
}
int f[N], g[N];
void solve(int l, int r) { // a 需要用到 [l, mid], b 需要用到 [0, r - l]
    if (l == r) return ;
    int mid = (l + r) >> 1;
    solve(l, mid);
    int L = 1;
    while (L <= r - l) L <<= 1;
    static int a[N], b[N], c[N];
    memset(a, 0, L << 2); memcpy(a, f + l, (mid - l + 1) << 2); memcpy(b, g, L << 2);
    NTT(L, a, 1); NTT(L, b, 1);
    for (int i = 0; i < L; i++) c[i] = (ll)a[i] * b[i] % mod;
    NTT(L, c, 0);
    for (int i = mid + 1; i <= r; i++) f[i] = (f[i] + c[i - l]) % mod;
    solve(mid + 1, r);
}
void Main(int tc) {
    cin >> n;
    for (int i = 1; i < n; i++) cin >> g[i];
    f[0] = 1;
    solve(0, n - 1);
    for (int i = 0; i < n; i++) cout << f[i] << " \n"[i == n - 1];
}

}

5. 多项式全家桶

这个部分我会将多项式板子封装成一个 namespace,默认使用 long long 类型,并放在这部分的最下方,参考了 lzyqwq 和 jiangly 的代码。

5.1 多项式加、减、乘法 P3803

不再赘述。

	const ll mod = 998244353, iv3 = (mod + 1) / 3;
    using poly = vector<ll>;
    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;
    }
    int deg(const poly &a) { return a.size() - 1; } // 如果不传 const & 就是 O(n) 的
    poly rev(poly a) { reverse(a.begin(), a.end()); return a; }
    void NTT(int L, poly &a, bool type = 0) {
        vector<ull>f(L), w(L);
        static int r[N];
        w[0] = 1;
        for (int i = 0; i < L; i++) {
            r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
            f[i] = a[r[i]];
        }
        for (int d = 1; d < L; d <<= 1) {
            ll w1 = qpow(type ? 3 : iv3, ((mod - 1) / d) >> 1);
            for (int i = 1; i < d; i++) w[i] = w[i - 1] * w1 % mod;
            for (int i = 0; i < L; i += d << 1) {
                for (int j = 0; j < d; j++) {
                    ll y = w[j] * f[i | j | d] % mod;
                    f[i | j | d] = f[i | j] + mod - y;
                    f[i | j] += y;
                }
            }
            if (d == (1 << 16)) for (int i = 0; i < L; i++) f[i] %= mod;
        }
        ll iv = qpow(L, mod - 2);
        for (int i = 0; i < L; i++) a[i] = f[i] % mod * (type ? 1 : iv) % mod;
    }
    poly modxk(poly a, int k) { a.resize(k); return a; }
    poly operator + (poly a, poly b) {
        int n = max(deg(a), deg(b)); a.resize(n + 1); b.resize(n + 1);
        poly c = poly(n + 1);
        for (int i = 0; i <= n; i++) c[i] = (a[i] + b[i]) % mod;
        return c; 
    }
    poly operator - (poly a, poly b) {
        int n = max(deg(a), deg(b)); a.resize(n + 1); b.resize(n + 1);
        poly c = poly(n + 1);
        for (int i = 0; i <= n; i++) c[i] = (a[i] + mod - b[i]) % mod;
        return c; 
    }
    poly operator * (poly a, poly b) {
        int n = deg(a), m = deg(b), L = 1;
        while (L <= n + m) L <<= 1;
        a.resize(L); b.resize(L);
        NTT(L, a, 1); NTT(L, b, 1);
        poly c = poly(L);
        for (int i = 0; i < L; i++) c[i] = a[i] * b[i] % mod;
        NTT(L, c, 0);
        c.resize(n + m + 1);
        return c;
    }

5.2 多项式求逆 P4238

给定 \(f(x)\),求 \(g(x)\) 使得 \(f(x)g(x)\equiv 1 \pmod {x^n}\)

5.2.1 递推法

朴素的 \(\mathcal{O}(n^2)\) 递推求逆:

\[[x^n](f*g)(x)=\sum_{i=0}^n f_ig_{n-i}=[n=0] \]

\[\sum_{i=0}^{n-1}f_ig_{n-i}+f_ng_0=0 \]

\[f_n = -g_0^{-1}\sum_{i=0}^{n-1}f_ig_{n-i} \]

可以用半在线卷积优化至 \(\mathcal{O}(n \log^2 n)\)

5.2.2 倍增法

我们设已经求出 \(f(x)\) 在模 \(x^{\lceil \frac n2\rceil}\) 意义下的逆元 \(f_0^{-1}(x)\),。
由于 \(f(x)f_0^{-1}(x) \equiv 1 \pmod {x^{\lceil \frac n2\rceil}}, f(x)f^{-1}(x) \equiv 1 \pmod {x^{\lceil \frac n2\rceil}}\),得到 \(f^{-1}(x)-f_0^{-1}(x) \equiv 0 \pmod{x^{\lceil \frac n2\rceil}}\)
两边平方:

\[f^{-2}(x)-2f^{-1}(x)f_0^{-1}(x)+f_0^{-2}(x) \equiv 0 \pmod{x^{\lceil \frac n2\rceil}} \]

两边同乘 \(f(x)\),移项:

\[f^{-1}(x)\equiv f_0^{-1}(2-f(x)f_0^{-1}(x)) \pmod{x^{\lceil \frac n2\rceil}} \]

递归或递推计算,时间复杂度 \(T(n) = T(\frac n2) + \mathcal{O}(n \log n) = \mathcal{O}(n \log n)\)

    poly modxk(poly a, int k) { a.resize(k); return a; }
    poly inv(poly a) {
        poly c = poly(1), d = poly(1);
        c[0] = qpow(a[0], mod - 2); d[0] = 2;
        for (int i = 1; (1 << (i - 1)) < a.size(); i++) {
            c = modxk(c * (d - modxk(a, 1 << i) * c), 1 << i);
        }
        return c;
    }

5.2.3 牛顿迭代法

以后再说

5.3 多项式除法 P4512

对于 \(f(x) = \sum_{i=0}^n f_ix^i\),我们发现 \(x^nf(x^{-1})=\sum_{i=0}^nf_ix^{n-i}=\sum_{i=0}^n f_{n-i}x^i\)。记 \(f^R(x) = x^nf(x^{-1})\)
我们有

\[F(x^{-1})=Q(x^{-1})G(x^{-1})+R(x^{-1}) \]

两边同乘 \(x^n\)

\[F^R(x)=G^R(x)Q^R(x)+x^{n-m+1}R^R(x) \]

\[F^R(x) \equiv G^R(x)Q^R(x) \pmod {x^{n-m+1}} \]

考虑两边同乘 \(G^R(x)\) 在模 \(x^{n - m + 1}\) 意义下的逆 \(H(x)\)

\[F^R(x)H(x) \equiv Q^R(x) \pmod{x^{n-m+1}} \]

\(\deg Q^R(x)=n-m<n-m+1\),所以

\[Q^R(x) = F^R(x)H(x) \]

然后根据 \(Q^R(x)\) 求出 \(Q(x)\),再用 \(F(x) - G(x)Q(x)\) 得到 \(R(x)\)

    int deg(poly a) { return a.size() - 1; }
    poly rev(poly a) { reverse(a.begin(), a.end()); return a; }
    poly inv(poly a, int n) {
        poly c = poly(1), d = poly(1);
        c[0] = qpow(a[0], mod - 2); d[0] = 2;
        for (int i = 1; (1 << (i - 1)) < n; i++) {
            c = modxk(c * (d - modxk(a, 1 << i) * c), 1 << i);
        }
        return modxk(c, n);
    }
    poly operator / (poly a, poly b) {
        int n = deg(a), m = deg(b);
        return rev(modxk(rev(a) * inv(rev(b), n - m + 1), n - m + 1));
    }
    poly operator % (poly a, poly b) {
        return modxk(a - a / b * b, deg(b));
    }

5.4 多项式求导、积分

很显然。

    poly direv(poly a) {
        poly b = poly(deg(a));
        for (int i = 0; i < deg(a); i++) b[i] = a[i + 1] * (i + 1) % mod;
        return b;
    }
    poly integr(poly a) {
        poly b = poly(deg(a) + 2);
        for (int i = 1; i <= deg(a) + 1; i++) b[i] = a[i - 1] * qpow(i, mod - 2) % mod;
        return b;
    }

5.5 多项式 \(\ln\) P4725

\[B(x) = \ln A(x) \]

\[B'(x) = \frac{A'(x)}{A(x)} \]

只需要知道 \(B'(x) \bmod x^{n-1}\) 就可以积分得到 \(B(x) \bmod x^n\),那么只需要知道 \(\dfrac{A'(x)}{A(x)} \bmod x^{n-1}\),多项式求逆即可。

这里保证了 \(a_0 = 1\) 的深层原因,是因为如果 \(a_0 \ne 1\) 的话 \(\ln\) 没有意义。

    poly ln(poly a, int n) {
        a = modxk(a, n);
        assert(a[0] == 1);
        return integr(modxk(direv(a) * inv(a, n - 1), n - 1));
    }

5.x 封装好的板子

持续更新中……

参考资料

快速傅里叶变换
[多项式 I:拉格朗日插值与快速傅里叶变换 Alex_Wei](多项式 I:拉格朗日插值与快速傅里叶变换 - qAlex_Weiq - 博客园)
【学习笔记】多项式 1:基础操作
NTT与多项式全家桶

posted @ 2026-05-15 10:36  循环一号  阅读(8)  评论(0)    收藏  举报