(v4 更新)OI+ACM 笔记:0x30 数学知识

0x31 数学知识 多项式

拉格朗日插值

多项式的插值:给出 \(n + 1\) 个点 \((x_1, y_1), (x_2, y_2), \cdots, (x_{n + 1}, y_{n + 1})\)其中横坐标两两不同。求一个至多 \(n\)的多项式,使得这 \(n + 1\) 个点都在 \(f(x)\) 上。

拉格朗日插值

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

构造方法:考虑构造 \(n + 1\) 个函数 \(f_1(x), \cdots, f_n(x), f_{n + 1}(x)\)其中第 \(i\) 个函数 \(f_i(x)\) 的图像经过 \((x_i, y_i)\),对于其他的 \(j \neq i\) 经过 \((x_j, 0)\)。则我们要求的 \(f(x)\) 即为这 \(n + 1\) 个函数的和 \(\sum_{i = 1}^{n + 1} f_i(x)\)

尝试构造 \(f_i(x) = a_i \prod\limits_{j \neq i}(x - x_j)\),其中 \(a_i\) 是常数。

这样构造可以满足,对于其他的 \(j \neq i\) 均有 \(f_i(x_j) = 0\)。代入 \(f_i(x_i) = y_i\),解得 \(a_i = \frac{y_i}{\prod_{j \neq i}(x_i - x_j)}\)

回代即可完成构造,不难发现 \(f(x)\) 是一个至多 \(n\)的多项式。并且可以证明拉格朗日插值是唯一的插值多项式

朴素实现的时间复杂度是 \(\mathcal{O}(n^2)\),可以使用多项式优化到 \(\mathcal{O}(n \log^2 n)\)(详见多项式快速插值)。

0x31 拉格朗日插值.cpp

/* 拉格朗日插值 */
int lagrange(const std::vector< std::pair<int, int> > &seq, int k) {
    int n = seq.size();
    int ans = 0;
    for (int i = 0; i < n; i ++) {
        int p = 1, q = 1; // 先求出分子与分母
        for (int j = 0; j < n; j ++) {
            if (i == j) continue;
            mul(p, k - seq[j].first);
            mul(q, seq[i].first - seq[j].first);
        }
        ans = (ans + 1ll * p * qpow(q, mod - 2, mod) % mod * seq[i].second) % mod;
    }
    return (ans + mod) % mod;
}

拉格朗日插值求系数:相当于是直接将整个多项式求出来。运用到了暴力除以单项式的技巧。

  • 记系数部分 \(a_i = \frac{y_i}{\prod_{j \neq i}(x_i - x_j)}\)。记多项式 \(g(x) = \prod_j(x - x_j)\),记多项式 \(h_i(x) = \prod_{j \neq i} (x - x_j)\)
  • 先暴力多项式乘法 \(\mathcal{O}(n^2)\) 处理出 \(g(x)\)考虑由 \(g(x)\) 得到 \(h_i(x)\),相当于是多项式 \(g(x)\) 除以一个单项式 \((x - x_i)\),仍然可以暴力 \(\mathcal{O}(n)\) 除以单项式。具体地,由于 \((x - x_i)h_i(x) = g(x)\),则对于 \(g(x)\)\(j(j \geq 1)\) 次项系数,有 \([x^{j - 1}]h_i - x_i[x^j]h_i = [x^j]g\),即

\[[x^0]h_i = \frac{[x^0]g}{-x_i} \\ [x^j]h_i = \frac{[x^j]g - [x^{j - 1}]h_i}{-x_i} \]

  • 求出了 \(a_i\)\(h_i(x)\) 之后,\(f(x) = \sum_{i = 1}^{n + 1} a_ih_i(x)\)。时间复杂度 \(\mathcal{O}(n^2)\)

0x31 拉格朗日插值求系数.cpp

/* 拉格朗日插值求系数 */
std::vector<int> lagrange(const std::vector< std::pair<int, int> > &seq) {
    int n = seq.size();

    /* 求系数 a[] */
    std::vector<int> a(n);
    for (int i = 0; i < n; i ++) {
        int q = 1;
        for (int j = 0; j < n; j ++) {
            if (i == j) continue;
            mul(q, seq[i].first - seq[j].first);
        }
        if (q < 0) q += mod;
        a[i] = 1ll * seq[i].second * qpow(q, mod - 2, mod) % mod;
    }

    /* 求总的多项式 g */
    std::vector<int> g(n + 1);
    g[0] = 1;
    for (int i = 0; i < n; i ++) {
        for (int j = i + 1; j >= 1; j --) {
            g[j] = (1ll * g[j] * (mod - seq[i].first) + g[j - 1]) % mod;
        }
        g[0] = 1ll * g[0] * (mod - seq[i].first) % mod;
    }

    std::vector<int> f(n); // 答案多项式 f
    for (int i = 0; i < n; i ++) {
        std::vector<int> h(n); // 求分的多项式 h_i
        int inv = qpow(mod - seq[i].first, mod - 2, mod);

        h[0] = 1ll * g[0] * inv % mod;
        for (int j = 1; j < n; j ++) {
            h[j] = 1ll * (g[j] - h[j - 1] + mod) * inv % mod;
        }

        for (int j = 0; j < n; j ++) {
            f[j] = (f[j] + 1ll * a[i] * h[j]) % mod;
        }
    }

    return f;
}

int func(const std::vector<int> &f, int k) {
    int n = f.size();
    int y = 0;
    for (int i = n - 1; i >= 0; i --) {
        y = (1ll * y * k + f[i]) % mod;
    }
    return y;
}

连续点值的拉格朗日插值:给出 \(n + 1\) 个连续点值 \(f(1), f(2), \cdots, f(n + 1)\),代入拉格朗日插值公式,得

\[f(x) = \sum\limits_{i = 1}^{n + 1}y_i \prod\limits_{j \neq i}\frac{x - j}{i - j} \]

现在考虑给定一个自变量 \(x\),求出因变量 \(f(x)\)。考虑右边的乘积项怎么求。

  • 分子:记 \(\mathrm{pre}_i = \prod_{1 \leq j \leq i}(x - j), \mathrm{suf}_i = \prod_{i\leq j \leq n + 1}(x - j)\),则分子显然为 \(\mathrm{pre}_{i - 1} \times \mathrm{suf}_{i + 1}\)
  • 分母:对于 \(j < i\) 的部分,\(i - 1\)\(1\) 乘起来;对于 \(j > i\) 的部分,\(-1\)\(-(n + 1 - i)\) 乘起来。故分母为 \((i - 1)! (n + 1 - i)!(-1)^{n + 1 - i}\)

故最后的式子为

\[f(x) = \sum\limits_{i = 1}^{n + 1}y_i \frac{\mathrm{pre}_{i - 1} \times \mathrm{suf}_{i + 1}}{(i - 1)!(n + 1 - i)!(-1)^{n + 1 - i}} \]

求出单个点值的时间为 \(\mathcal{O}(n)\)

若给出 \(n + 1\) 个连续点值 \(f(h), f(h + 1), \cdots, f(h + n)\),分母的求法是完全不变的,只需将分子改成 \(x - x_j\) 相乘即可。

或者可以考虑将坐标系向左平移 \(h - 1\)

0x31 连续点值的拉格朗日插值.cpp

/* 连续点值的拉格朗日插值 */
int lagrange_conti(const std::vector<int> &y, int k) {
    int n = y.size() - 1; // y 的下标从 1 开始
    if (k <= n) {
        return y[k];
    }

    std::vector<int> pre(n + 2), suf(n + 2);
    pre[0] = 1;
    for (int i = 1; i <= n; i ++) {
        pre[i] = 1ll * pre[i - 1] * (k - i) % mod;
    }
    suf[n + 1] = 1;
    for (int i = n; i >= 1; i --) {
        suf[i] = 1ll * suf[i + 1] * (k - i) % mod;
    }

    std::vector<int> fact(n + 1), facv(n + 1);
    fact[0] = 1;
    for (int i = 1; i <= n; i ++) {
        fact[i] = 1ll * fact[i - 1] * i % mod;
    }
    facv[n] = qpow(fact[n], mod - 2, mod);
    for (int i = n - 1; i >= 0; i --) {
        facv[i] = 1ll * facv[i + 1] * (i + 1) % mod;
    }

    int ans = 0;
    for (int i = 1; i <= n; i ++) {
        int cur = y[i];
        cur = 1ll * cur * pre[i - 1] % mod * suf[i + 1] % mod;
        cur = 1ll * cur * facv[i - 1] % mod * facv[n - i] % mod;
        if ((n - i) & 1) {
            dec(ans, cur);
        } else {
            add(ans, cur);
        }
    }

    return ans;
}

FFT(快速傅里叶变换)

单位根\(n\) 次单位根,即为 \(x^n = 1\) 的所有复数解。单位圆上辐角为 \(0, \frac{2\pi}{n}, \frac{4\pi}{n}, \cdots, \frac{2(n - 1)\pi}{n}\) 的复数都是单位根,共 \(n\) 个。这些单位根依次表示为

\[\omega_n^0, \omega_n^1, \omega_n^2, \cdots, \omega_n^{n - 1} \]

单位根的简单性质:欧拉公式:\(e^{ix} = \cos(x) + i \sin(x)\)。这说明 \(x \in \R\) 时,\(e^{ix}\) 可以描述一个单位圆上辐角为 \(x\) 的复数。

  • \(\omega_n^1 = e^{\frac{2\pi i}{n}} = \cos(\frac{2\pi}{n}) + i \sin(\frac{2\pi}{n})\)
  • \(\omega_n^i \times \omega_n^j = \omega_n^{i + j}\)
  • \(\left(\omega_n^i\right)^j = \omega_n^{ij}\)
  • \(\omega_{2n}^{2k} = \omega_n^k\)
  • \(\omega_{2n}^k = -\omega_{2n}^{k + n}\)

补充界:在进行 FFT 前,需要对其最高位补 \(0\),将其补成一个最高次数为 \(2^P - 1\) 的多项式。方便后续的奇偶性分组。

DFT

DFT 的思想:"系数表示法" 转 "点值表示法"。取自变量 \(\omega_n^0, \omega_n^1, \cdots, \omega_n^{n - 1}\),计算因变量 \(F(\omega_n^0), F(\omega_n^1), \cdots, F(\omega_n^{n - 1})\)

\(n = 2^P\),考虑一个 \(2^P - 1\) 次的多项式 \(F(x)\)

\[F(x) = a_0 + a_1x + \cdots + a_{n - 1}x^{n - 1} \]

将每一项按照次数的奇偶性,划分成两部分(由于 \(n\)\(2\) 的若干次幂,每次分出来的奇偶部分的次数一定相等)。

\[F(x) = \left( a_0 + a_2x^2 + \cdots + a_{n - 2}x^{n - 2} \right) + \left( a_1x + a_3x^3 + \cdots + a_{n - 1}x^{n - 1} \right) \]

\[F_L(x) = a_0 + a_2x + \cdots + a_{n - 2}x^{n / 2 - 1} \\ F_R(x) = a_1 + a_3x + \cdots + a_{n - 1}x^{n / 2 - 1} \]

\[F(x) = F_L(x^2)+ xF_R(x^2) \]

考虑计算因变量 \(F(\omega_n^0), F(\omega_n^1), \cdots, F(\omega_n^{n - 1})\)

  • 对于 \(k < n / 2\),代入 \(\omega_n^k\)

\[\begin{aligned} F(\omega_n^k) & = F_L\left((\omega_n^k)^2\right) + \omega_n^k F_R\left((\omega_n^k)^2\right) \\ & = F_L(\omega_{n / 2}^k) + \omega_n^k F_R(\omega_{n / 2}^k) \end{aligned} \]

  • 对于 \(k < n / 2\),代入 \(\omega_n^{k + n/2}\)

\[\begin{aligned} F(\omega_n^{k + n / 2}) & = F_L\left((\omega_n^{k + n / 2})^2\right) + \omega_n^{k + n / 2}F_R\left((\omega_n^{k + n / 2})^2\right) \\ & = F_L(\omega_n^{2k + n}) + \omega_n^{k + n / 2} F_R(\omega_n^{2k + n}) \\ & = F_L(\omega_{n / 2}^k) - \omega_{n}^{k} F_R(\omega_{n / 2}^k) \end{aligned} \]

注意到 \(F(\omega_n^k), F(\omega_n^{k + n / 2})\) 等于 \(F_L(\omega_{n / 2}^k) \pm \omega_n^kF_R(\omega_{n / 2}^k)\)

如果我们知道 \(F_L(x), F_R(x)\) 分别在 \(\omega_{n / 2}^0, \omega_{n / 2}^1, \cdots, \omega_{n / 2}^{n / 2 - 1}\) 处的点值表示,就可以 \(\mathcal{O}(n)\) 求出 \(F(x)\)\(\omega_n^0, \omega_n^1, \cdots, \omega_n^{n - 1}\) 处的点值表示。

这是一个分治的过程,不断分治直到仅剩一个项即可。

时间复杂度 \(\mathcal{O}(n \log n)\)

IDFT

结论:将 DFT 中的 \(\omega_n^1\) 换成 \(\omega_n^{-1}\),做完以后除以 \(n\) 即可

IDFT 的思想:"点值表示法" 转 "系数表示法"。已知因变量 \(F(\omega_n^0), F(\omega_n^1), \cdots, F(\omega_n^{n - 1})\),求出自变量 \(\omega_n^0, \omega_n^1, \cdots, \omega_n^{n - 1}\)

\(n = 2^P\),对于一个 \(2^{P} - 1\) 的多项式 \(F(x)\),设其经过 DFT 之后,得到的点值序列 \(g_0, g_1, \cdots, g_{n - 1}\),可以证明:

\[g_k = \sum\limits_{i = 0}^{n - 1}a_i \left( \omega_n^k \right)^i \iff a_k = \frac{1}{n} \sum\limits_{i = 0}^{n - 1}g_i \left( \omega_n^{-k} \right)^i \]

证明:将 \(g_i\) 代入该式子,得

\[\begin{aligned} & \frac{1}{n} \sum\limits_{i = 0}^{n - 1}\sum\limits_{j = 0}^{n - 1}(\omega_n^{-k})^i(\omega_n^i)^j a_j \\ = & \sum\limits_{j = 0}^{n - 1} a_j \left(\frac{1}{n}\sum\limits_{i = 0}^{n - 1}\omega_{n}^{i(j - k)}\right) \end{aligned} \]

后面括号里的和式,即为单位根反演的经典结论 \([n \mid k] = \frac{1}{n}\sum_{i = 0}^{n - 1}\omega_{n}^{ik}\)。具体地,分类讨论:

  • \(j = k\) 时,和式为 \(\frac{1}{n}\sum_{i = 0}^{n - 1}\omega_n^0 = 1\)
  • \(j \neq k\) 时,和式为

\[\frac{1}{n} \sum_{i = 0}^{n - 1} \omega_{n}^{i(j - k)} = \frac{1}{n}\sum_{i = 0}^{n - 1} \left(\omega_n^{j - k} \right)^i = \frac{1}{n}\sum_{i = 0}^{n - 1} \frac{1 - (\omega_n^{j - k})^n}{1 - \omega_n^{j - k}} = 0 \]

\(a_k = \frac{1}{n} \sum_{i = 0}^{n - 1}g_i \left( \omega_n^{-k} \right)^i\) 成立。


进一步,观察 IDFT 的式子,相当于将 DFT 中的 \(\omega_n^1\) 换成 \(\omega_n^{-1}\),做完以后再除以 \(n\)

再进一步,发现 \(F(\omega_n^{-i}) = F(\omega_n^{n - i})\),于是 IDFT 可以看作将 DFT 后得到的点值进行翻转(注意 \(F(\omega_n^0)\) 这一项不参与翻转),然后再除以 \(n\)

套用 DFT 的板子即可。

*DFT 与 IDFT 的矩阵角度

对于 DFT 前的系数表示法 \(a_0, a_1, \cdots, a_{n - 1}\) 以及 DFT 后的点值表示法 \(g_0, g_1, \cdots, g_{n - 1}\)。可以写成一个系数向量左乘一个转移矩阵得到点值向量的式子:

\[\begin{bmatrix} g_0 \\ g_1 \\ \vdots \\ g_{n - 1} \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n - 1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n - 1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n - 1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n - 1} & \omega_n^{2(n - 1)} & \omega_n^{3(n - 1)} & \cdots & \omega_n^{(n - 1)(n - 1)} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n - 1} \end{bmatrix} \]

其中转移矩阵(从 \(0\) 开始的)第 \(i\) 行第 \(j\) 列的系数即为因变量 \(\omega_n^i\)\(j\) 次方 \(\left(\omega_{n}^i\right)^j\)。这恰好是范德蒙德矩阵

如何从点值向量得到系数向量?只需让上式等号两边都左乘一个转移矩阵的逆即可。结论:范德蒙德矩阵的逆,为各个系数取倒数,再除以矩阵大小 \(n\)。故有

\[\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n - 1} \end{bmatrix} = \frac{1}{n} \begin{bmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^{-1} & \omega_n^{-2} & \omega_n^{-3} & \cdots & \omega_n^{-(n - 1)} \\ 1 & \omega_n^{-2} & \omega_n^{-4} & \omega_n^{-6} & \cdots & \omega_n^{-2(n - 1)} \\ 1 & \omega_n^{-3} & \omega_n^{-6} & \omega_n^{-9} & \cdots & \omega_n^{-3(n - 1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{-(n - 1)} & \omega_n^{-2(n - 1)} & \omega_n^{-3(n - 1)} & \cdots & \omega_n^{-(n - 1)(n - 1)} \end{bmatrix} \begin{bmatrix} g_0 \\ g_1 \\ \vdots \\ g_{n - 1} \end{bmatrix} \]

这也解释了 IDFT 的结论:将 DFT 中的 \(\omega_n^1\) 换成 \(\omega_n^{-1}\),做完以后除以 \(n\)

FFT 倍增实现

位逆序置换

\(n = 8\) 为例,模拟分治的过程:

\[\{ x_0, x_1, x_2, x_3, x_4, x_5, x_6, x_7 \} \\ \{ x_0, x_2, x_4, x_6 \}\{ x_1, x_3, x_5, x_7 \} \\ \{ x_0, x_4 \} \{ x_2, x_6 \} \{ x_1, x_5 \} \{ x_3, x_7 \} \\ \{ x_0 \} \{ x_4 \} \{ x_2 \} \{ x_6 \} \{ x_1 \} \{ x_5 \} \{ x_3 \} \{ x_7 \} \]

从分治的角度,每次将 \(x_i\) 按照奇偶性分类,偶则分到左边,奇则分到右边。直到只剩下一个项为止。

从倍增的角度,我们希望对于每个项 \(x_i\),求出其经过不断分治以后,只剩下一个项时,在哪一个位置。然后根据变换后的位置,不断地倍增合并点值。

\(x\) 用二进制数表示,变换后的位置即为该二进制数的翻转(例如 011000 变成 000110)。证明:每次根据奇偶性将 \(x_i\) 进行分类,相当于是由最低位决定最高位。以此类推,次低位决定次高位,第三低位决定第三高位 ...

\(\mathrm{rev}(x)\) 表示二进制数 \(x\) 的翻转。首先有 \(\mathrm{rev}(0) = 0\),考虑从小到大递推,有递推式

\[\mathrm{rev}(x) = \left\lfloor \frac{\mathrm{rev}\left( \left\lfloor \frac{x}{2} \right\rfloor \right)}{2} \right\rfloor + (x \bmod 2) \times 2^{P - 1} \]

蝶形运算:已知 \(F_L(\omega_{n / 2}^k), F_R(\omega_{n / 2}^k)\)。每次更新,需要用 \(F_L(\omega_{n / 2}^k) \pm \omega_n^kF_R(\omega_{n / 2}^k)\) 求出 \(F(\omega_n^k), F(\omega_n^{k + n / 2})\)。使用位逆序置换后

  • 更新前:\(F_L(\omega_{n / 2}^k)\) 储存在数组下标为 \(k\) 的位置,\(F_R(\omega_{n / 2}^k)\) 储存在数组下标为 \(k + n / 2\) 的位置。
  • 更新后:\(F(\omega_n^k)\) 储存在数组下标为 \(k\) 的位置,\(F(\omega_n^{k + n / 2})\) 储存在数组下标为 \(k + n / 2\) 的位置。

因此可以直接在数组下标为 \(k\)\(k + n / 2\) 的位置进行覆写,而不用开额外的数组保存值。

FFT 多项式乘法

对于两个多项式 \(A, B\),现在要求出多项式 \(C = A \times B\)

考虑对 \(A, B\) 进行 DFT,经过 DFT 之后得到一系列关于 \(A, B\) 的点值,将关于 \(A, B\) 的点值对应位置相乘即可得到 \(C\) 的点值,再对 \(C\) 的点值进行 IDFT 即可得到 \(C\) 的系数。

核心思想:\(A, B\) 的系数转 \(A, B\) 的点值,\(A, B\) 的点值点积转 \(C\) 的点值,\(C\) 的点值转 \(C\) 的系数。

时间复杂度 \(\mathcal{O}((n + m) \log (n + m))\)

0x31 FFT.cpp

const double pi = acos(-1);
using comp = std::complex<double>;

std::vector<int> rev;
void dft(std::vector<comp> &a) {
    int n = a.size();
    for (int i = 0; i < n; i ++) {
        if (i < rev[i]) {
            std::swap(a[i], a[rev[i]]);
        }
    }
    for (int k = 1; k < n; k <<= 1) {
        comp omega(cos(pi / k), sin(pi / k)); // 2k 阶单位根
        for (int i = 0; i < n; i += (k << 1)) {
            comp x(1, 0);
            for (int j = 0; j < k; j ++, x *= omega) {
                comp u = a[i + j], v = x * a[i + j + k];
                a[i + j] = u + v, a[i + j + k] = u - v;
            }
        }
    }
}

void idft(std::vector<comp> &a) {
    int n = a.size();
    std::reverse(a.begin() + 1, a.end());
    dft(a);
    for (int i = 0; i < n; i ++) {
        a[i] /= n;
    }
}

struct poly : public std::vector<comp> {
    poly() : std::vector<comp>() {}
    explicit constexpr poly(int n) : std::vector<comp>(n) {}
    explicit constexpr poly(const std::vector<comp> &a) : std::vector<comp>(a) {}
    constexpr poly(const std::initializer_list<comp> &a) : std::vector<comp>(a) {}

    template <class InputIt, class = std::_RequireInputIter<InputIt>>
    explicit constexpr poly(InputIt st, InputIt ed) : std::vector<comp>(st, ed) {}

    constexpr friend poly operator * (poly a, poly b) {
        int tot = a.size() + b.size() - 1;
        if (tot < 128) {
            poly c(tot);
            for (int i = 0; i < a.size(); i ++) {
                for (int j = 0; j < b.size(); j ++) {
                    c[i + j] += a[i] * b[j];
                }
            }
            return c;
        }

        int L = 1, P = 0;
        while (L < tot) L <<= 1, P ++;
        rev.resize(L);
        for (int i = 0; i < L; i ++) {
            rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (P - 1));
        }

        a.resize(L), b.resize(L);
        dft(a), dft(b);
        for (int i = 0; i < L; i ++) {
            a[i] *= b[i];
        }
        idft(a);
        a.resize(tot);
        return a;
    }
};

NTT(快速数论变换)

回顾一些内容:

:设整数 \(m > 0\),满足 \(a^x \equiv 1 \pmod m\) 的最小正整数 \(x\),称作 \(a\)\(m\) 的阶。记作 \(\mathrm{ord}_m(a)\)

阶的存在条件当且仅当 \(\gcd(a, m) = 1\)\(a\)\(m\) 的阶存在。

阶的简单性质

  • \(a\perp m\),则 \(a, a^2, \cdots, a^{\mathrm{ord}_m(a)}\)\(m\) 两两不同。
  • \(a\perp m\),则 \(\mathrm{ord}_m(a) \mid \varphi(m)\)
  • \(a \perp m\),则

\[\mathrm{ord}_m(a^k) = \frac{\mathrm{ord}_m(a)}{\gcd(\mathrm{ord}_m(a), k)} \]

原根:设整数 \(m > 0\),若 \(\mathrm{ord}_m(g) = \varphi(m)\),则称 \(g\) 为模 \(m\) 的原根。


在模意义下, 我们希望找到一个东西来代替单位根。主要是要满足 FFT 过程中,用到的单位根满足的三个性质:

  • \(\omega_n\) 的阶为 \(n\)
  • \(\omega_{2n}^{2k} = \omega_n^k\)
  • \(\omega_{2n}^k = -\omega_{2n}^{n + k}\)

对于一个质数 \(m\),考虑 \(m\) 的一个原根 \(g\)。此时 \(g\) 的阶为 \(m - 1\),显然原根 \(g\) 不能直接代替 \(n\) 阶单位根。

考虑 \(g\) 的若干次幂。\(g^k\) 的阶为 \(\frac{m - 1}{\gcd(m - 1, k)}\),必为 \(m - 1\) 的约数。且当 \(n \mid (m - 1)\) 时,可以构造 \(g^{\frac{m - 1}{n}}\) 的阶恰好等于 \(n\)

尝试使用 \(g^{\frac{m - 1}{n}}\) 代替 \(n\) 阶单位根,性质 1, 2 的证明显然,性质 3 相当于是要证明 \(g^{\frac{m - 1}{2}} \equiv -1 \pmod m\),可以使用二次剩余中的欧拉判别法来证明(原根必定不是二次剩余)。

于是当 \(n \mid (m - 1)\) 时,我们完全可以使用 \(g^{\frac{m - 1}{n}}\) 来代替 \(n\) 阶单位根 \(\omega_n\)

现在还要满足 \(n \mid (m - 1)\) 才可以代替,对模数 \(m\) 的取值有特殊的要求。注意到 FFT 过程中的 \(n\) 都是 \(2\) 的若干次幂,所以 \(m - 1\) 只需要包含大量的因子 \(2\) 即可。例如 \(998244353 = 2^{23} \times 7 \times 17 + 1\),就是一个经典的 NTT 模数。

常用的 NTT 模数

  • 998244353:原根为 \(3\)
  • 1004535809:原根为 \(3\)
  • 469762049:原根为 \(3\)
  • 167772161:原根为 \(3\)
  • 924844033:原根为 \(5\)
  • 4179340454199820289\(4 \times 10^{18}\) 级别,以时间换精度):原根为 \(3\)

0x31 NTT.cpp

// 预处理单位根
constexpr auto findRootPower() {
    int g = 3; // 模数对应的原根,需根据实际问题调整
    std::array<int, 32> w{};
    for (int k = 1, idx = 0; (mod - 1) % (k << 1) == 0; k <<= 1) {
        w[idx ++] = qpow(g, (mod - 1) / (k << 1), mod); // 2k 阶单位根
    }
    return w;
}

constexpr auto RootPower = findRootPower();

std::vector<int> rev;
void dft(std::vector<int> &a) {
    int n = a.size();
    for (int i = 0; i < n; i ++) {
        if (i < rev[i]) {
            std::swap(a[i], a[rev[i]]);
        }
    }
    for (int k = 1, idx = 0; k < n; k <<= 1) {
        int omega = RootPower[idx ++];
        for (int i = 0; i < n; i += (k << 1)) {
            int x = 1;
            for (int j = 0; j < k; j ++, mul(x, omega)) {
                int u = a[i + j], v = 1ll * x * a[i + j + k] % mod;
                add(a[i + j] = u, v), dec(a[i + j + k] = u, v);
            }
        }
    }
}

void idft(std::vector<int> &a) {
    int n = a.size(), inv = qpow(n, mod - 2, mod);
    std::reverse(a.begin() + 1, a.end());
    dft(a);
    for (int i = 0; i < n; i ++) {
        mul(a[i], inv);
    }
}

struct poly : public std::vector<int> {
    poly() : std::vector<int>() {}
    explicit constexpr poly(int n) : std::vector<int>(n) {}
    explicit constexpr poly(const std::vector<int> &a) : std::vector<int>(a) {}
    constexpr poly(const std::initializer_list<int> &a) : std::vector<int>(a) {}

    template <class InputIt, class = std::_RequireInputIter<InputIt>>
    explicit constexpr poly(InputIt st, InputIt ed) : std::vector<int>(st, ed) {}

    constexpr friend poly operator * (poly a, poly b) {
        int tot = a.size() + b.size() - 1;
        if (tot < 128) {
            poly c(tot);
            for (int i = 0; i < a.size(); i ++) {
                for (int j = 0; j < b.size(); j ++) {
                    c[i + j] = (c[i + j] + 1ll * a[i] * b[j]) % mod;
                }
            }
            return c;
        }

        int L = 1, P = 0;
        while (L < tot) L <<= 1, P ++;
        rev.resize(L);
        for (int i = 0; i < L; i ++) {
            rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (P - 1));
        }

        a.resize(L), b.resize(L);
        dft(a), dft(b);
        for (int i = 0; i < L; i ++) {
            mul(a[i], b[i]);
        }
        idft(a);
        a.resize(tot);
        return a;
    }
};

MTT(任意模数 NTT)

当模数不为常用的 NTT 模数时,我们就需要使用一些技巧使得多项式乘法可以继续进行。

首先不可以简单地使用 FFT,因为 double 的有效精度只有 15~16 位,而对于常见的数据范围 \(n, m \leq 10^5\) 以及 \(a_i, b_i \leq 10^9\),卷积结果的最大值接近 \(10^5 \times 10^9 \times 10^9 = 10^{23}\),一定会掉精度。

三模 NTT

由于常见的数据范围下,卷积结果的最大值接近 \(10^{23}\)。于是我们考虑选择三个 \(10^9\) 级别的常用 NTT 模数,用三个模数分别做一次多项式乘法,将得到的三个结果使用 CRT 合并

常用的 NTT 模数选法如下,因为这三个 NTT 模数均有一个原根为 \(3\)

\[\begin{aligned} & \mathrm{mod}1 = 998244353 \\ & \mathrm{mod}2 = 1004535809 \\ & \mathrm{mod}3 = 469762049 \end{aligned} \]

CRT 过程中需要使用 __int128

0x31 MTT(三模 NTT).cpp

// 预处理单位根
constexpr auto findRootPower(int mod) {
    int g = 3; // 模数对应的原根
    std::array<int, 32> w{};
    for (int k = 1, idx = 0; (mod - 1) % (k << 1) == 0; k <<= 1) {
        w[idx ++] = qpow(g, (mod - 1) / (k << 1), mod); // 2k 阶单位根
    }
    return w;
}

std::vector<int> rev;
template <const int mod>
struct mulSpace {
    std::array<int, 32> RootPower;
    mulSpace() { RootPower = findRootPower(mod); }

    inline void add(int &x, const int &y) { x += y; if (x >= mod) x -= mod; }
    inline void dec(int &x, const int &y) { x -= y; if (x < 0) x += mod; }
    inline void mul(int &x, const int &y) { x = 1ll * x * y % mod; }

    void dft(std::vector<int> &a) {
        int n = a.size();
        for (int i = 0; i < n; i ++) {
            if (i < rev[i]) {
                std::swap(a[i], a[rev[i]]);
            }
        }
        for (int k = 1, idx = 0; k < n; k <<= 1) {
            int omega = RootPower[idx ++];
            for (int i = 0; i < n; i += (k << 1)) {
                int x = 1;
                for (int j = 0; j < k; j ++, mul(x, omega)) {
                    int u = a[i + j], v = 1ll * x * a[i + j + k] % mod;
                    add(a[i + j] = u, v), dec(a[i + j + k] = u, v);
                }
            }
        }
    }

    void idft(std::vector<int> &a) {
        int n = a.size(), inv = qpow(n, mod - 2, mod);
        std::reverse(a.begin() + 1, a.end());
        dft(a);
        for (int i = 0; i < n; i ++) {
            mul(a[i], inv);
        }
    }

    std::vector<int> mul(std::vector<int> a, std::vector<int> b, int L) {
        for (int &x : a) x %= mod;
        for (int &x : b) x %= mod;
        a.resize(L), b.resize(L);
        dft(a), dft(b);
        for (int i = 0; i < L; i ++) {
            mul(a[i], b[i]);
        }
        idft(a);
        return a;
    }
};

const int mod1 = 998244353, mod2 = 1004535809, mod3 = 469762049;
mulSpace<mod1> m1;
mulSpace<mod2> m2;
mulSpace<mod3> m3;

/* (完整地)得出 CRT 的解 */
int CRT(int x1, int x2, int x3) {
    static const s128 M = (s128)mod1 * mod2 * mod3;
    static const s64 M1 = M / mod1, M2 = M / mod2, M3 = M / mod3;
    static const int
        V1 = qpow(M1 % mod1, mod1 - 2, mod1),
        V2 = qpow(M2 % mod2, mod2 - 2, mod2),
        V3 = qpow(M3 % mod3, mod3 - 2, mod3);
    s128 x = 0;
    x += (s128)x1 * M1 * V1;
    x += (s128)x2 * M2 * V2;
    x += (s128)x3 * M3 * V3;
    x %= M; // 此时的 x 即为 CRT 的解
    return x % mod;
}

struct poly : public std::vector<int> {
    poly() : std::vector<int>() {}
    explicit constexpr poly(int n) : std::vector<int>(n) {}
    explicit constexpr poly(const std::vector<int> &a) : std::vector<int>(a) {}
    constexpr poly(const std::initializer_list<int> &a) : std::vector<int>(a) {}

    template <class InputIt, class = std::_RequireInputIter<InputIt>>
    explicit constexpr poly(InputIt st, InputIt ed) : std::vector<int>(st, ed) {}

    constexpr friend poly operator * (poly a, poly b) {
        int tot = a.size() + b.size() - 1;
        if (tot < 128) {
            poly c(tot);
            for (int i = 0; i < a.size(); i ++) {
                for (int j = 0; j < b.size(); j ++) {
                    c[i + j] = (c[i + j] + 1ll * a[i] * b[j]) % mod;
                }
            }
            return c;
        }

        int L = 1, P = 0;
        while (L < tot) L <<= 1, P ++;
        rev.resize(L);
        for (int i = 0; i < L; i ++) {
            rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (P - 1));
        }

        std::vector<int> c1 = m1.mul(a, b, L), c2 = m2.mul(a, b, L), c3 = m3.mul(a, b, L);
        
        poly c(tot);
        for (int i = 0; i < tot; i ++) {
            c[i] = CRT(c1[i], c2[i], c3[i]);
        }
        return c;
    }
};

Trick:翻转卷积

给出序列 \(a_0, \cdots, a_n\) 与序列 \(b_0, \cdots, b_m\),对于每一个 \(k\) 需要求出 \(c_k = \sum_{i} a_ib_{i - k}\),即下标差恒为 \(k\) 的卷积式(相当于 \(a_ib_j\) 贡献给了 \(c_{i - j}\))。

将序列 \(b\) 翻转得到 \(b'\),其中 \(b'_{m - j} = b_j\)对序列 \(a, b'\) 进行普通卷积得到序列 \(c'\),此时 \(a_ib_j\) 贡献给了 \(c'_{m + i - j}\),于是有 \(c_k = c'_{k + m}\)

Trick:积卷积

给出序列 \(a_1, \cdots, a_{p - 1}\) 与序列 \(b_1, \cdots, b_{p - 1}\),对于每一个 \(k \in [1, p)\) 需要求出 \(c_k = \sum_{i\cdot j \bmod p = k} a_ib_j\)

当模数为较小的质数 \(p\) 时,可以使用 "离散对数",将模 \(p\) 意义下的乘法转换为模 \(p - 1\) 意义下的加法。

Trick:多项式点乘向量

给出多项式 \(A(x) = \sum_{i = 0}^n a_ix^i\) 与向量 \(\mathbf{B} = (b_0, \cdots, b_n)\),现在我们要求多项式 \(A\) 点乘向量 \(\mathbf{B}\) 的值

\[A \cdot \mathbf{B} = \sum_{i = 0}^n a_ib_i \]

记翻转多项式 \(B^R(x) = \sum_{i = 0}^n b_{n - i}x^i\),则点乘的值即为 \([x^n](A \times B^R)\),即 \(A \times B^R\) 的其中一项。

\(A\) 可以分解成两个多项式乘积 \(A = A_1 \times A_2\) 时,若已知 \(A_1 \times B^R\) 以及 \(A_2\),可以 \(\mathcal{O}(n)\) 求出 \([x^n](A \times B^R)\)

(例如:给出多项式序列 \(p_1, \cdots, p_n\),欲求 \(\left(\prod_{i = l}^r p_i\right) \cdot \mathbf{B}\),即求 \(\mathrm{pre}_r \times \mathrm{ipre}_{l - 1} \times B^R\)。预处理出所有的 \(\mathrm{pre}_i \times B^R\) 以及 \(\mathrm{ipre}_i\) 即可)

Model:分治卷积

分治 NTT - 模型 1:给出 \(n\) 个数 \(a_1, \cdots, a_n\),求出 \(n\) 个单项式的积 \(\prod_{i = 1}^n(a_ix + 1)\)

\(f(l, r)\) 表示区间 \([l, r]\) 中所有单项式的积,\(f(l, r)\) 是一个次数为 \(r - l + 1\) 的单项式(与区间长度有关)

\(\mathrm{mid} = \lfloor \frac{l + r}{2} \rfloor\),则有

\[f(l, r) = f(l, \mathrm{mid}) \times f(\mathrm{mid} + 1, r) \]

这是一个分治的过程,不断分治直到 \(l = r\) 即可。

时间复杂度 \(T(n) = T\left( \frac{n}{2} \right) + \mathcal{O}(n \log n) = \mathcal{O}(n \log^2 n)\)

By the way:求出 \(n\) 个多项式的积,次数之和为 \(m\)

可以每次取出两个次数最小的多项式相乘后放回,时间复杂度 \(\mathcal{O}(m \log^2 m)\)


分治 NTT - 模型 2:给出 \(n\) 个数 \(a_1, \cdots, a_n\),求出 \(n\) 个单项分式的和 \(\sum_{i = 1}^n \frac{1}{1 - a_ix}\)

对于分式 \(\frac{A_1}{A_2}, \frac{B_1}{B_2}\),若我们已知分子 \(A_1, B_1\) 与分母 \(A_2, B_2\)由通分可得 \(\frac{A_1}{A_2} + \frac{B_1}{B_2} = \frac{A_1B_2 + A_2B_1}{A_2B_2}\),由此可以得到新的分子 \(A_1B_2 + A_2B_1\) 与分母 \(A_2B_2\)

\(A(l, r), B(l, r)\) 分别表示区间 \([l, r]\) 中所有单项分式的和,按照上述合并方法所得的分子与分母。\(A(l, r), B(l, r)\) 的次数同样与区间长度有关。

\(\mathrm{mid} = \lfloor \frac{l + r}{2} \rfloor\),则有

\[A(l, r) = A(l, \mathrm{mid}) \times B(\mathrm{mid} + 1, r) + A(\mathrm{mid} + 1, r) \times B(l, \mathrm{mid}) \\ B(l, r) = B(l, \mathrm{mid}) \times B(\mathrm{mid} + 1, r) \]

通过分治求出 \(A(1, n), B(1, n)\),最后求逆一次即可。

时间复杂度 \(\mathcal{O}(n \log^2 n)\)


分治 NTT - 模型 3:给出 \(n\) 个数 \(a_1, \cdots, a_n\),求出 \(\sum_{i = 1}^n \prod_{j \neq i} (a_jx + 1)\)

\(f(l, r) = \sum_{i = l}^r \prod_{j \in [l, r], j \neq i} (a_jx + 1)\)\(g(l, r) = \prod_{i = l}^r (a_ix + 1)\)\(f(l, r), g(l, r)\) 的次数同样与区间长度有关。

\(\mathrm{mid} = \lfloor \frac{l + r}{2} \rfloor\),则有

\[f(l, r) = f(l, \mathrm{mid}) \times g(\mathrm{mid} + 1, r) + f(\mathrm{mid} + 1, r) \times g(l, \mathrm{mid}) \\ g(l, r) = g(l, \mathrm{mid}) \times g(\mathrm{mid} + 1, r) \]

这是一个分治的过程,不断分治直到 \(l = r\) 即可。

时间复杂度 \(\mathcal{O}(n \log^2 n)\)

Model:半在线卷积

半在线卷积:给出 \(g_1, \cdots, g_n\),求出 \(f_0, \cdots, f_n\),其中初值 \(f_0 = 1\),递推式 \(f_i = \sum_{j = 1}^i f_{i - j}g_j\)

使用强制中序遍历转移的 CDQ 分治:设 \(\mathrm{solve}(l, r)\) 表示求出区间 \([l, r]\) 中所有的 \(f\) 值,取 \(\mathrm{mid} = \lfloor \frac{l + r}{2} \rfloor\)

  • 先递归 \(\mathrm{solve}(l, \mathrm{mid})\),求出 \(f_l, \cdots, f_{\mathrm{mid}}\)
  • 计算 \(f_{[l, \mathrm{mid}]}\)\(g_{[0, r - l]}\) 的卷积结果,求出左半区间对右半区间的贡献
  • 后递归 \(\mathrm{solve}(\mathrm{mid} + 1, r)\),求出 \(f_{\mathrm{mid} + 1}, \cdots, f_r\)
  • 最后递归到 \(l = r\) 时,请注意根据具体递推式调整 \(f_i\)(e.g. 递推式 \(f_i = g_i - \sum_{j = 1}^{i - 1} \binom{i - 1}{j - 1} f_{i - j}g_j\))。

时间复杂度 \(T(n) = T\left( \frac{n}{2} \right) + \mathcal{O}(n \log n) = \mathcal{O}(n \log^2 n)\)

卡常技巧:计算 \(f_{[l, \mathrm{mid}]}\)\(g_{[0, r - l]}\) 的卷积结果时,实际上是 \(\frac{\mathrm{len}}{2}\) 次多项式与 \(\mathrm{len}\) 次多项式相乘。

由于 NTT 是循环卷积(\(a_ib_j \to c_{(i + j) \bmod L}\),其中 \(L\) 表示补充界),将补充界设定成第一个 \(\geq \mathrm{len}\)\(2\) 的幂,普通卷积的最后 \(\frac{\mathrm{len}}{2}\) 项会破坏掉循环卷积的前 \(\frac{\mathrm{len}}{2}\) 项,但由于我们只需要循环卷积的后 \(\frac{\mathrm{len}}{2}\) 项,所以将补充界调小不会对贡献产生影响。


全在线卷积:求出 \(f_0, \cdots, f_n\),其中初值 \(f_0 = 1\),递推式 \(f_i = \sum_{j = 1}^i f_{i - j}g_j\),其中 \(g_i\)\(f_0, \cdots, f_i\) 强相关。

仍然使用强制中序遍历转移的 CDQ 分治。但 \(g_{[0, r - l]}\) 我们不一定提前知道,进一步分析分治树的结构,发现仅有 \(l = 0\) 时不知道 \(g_{[0, r - l]}\)。所以我们需要修改计算贡献的方式。

  • \(l = 0\) 时,计算 \(f_{[0, \mathrm{mid}]} \times g_{[0, \mathrm{mid}]}\) 的贡献(此时还有 \(f_{[0, \mathrm{mid}]} \times g_{[\mathrm{mid} + 1, r]}\) 的贡献没被计算)。
  • \(l > 0\) 时,计算 \(f_{[l, \mathrm{mid}]} \times g_{[0, r - l]} + g_{[l, \mathrm{mid}]} \times f_{[0, r - l]}\) 的贡献(后半部分即为 \(l = 0\) 中漏掉的贡献)。

可以证明,点对的贡献不重不漏。

时间复杂度 \(\mathcal{O}(n \log^2 n)\)

多项式除法(暴力)

多项式除法:给出两个多项式 \(A(x), B(x)\),你需要求出多项式 \(C(x)\),满足 \(B(x)C(x) = A(x)\)

\(A(x) = \sum_{i = 0}^n a_ix^i, B(x) = \sum_{i = 0}^m b_ix^i, C(x) = \sum_{i = 0}^{n - m}c_ix^i\)。直接根据定义 \(\sum_{j = 0}^i c_jb_{i - j} = a_i\) 暴力展开(这里需要保证 \(b_0 \neq 0\),否则需要对 \(b\) 的前导零进行处理)。

初值

\[c_0 = \frac{a_0}{b_0} \]

递推式

\[c_i = \frac{1}{b_0} \left( a_i - \sum_{j = 0}^{i - 1}c_jb_{i - j} \right) \]

但递推式里的 \(j\) 只需要枚举到 \(i - j \leq m\)\(j \geq i - m\) 就可以了。

时间复杂度 \(\mathcal{O}(nm)\)

多项式导数/积分

多项式形式导数

\[f'(x) = \left( \sum\limits_{i = 0}^na_ix^i \right)' = \sum\limits_{i = 1}^n ia_ix^{i - 1} \]

多项式形式积分

\[\int f(x) \mathrm{dx} = \int \left( \sum\limits_{i = 0}^n a_ix^i \right) \mathrm{dx} = \sum\limits_{i = 0}^n \frac{1}{i + 1}a_ix^{i + 1} \]

多项式牛顿迭代

问题:已知函数 \(g\),假设满足 \(g(f(x)) = 0\) 的函数 \(f(x) = h(x)\),我们需要求出 \(h \bmod x^n\)(在这里,我们将 \(f(x)\) 看成函数 \(g(f(x))\) 的自变量,则我们要求的就是 \(g(f(x)) = 0\)\(x^n\) 意义下的零点)。

\(n = 1\) 时,\([x^0]h(x)\) 需要单独求出。

\(n > 1\) 时,\(h_0 = h \bmod x^{\left\lceil \frac{n}{2} \right\rceil}, h_1 = h \bmod x^n\),假设我们现在已经求出了 \(h_0\),我们希望通过 \(h_0\) 推出 \(h_1\)。考虑将函数 \(g(f(x))\)\(f(x) = h_0(x)\) 处泰勒展开。

\[g(f(x)) = \sum\limits_{i = 0}^{\infty} \frac{g^{(i)}(h_0(x))}{i!}(f(x) - h_0(x))^i \]

\(h_1(x)\) 代入自变量 \(f(x)\),观察到 \(h_1(x) - h_0(x)\) 的前 \(\left\lceil \frac{n}{2} \right\rceil\) 项均为 \(0\)(因为模 \(x^{\left\lceil \frac{n}{2} \right\rceil}\) 意义下两者显然相等)。

也就是说 \(h_1(x) - h_0(x)\)最低次数\(\left\lceil \frac{n}{2} \right\rceil\),进一步有 \(\left( h_1(x) - h_0(x) \right)^i\) 的最低次数为 \(\left\lceil \frac{n}{2} \right\rceil^i\)。当 \(i \geq 2\) 时,\(\left( h_1(x) - h_0(x) \right)^i \bmod x^n\) 均为 \(0\)于是可以忽略 \(i \geq 2\) 的项

进一步可以得到

\[g(h_0(x)) + g'(h_0(x))\left( h_1(x) - h_0(x) \right) \equiv 0 \pmod{x^n} \]

移项可得

\[h_1(x) \equiv h_0(x) - \frac{g(h_0(x))}{g'(h_0(x))} \pmod{x^n} \]

时间复杂度 \(T(n) = T\left(\frac{n}{2}\right) + \mathcal{O}(n \log n) = \mathcal{O}(n \log n)\)

多项式求逆

多项式逆:给出一个多项式 \(A(x)\),你需要求出多项式 \(B(x)\),满足 \(A(x) \times B(x) = 1\)。一般我们只需要求出 \(B \bmod{x^m}\) 的结果。

多项式逆存在条件:多项式 \(A(x)\) 存在逆,当且仅当 \([x^0]A(x) \neq 0\)

(注意:一个多项式的逆可能是有无穷项的,例如 \(\frac{1}{1 - x} = 1 + x + x^2 + \cdots\)

少项式求逆(暴力)

\(A(x) = \sum_{i = 0}^n a_ix^i, B(x) = \sum b_ix^i\)。直接根据多项式逆的定义 \(\sum_{j = 0}^{i} b_ja_{i - j} = [i = 0]\) 暴力展开。

初值

\[b_0 = \frac{1}{a_0} \]

递推式

\[b_i = -\frac{1}{a_0} \sum\limits_{j = 0}^{i - 1}b_ja_{i - j} \]

但递推式里的 \(j\) 只需要枚举到 \(i - j \leq n\)\(j \geq i - n\) 就可以了。

时间复杂度 \(\mathcal{O}(nm)\)当次数 \(n\) 较小时有奇效

这也验证了 "多项式逆存在条件" 的充分性。

多项式求逆(牛顿迭代)

构造函数 \(g(f(x)) = \frac{1}{f(x)} - A(x)\),代入牛顿迭代法得

\[h_1(x) \equiv h_0(x) - \frac{\frac{1}{h_0(x)} - A(x)}{-\frac{1}{h_0^2(x)}} \pmod{x^m} \\ h_1(x) \equiv 2h_0(x) - A(x)h_0^2(x) \pmod{x^m} \]

初始时,\(A^{-1}(x)\) 的常数项即为 \(A(x)\) 常数项的逆元。

时间复杂度 \(\mathcal{O}(m \log m)\)

多项式开方

多项式开方:给出一个多项式 \(A(x)\),你需要求出多项式 \(B(x)\),满足 \(B^2(x) \equiv A(x)\)。一般我们只需要求出 \(B \bmod x^m\) 的结果。

多项式开方存在条件:多项式 \(A(x)\) 存在开方,当且仅当 \([x^0]A(x)\) 存在模意义下的二次剩余。

构造函数 \(g(f(x)) = f^2(x) - A(x)\),代入牛顿迭代法得

\[h_1(x) \equiv h_0(x) - \frac{h_0^2(x) - A(x)}{2h_0(x)} \pmod{x^m} \\ h_1(x) \equiv \frac{h_0^2(x) + A(x)}{2h_0(x)} \pmod{x^m} \]

初始时,\(\sqrt{A(x)}\) 的常数项即为 \(A(x)\) 常数项的二次剩余。当 \(A(x)\) 常数项不等于 \(1\) 时,还需要通过二次剩余求解常数项。

时间复杂度 \(\mathcal{O}(m \log m)\)

By the way:注意到 \(\sqrt{A(x)} = e^{\frac{1}{2}\ln A(x)}\),可以直接套用多项式 ln 与多项式 exp 的模板。

多项式带余除法

多项式除法 & 取模:给出一个 \(n\) 次多项式 \(A(x)\) 和一个 \(m\) 次多项式 \(B(x)\),你需要求出多项式 \(Q(x), R(x)\) 满足:

  • \(Q(x)\) 的次数为 \(n - m\)\(R(x)\) 的次数 \(<m\)
  • \(A(x) = Q(x) \times B(x) + R(x)\)

考虑构造变换 \(f^R(x) = x^{\mathrm{deg} f} f\left(\frac{1}{x}\right)\),实际上即为翻转 \(f\) 的系数。

\(A(x) = Q(x) \times B(x) + R(x)\) 中的 \(x\) 替换为 \(\frac{1}{x}\),并且等式两边同乘以 \(x^n\)

\[x^nA\left(\frac{1}{x}\right) = x^{n - m}Q\left(\frac{1}{x}\right) \times x^mB\left(\frac{1}{x}\right) + x^{n - m + 1}x^{m - 1} R\left(\frac{1}{x}\right) \\ A^R(x) = Q^R(x) \times B^R(x) + x^{n - m + 1}x^{m - 1}R\left(\frac{1}{x}\right) \\ A^R(x) = Q^R(x) \times B^R(x) \pmod{x^{n - m + 1}} \]

由于 \(Q(x)\) 的次数小于 \(n - m + 1\),故对 \(x^{n - m + 1}\) 取模没有影响。

故在模 \(x^{n - m + 1}\) 意义下求出 \(\frac{A^R(x)}{B^R(x)}\) 再翻转一遍即可求出 \(Q(x)\),再使用 \(A(x) - Q(x) \times B(x)\) 即可求出 \(R(x)\)

时间复杂度 \(\mathcal{O}(n \log n)\)

多项式 ln/exp

\(e\) 在模 \(998244353\) 下没有意义。同理,\(\ln 2\) 在模 \(998244353\) 下没有意义。

简单的解释:考察 \(e = \sum_{i = 0}^\infty \frac{1}{i!}\),在 \(i \geq 998244353\) 时,分式就已经失去了意义。即使有意义,无穷项求和在模 \(998244353\) 下也不会收敛。

多项式 ln 与多项式 exp 的定义:由麦克劳林级数定义

\[\begin{aligned} \ln f(x) & = \sum\limits_{i = 1}^{\infty} (-1)^{i + 1} \frac{(f(x) - 1)^i}{i} \\ & = - \sum\limits_{i = 1}^{\infty} \frac{(1 - f(x))^i}{i} \\ \exp f(x) & = \sum\limits_{i = 0}^{\infty} \frac{f^i(x)}{i!} \end{aligned} \]

多项式 ln 存在条件多项式 \(f(x)\) 存在 ln,当且仅当 \([x^0]f(x) = 1\),并且由此可得 \([x^0] \ln f(x) = 0\)

多项式 ln:给出一个多项式 \(A(x)\),你需要求出多项式 \(B(x) = \ln A(x)\)。一般我们只需要求出 \(B \bmod{x^m}\) 的结果。

\(B(x) = \ln A(x)\) 等式两边对 \(x\) 求导可得

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

两边同时积分可得

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

注意:虽然多项式先求导后积分会丢掉常数项的信息,但是容易验证当 \(A(x)\) 的常数项为 \(1\) 时,\(B(x)\) 的常数项必定为 \(0\)

时间复杂度 \(\mathcal{O}(m \log m)\)

多项式 exp 存在条件多项式 \(f(x)\) 存在 exp,当且仅当 \([x^0]f(x) = 0\),并且可以推出 \([x^0]\exp f(x) = 1\)

多项式 exp:给出一个多项式 \(A(x)\),你需要求出多项式 \(B(x) = \exp A(x)\)。一般我们只需要求出 \(B \bmod{x^m}\) 的结果。

构造函数 \(g(f(x)) = \ln f(x) - A(x)\),代入牛顿迭代得

\[h_1(x) \equiv h_0(x) - \frac{\ln h_0(x) - A(x)}{\frac{1}{h_0(x)}} \pmod{x^m} \\ h_1(x) \equiv h_0(x)\left( 1 - \ln h_0(x) + A(x) \right) \pmod{x^m} \]

初始时,\(\exp A(x)\) 的常数项必为 \(1\)

时间复杂度 \(\mathcal{O}(m \log m)\)

多项式快速幂

多项式快速幂:给出一个多项式 \(A(x)\),你需要求出 \(A^k(x) \bmod{x^m}\)

多项式快速幂(倍增法):具体算法流程同快速幂。

时间复杂度 \(\mathcal{O}(m \log m \log k)\),但可以处理的问题形式更加灵活。

e.g. 多项式快速幂对另一多项式取模。

e.g. 多项式快速幂且保留次数在 \([-m, m]\) 的项。

多项式快速幂(先 ln 后 exp):注意到 \(A^k(x) = e^{k\ln A(x)}\),考虑直接套用多项式 ln 与多项式 exp 的模板。

但由于多项式 ln 的一些限制(需要保证 \([x^0]A(x) = 1\)),首先要对 \(A(x)\) 进行一些操作

找出一个最小的 \(i\),满足 \([x^i]A(x) \neq 0\)(相当于是找出 \(A(x)\) 的第一个非零系数),记 \(v = [x^i]A(x)\)。则有

\[A(x) = \left(\frac{A(x)}{x^iv}\right)x^iv \]

注意到 \(\frac{A(x)}{x^iv}\) 满足常数项为 \(1\) 的限制,可以进行多项式 ln

然后有

\[A^k(x) = \left(\frac{A(x)}{x^iv}\right)^k x^{ik}v^k \]

先使用多项式 ln 与多项式 exp 求出 \(\left(\frac{A(x)}{x^iv}\right)^k \bmod{x^{m - ik}}\) 的值,最后再乘上 \(x^{ik}v^k\) 即可。
(注意:这里的乘 \(x^{ik}\) 相当于多项式系数向右移动 \(ik\) 位;上面的除以 \(x^i\) 同理,相当于多项式系数向左移动 \(i\) 位,即舍弃掉前 \(i\) 项系数)

时间复杂度 \(\mathcal{O}(m \log m)\)

设模数为质数 \(P\)

  • \(k \leq P\) 时,可以放心应用该做法。
  • \(k > P\) 时,需要进一步讨论 \(k\) 的取模相关问题

引理 1:设 \(P\) 为质数。则对于任意整数 \(a, b\)

\[(a + b)^P \equiv a^P + b^P \pmod{P} \]

证明:因为 \((a + b)^P = \sum_{i = 0}^P \binom{P}{i}a^ib^{P - i}\),当 \(0 < i < P\) 时,有 \(\binom{P}{i} = \frac{P}{i}\binom{P - 1}{i - 1}\),由于 \(P\) 是质数,所以该式子是 \(P\) 的倍数。故上式得证。

引理 2:设 \(f(x)\) 为一个 \(n\) 次多项式,\(P\) 为质数。则有

\[f^P(x) \equiv f(x^P) \pmod{P} \]

证明:不断地应用引理 1 即可。

考虑计算 \(f^P(x) \bmod x^m\),由于一般情况下 \(m < p\),所以 \(f^P(x) \equiv f(x^P) \equiv \left([x^0]f(x)\right)^P \pmod{x^m}\)。由于我们进行了一系列操作使得 \([x^0]f(x) = 1\),所以 \(f^P(x) \equiv 1 \pmod{x^m}\)

  • 所以在多项式 ln 与多项式 exp 部分,\(k\) 需要对 \(P\) 取模
  • 考虑 \(x^{ik}\),当 \(i = 0\) 时,可以直接忽略;当 \(i > 0\) 时,\(k\) 显然不能超过 \(P\),否则右移 \(ik\) 位已经远超需要保留的范围。所以乘以 \(x^{ik}\) 部分,当 \(i > 0\)\(k\) 不能超过 \(P\)
  • 考虑 \(v^k\),由于是数值计算,所以乘以 \(v^k\) 部分,\(k\) 需要对 \(\varphi(P) = P - 1\) 取模

少项式快速幂

少项式快速幂:给出一个次数不高的多项式 \(A(x)\),你需要求出 \(A^k(x) \bmod x^m\)

\(A(x) = \sum_{i = 0}^n a_ix^i, A^k(x) = \sum_{i = 0}^{m - 1}b_ix^i\)。考虑对 \(A^k(x)\) 求导

\[A^k(x)' = kA^{k - 1}(x)A'(x) \\ {\color{blue}A^k(x)'}A(x) = k{\color{blue}A^{k}(x)}A'(x) \]

注意到 \(A^k(x)\)\(A^k(x)'\) 有一定的递推关系。具体地,考虑等式左右两边的 \(i\) 次项

\[\sum\limits_{j = 0}^i (j + 1)b_{j + 1}a_{i - j} = k\sum_{j = 0}^i(i - j + 1)a_{i - j + 1}b_j \]

等式左边只需枚举到 \(j \geq i - n\),等式右边只需枚举到 \(j \geq i - n + 1\)。可以解得

\[b_{i + 1} = \frac{\left( k\sum_{j = i - n + 1}^{i} (i - j + 1)a_{i - j + 1}b_j \right) - \left( \sum_{j = i - n}^{i - 1}(j + 1)b_{j + 1}a_{i - j} \right)}{(i + 1)a_0} \]

初值 \(b_0 = a_0^k\),上式可以在已知 \(b_0, b_1, \cdots, b_i\) 的情况下递推出 \(b_{i + 1}\)

需要线性预处理出 \(1, 2, \cdots, m - 1\) 的逆元,递推式:\(i^{-1} \equiv -\left\lfloor \frac{p}{i} \right\rfloor (p \bmod i)^{-1} \pmod{p}\)

时间复杂度 \(\mathcal{O}(nm)\)当次数 \(n\) 较小时有奇效

By the way:从这个递推式进行归纳可知,当 \(a_0 = 1\) 时,\([x^n]A^k(x)\) 是一个关于 \(k\)\(n\) 次多项式。

*转置原理

待填坑。

多项式多点求值

多项式多点求值:给出一个多项式 \(f(x)\)\(n\) 个点 \(x_1, x_2, \cdots, x_n\),求 \(f(x_1), f(x_2), \cdots, f(x_n)\)

多项式取模做法

考虑使用分治将问题规模减半。设 \(\mathrm{mid} = \left\lfloor \frac{1 + n}{2} \right\rfloor\),将给定的点集分成两部分

\[X_1 = \{ x_1, \cdots, x_{\mathrm{mid}} \}, \quad X_2 = \{ x_{\mathrm{mid} + 1}, \cdots, x_{n} \} \]

构造多项式

\[g_1(x) = \prod\limits_{x_i \in X_1}(x - x_i), \quad g_2(x) = \prod\limits_{x_i \in X_2}(x - x_i) \]

由多项式取模,记 \(f(x) = Q_1(x) \times g_1(x) + R_1(x)\)对于 \(X_1\) 中的元素 \(x_i\),由于 \(g_1(x_i) = 0\),则有 \(f(x_i) = R_1(x_i)\)。又由于 \(R_1(x)\) 的次数小于区间长度的一半,于是问题规模成功减半。对于 \(X_2\) 中的元素也是同理。

多项式 \(g\) 可以通过分治 NTT 预处理。

时间复杂度 \(T(n) = 2T(n) + \mathcal{O}(n \log n) = \mathcal{O}(n \log^2 n)\)

另一种理解角度:\(f(x) \equiv f(x_i) \pmod{x - x_i}\)。证明只需将 \(x^n = \left((x - x_i) + x_i\right)^n\) 二项式定理展开即可。

/* 多项式多点求值(多项式取模做法),作为 poly 的成员函数 */
constexpr std::vector<int> eval(std::vector<int> x) const {
    int n = x.size();
    if (size() == 0) {
        return std::vector<int>(n, 0);
    }

    std::vector<poly> t(n * 4);
    std::vector<int> ans(n);

    std::function<void(int, int, int)> build = [&] (int p, int l, int r) {
        if (l == r) {
            int v = x[l];
            t[p] = poly{v ? mod - v : 0, 1};
            return;
        }
        int mid = (l + r) >> 1;
        build(p * 2, l, mid), build(p * 2 + 1, mid + 1, r);
        t[p] = t[p * 2] * t[p * 2 + 1];
    };
    build(1, 0, n - 1);

    std::function<void(int, int, int, const poly &)> solve = [&] (int p, int l, int r, const poly &f) {
        if (l == r) {
            ans[l] = f[0];
            return;
        }
        int mid = (l + r) >> 1;
        auto [q1, r1] = divmod(f, t[p * 2]);
        solve(p * 2, l, mid, r1);
        auto [q2, r2] = divmod(f, t[p * 2 + 1]);
        solve(p * 2 + 1, mid + 1, r, r2);
    };
    solve(1, 0, n - 1, *this);

    return ans;
}

转置做法

待填坑。

多项式快速插值

多项式快速插值:给出 \(n\) 个点 \((x_1, y_1), (x_2, y_2), \cdots, (x_n, y_n)\),其中横坐标两两不同。快速地求一个 \(n - 1\) 次多项式,使得这 \(n\) 个点都在 \(f(x)\) 上。

考虑拉格朗日插值

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

\(g(x) = \prod_{j = 1}^n(x - x_j)\),则

\[\prod_{j \neq i} (x_i - x_j) = \lim_{x \to x_i} \frac{\prod_{j = 1}^n (x - x_j)}{x - x_i} = g'(x_i) \]

首先使用分治 NTT 求出 \(g'(x)\),再使用多项式多点求值,求出所有的 \(g'(x_i)\)

\(v_i = \frac{y_i}{g'(x_i)}\),则原式化为

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

仍然使用分治 NTT 求出上式。具体地,设 \(\mathrm{mid} = \left\lfloor \frac{1 + n}{2} \right\rfloor\),设多项式

\[f_1(x) = \sum\limits_{i = 1}^\mathrm{mid} v_i \left( \prod\limits_{j \neq i, j \leq \mathrm{mid}} (x - x_j) \right), \quad g_1(x) = \prod\limits_{j \leq \mathrm{mid}} (x - x_j) \\ f_2(x) = \sum\limits_{i = \mathrm{mid} + 1}^n v_i \left( \prod\limits_{j \neq i, j > \mathrm{mid}} (x - x_j) \right), \quad g_2(x) = \prod\limits_{j > \mathrm{mid}} (x - x_j) \]

显然有 \(f(x) = f_1(x)g_2(x) + f_2(x)g_1(x)\)\(f_1, f_2, g_1, g_2\) 可以进一步分治求解。

时间复杂度 \(\mathcal{O}(n \log^2 n)\)

多项式平移

多项式平移:给出一个多项式 \(A(x)\) 以及一个常数 \(c\),你需要求出多项式 \(A(x + c)\)

多项式平移(二项式定理法):设 \(A(x) = \sum_{i = 0}^n a_ix^i\),考虑二项式定理

\[\begin{aligned} A(x + c) & = \sum_{i = 0}^n a_i(x + c)^i \\ & = \sum_{i = 0}^n a_i \sum_{j = 0}^i \binom{i}{j}x^jc^{i - j} \\ & = \sum_{i = 0}^n a_i \sum_{j = 0}^i \frac{i!}{j!(i - j)!}x^jc^{i - j} \\ & = \sum_{i = 0}^n a_i \cdot i! \cdot \sum_{j = 0}^i \frac{x^j}{j!} \cdot \frac{c^{i - j}}{(i - j)!} \\ & = \sum_{j = 0}^n \frac{x^j}{j!} \cdot \left( \sum_{i = j}^n {\color{red} a_i \cdot i!} \cdot {\color{blue} \frac{c^{i - j}}{(i - j)!}} \right) \\ \end{aligned} \]

注意到括号里的求和式,下标之差恒为 \(j\),于是可以使用 "翻转卷积" 的技巧求出每个关于 \(j\) 的求和式。

时间复杂度 \(\mathcal{O}(n \log n)\)

连续点值平移:给出 \(n + 1\) 个连续点值 \(f(0), f(1), \cdots, f(n)\)(其中 \(f\) 是一个次数至多为 \(n\) 的多项式)以及一个常数 \(c\),你需要求出 \(f(c), f(c + 1), \cdots, f(c + n)\)

由于算法流程会涉及到 \(\frac{1}{c - n + i}\)(其中 \(i = 0 \sim 2n\)),我们不妨设 \(c > n\)

对于 \(c \leq n\) 的情况,首先 \(f(c), \cdots, f(n)\) 是已知的,然后 \(f(n + 1), \cdots, f(n + c)\) 可以通过 \(f(0), \cdots, f(n)\) 向右平移 \(n + 1\) 然后取前 \(c\) 项获得(注意这里不能只取 \(f(0), \cdots, f(c - 1)\) 向右平移 \(n + 1\),否则多项式的次数会变小)。

代入拉格朗日插值公式,得

\[\begin{aligned} f(x) & = \sum_{i = 0}^n y_i \prod_{j \neq i} \frac{x - j}{i - j} \\ & = \sum_{i = 0}^n y_i \cdot \frac{\prod_{p = x - n}^{x} p}{x - i} \cdot \frac{(-1)^{n - i}}{i!(n - i)!} \\ & = \left(\prod_{p = x - n}^{x} p\right) \sum_{i = 0}^n \frac{1}{x - i} \cdot \frac{y_i(-1)^{n - i}}{i!(n - i)!} \end{aligned} \]

于是

\[f(c + x) = \left( \prod_{p = c + x - n}^{c + x} p \right)\sum_{i = 0}^n \frac{1}{c + x - i} \cdot \frac{y_i(-1)^{n - i}}{i!(n - i)!} \]

注意到右边的求和式,是一个卷积的形式。

\(a_i = \frac{1}{c - n + i}\)(其中 \(i = 0 \sim 2n\),因为分母的取值范围为 \([c - n, c + n]\)),\(b_i = \frac{y_i(-1)^{n - i}}{i!(n - i)!}\)。则右边的求和式为 \(\sum_{i = 0}^n a_{x - i + n}b_i\)

\(a, b\) 进行卷积得到 \(\mathrm{res}\),则

\[f(c + x) = \left( \prod_{p = c + x - n}^{c + x} p \right) \mathrm{res}_{x + n} \]

时间复杂度 \(\mathcal{O}(n \log n)\)

给出 \(n + 1\) 个等差点值 \(f(b), f(b + k), \cdots, f(b + nk)\)(其中 \(f\) 是一个次数至多为 \(n\) 的多项式) 以及一个常数 \(c\),你需要求出 \(f(c + b), f(c + b + k), \cdots, f(c + b + nk)\)

\(g(x) = f(b + xk)\),可以看成:已知 \(g(0), g(1), \cdots, g(n)\),向右平移 \(\frac{c}{k}\) 得到 \(g(\frac{c}{k}), g(\frac{c}{k} + 1), \cdots, g(\frac{c}{k} + n)\)

应用:快速阶乘算法

快速阶乘算法:求 \(n! \bmod p\),其中 \(p\) 是一个大质数(不保证是一个 NTT 模数的情况下,需要使用 MTT)。

当模数固定时,可以考虑分段打表。

\(v = \left\lfloor \sqrt{n} \right\rfloor\),设多项式 \(g(x) = \prod_{i = 1}^v (x + i)\),则

\[n! = \left( \prod_{i = 0}^{v - 1}g(iv) \right) \cdot \left( \prod_{i = v^2 + 1}^n i \right) \]

首先后半部分的 \(\prod_{i = v^2 + 1}^n i\) 可以 \(\mathcal{O}(\sqrt{v})\) 求出。我们现在只关心前半部分。

设多项式 \(g_d(x) = \prod_{i = 1}^d (x + i)\),则 \(d + 1\) 个点值 \(g_d(0), g_d(v), \cdots, g_d(dv)\) 可以唯一确定 \(d\) 次多项式 \(g_d(x)\)

所以对于当前的 \(d\),我们都需要维护出这 \(d + 1\) 个点值。初始时 \(d = 1\),只有点值 \(g_1(0) = 1, g_1(v) = v + 1\)。考虑两种操作:

操作 1:\(d \gets 2d\)

  • 补齐点值:补齐 \(g_d((d + 1)v), \cdots, g_d(2dv)\),可以通过 \(g_d(0), g_d(v), \cdots, g_d(dv)\) 向右平移 \(dv\) 得到。
    (可以看作连续点值 \(h(x) = g(xv)\) 向右平移 \(d\)
  • 修正点值:有公式 \(g_{2d}(x) = g_d(x)g_d(x + d)\),将所有点值 \(g_d(x)\) 向右平移 \(d\) 即可得到 \(g_d(x + d)\),相乘即可。
    (可以看作连续点值 \(h(x) = g(xv)\) 向右平移 \(\frac{d}{v}\)

操作 2:\(d \gets d + 1\)

  • 修正点值:有公式 \(g_{d + 1}(x) = g_d(x) \cdot (x + d + 1)\),直接相乘即可。
  • 补齐点值:补齐 \(g_{d + 1}((d + 1)v)\),直接暴力 \(\mathcal{O}(d)\) 求出即可。

于是可以从 \(d = 1\) 通过倍增变换到 \(d = v\)。倍增过程的时间复杂度为 \(T(n) = T(\frac{n}{2}) + \mathcal{O}(n \log n) = \mathcal{O}(n \log n)\)

而我们只需要 \(\mathcal{O}(\sqrt{n})\) 个点值,故时间复杂度 \(\mathcal{O}(\sqrt{n} \log n)\)

0x31 快速阶乘算法.cpp

// 快速阶乘算法
int fastFactorial(int n) {
    if (n >= mod) {
        return 0;
    }

    int v = sqrt(n), iv = qpow(v, mod - 2, mod);

    std::function< std::vector<int>(int) > solve = [&] (int d) {
        if (d == 1) {
            return std::vector<int>{1, v + 1};
        }
        if (d & 1) {
            auto y = solve(d - 1);
            for (int i = 0; i < y.size(); i ++) {
                mul(y[i], i * v + d);
            }

            int num = 1;
            for (int i = 1; i <= d; i ++) {
                mul(num, d * v + i);
            }
            y.push_back(num);

            return y;
        } else {
            auto y = solve(d / 2);
            auto y1 = shift_conti(y, d / 2);
            y.insert(y.end(), y1.begin() + 1, y1.end());

            auto y2 = shift_conti(y, 1ll * (d / 2) * iv % mod);
            for (int i = 0; i < y.size(); i ++) {
                mul(y[i], y2[i]);
            }

            return y;
        }
    };

    auto res = solve(v);

    int ans = 1;
    for (int i = 0; i < v; i ++) {
        mul(ans, res[i]);
    }
    for (int i = v * v + 1; i <= n; i ++) {
        mul(ans, i);
    }
    return ans;
}

⭐多项式全家桶

reference:jiangly 算法模板。

封装理念:

  • 构建结构体 poly 继承自 std::vector<int>,自由度高且易于访问。
  • 若函数的功能涉及 "当前多项式的变换或求值",均作为 poly 的内置函数;其余功能作为外置函数。

详细的使用手册位于代码注释之中!

0x31 多项式全家桶.cpp

/**
 *    多项式全家桶 使用手册
 *    author:Calculatelove
 *    version:2025.10.14
 * 
 *  版本兼容:
 *  - C++23 以上:请放心使用
 *  - C++17 ~ C++20:
 *      请删去所有 poly 中的 constexpr(在 poly 上方 #define constexpr 即可)
 *  - C++14:
 *      请将 RootPower 前面的 constexpr 改成 const
 *  - C++11:
 *      请删去所有函数体前面的 constexpr,
 *      请将 findRootPower 与 RootPower 前面的 auto 改成 std::array<int, 32>
 * 
 *  模数:
 *  - 涉及到多项式乘法时:
 *      必须为 NTT 模数!否则请将 "多项式乘法" 替换为 "MTT 版本的多项式乘法"
 *  - 仅涉及到 poly 的四则运算、求导、积分,以及暴力多项式 mul_bf, div_bf, inv_bf,pow_bf 时:
 *      只需模数为质数即可
 * 
 *  原根:
 *  - 默认为 3,请根据具体 NTT 模数确认原根(在 findRootPower() 中修改)
 * 
 *  已有的常数优化:
 *  - 使用了一些函数辅助取模(0x11 取模.cpp)
 *  - 使用 findRootPower() 预处理原根的幂(即单位根)
 * 
 *  可能的常数优化:
 *  - 半在线卷积中,补充界只需 >= r - l + 1
 *  - 预处理阶乘以及阶乘逆元
 *      polyinit_factorial() -> shift(), shift_conti()
 *  - 预处理线性逆元
 *      polyinit_linearInverse() -> integ(), pow_bf()
 */
 
// 预处理单位根
constexpr auto findRootPower() {
    int g = 3; // 模数对应的原根,需根据实际问题调整
    std::array<int, 32> w{};
    for (int k = 1, idx = 0; (mod - 1) % (k << 1) == 0; k <<= 1) {
        w[idx ++] = qpow(g, (mod - 1) / (k << 1), mod); // 2k 阶单位根
    }
    return w;
}

constexpr auto RootPower = findRootPower();

std::vector<int> rev;
void dft(std::vector<int> &a) {
    int n = a.size();
    for (int i = 0; i < n; i ++) {
        if (i < rev[i]) {
            std::swap(a[i], a[rev[i]]);
        }
    }
    for (int k = 1, idx = 0; k < n; k <<= 1) {
        int omega = RootPower[idx ++];
        for (int i = 0; i < n; i += (k << 1)) {
            int x = 1;
            for (int j = 0; j < k; j ++, mul(x, omega)) {
                int u = a[i + j], v = 1ll * x * a[i + j + k] % mod;
                add(a[i + j] = u, v), dec(a[i + j + k] = u, v);
            }
        }
    }
}

void idft(std::vector<int> &a) {
    int n = a.size(), inv = qpow(n, mod - 2, mod);
    std::reverse(a.begin() + 1, a.end());
    dft(a);
    for (int i = 0; i < n; i ++) {
        mul(a[i], inv);
    }
}

/* 多项式全家桶 */
struct poly : public std::vector<int> {
    poly() : std::vector<int>() {}
    explicit constexpr poly(int n) : std::vector<int>(n) {}
    explicit constexpr poly(const std::vector<int> &a) : std::vector<int>(a) {}
    constexpr poly(const std::initializer_list<int> &a) : std::vector<int>(a) {}

    template <class InputIt, class = std::_RequireInputIter<InputIt>>
    explicit constexpr poly(InputIt st, InputIt ed) : std::vector<int>(st, ed) {}

    // 多项式乘法
    constexpr friend poly operator * (poly a, poly b) {
        int tot = a.size() + b.size() - 1;
        if (tot < 128) {
            poly c(tot);
            for (int i = 0; i < a.size(); i ++) {
                for (int j = 0; j < b.size(); j ++) {
                    c[i + j] = (c[i + j] + 1ll * a[i] * b[j]) % mod;
                }
            }
            return c;
        }

        int L = 1, P = 0;
        while (L < tot) L <<= 1, P ++;
        rev.resize(L);
        for (int i = 0; i < L; i ++) {
            rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (P - 1));
        }

        a.resize(L), b.resize(L);
        dft(a), dft(b);
        for (int i = 0; i < L; i ++) {
            mul(a[i], b[i]);
        }
        idft(a);
        a.resize(tot);
        return a;
    }

    // 多项式乘法(暴力)
    constexpr friend poly mul_bf(poly a, poly b) {
        poly c(a.size() + b.size() - 1);
        for (int i = 0; i < a.size(); i ++) {
            for (int j = 0; j < b.size(); j ++) {
                c[i + j] = (c[i + j] + 1ll * a[i] * b[j]) % mod;
            }
        }
        return c;
    }
    // 多项式除法(暴力)
    constexpr friend poly div_bf(poly a, poly b) { // 需要保证 b 的常数项不为 0
        int m = b.size() - 1, inv = qpow(b[0], mod - 2, mod);
        poly c(a.size() - b.size() + 1);
        for (int i = 0; i < c.size(); i ++) {
            c[i] = a[i];
            for (int j = std::max(0, i - m); j < i; j ++) {
                dec(c[i], 1ll * c[j] * b[i - j] % mod);
            }
            mul(c[i], inv);
        }
        return c;
    }

    constexpr friend poly operator + (poly a, poly b) {
        poly c(std::max(a.size(), b.size()));
        for (int i = 0; i < a.size(); i ++) {
            add(c[i], a[i]);
        }
        for (int i = 0; i < b.size(); i ++) {
            add(c[i], b[i]);
        }
        return c;
    }
    constexpr friend poly operator - (poly a, poly b) {
        poly c(std::max(a.size(), b.size()));
        for (int i = 0; i < a.size(); i ++) {
            add(c[i], a[i]);
        }
        for (int i = 0; i < b.size(); i ++) {
            dec(c[i], b[i]);
        }
        return c;
    }
    constexpr friend poly operator - (poly a) {
        for (int &x : a) {
            neg(x);
        }
        return a;
    }

    constexpr friend poly operator * (poly a, int b) {
        for (int &x : a) {
            mul(x, b);
        }
        return a;
    }
    constexpr friend poly operator / (poly a, int b) {
        int inv = qpow(b, mod - 2, mod);
        for (int &x : a) {
            mul(x, inv);
        }
        return a;
    }

    constexpr poly &operator += (poly b) {
        return (*this) = (*this) + b;
    }
    constexpr poly &operator -= (poly b) {
        return (*this) = (*this) - b;
    }
    constexpr poly &operator *= (poly b) {
        return (*this) = (*this) * b;
    }
    constexpr poly &operator *= (int b) {
        return (*this) = (*this) * b;
    }
    constexpr poly &operator /= (int b) {
        return (*this) = (*this) / b;
    }

    constexpr poly mulxk(int k) const {
        auto res = *this;
        res.insert(res.begin(), k, 0);
        return res;
    }
    constexpr poly divxk(int k) const {
        if (k >= size()) return poly();
        return poly((*this).begin() + k, (*this).end());
    }
    constexpr poly modxk(int k) const {
        if (k >= size()) return *this;
        return poly((*this).begin(), (*this).begin() + k);
    }

    constexpr poly circ(int k) const {
        auto res = *this;
        for (int i = k; i < size(); i ++) {
            add(res[i - k], res[i]);
        }
        return res.modxk(k);
    }

    constexpr poly flip() const {
        auto res = *this;
        std::reverse(res.begin(), res.end());
        return res;
    }

    constexpr poly mulT(poly b) const {
        if (b.size() == 0) return poly();
        std::reverse(b.begin(), b.end());
        return ((*this) * b).divxk(b.size() - 1);
        // 等价于“翻转卷积”,a.mulT(b) 相当于是求 a[i] * b[j] -> c[i - j]
    }

    // 多项式导数
    constexpr poly deriv() const {
        if (empty()) return poly();
        poly res(size() - 1);
        for (int i = 1; i < size(); i ++) {
            res[i - 1] = 1ll * (*this)[i] * i % mod;
        }
        return res;
    }

    // 多项式积分
    constexpr poly integ() const {
        poly res(size() + 1);
        for (int i = 0; i < size(); i ++) {
            res[i + 1] = 1ll * (*this)[i] * qpow(i + 1, mod - 2, mod) % mod;
        }
        return res;
    }

    // 多项式求逆
    constexpr poly inv(int m) const { // 需要保证常数项不为 0
        poly x{qpow((*this)[0], mod - 2, mod)};
        int k = 1;
        while (k < m) {
            k <<= 1;
            x = (x * (poly{2} - modxk(k) * x)).modxk(k);
        }
        return x.modxk(m);
    }
    // 少项式求逆
    constexpr poly inv_bf(int m) const { // 需要保证常数项不为 0
        auto &&a = *this;
        int n = size() - 1, inv = qpow(a[0], mod - 2, mod);

        poly b(m);
        for (int i = 0; i < m; i ++) {
            b[i] = i == 0;
            for (int j = std::max(0, i - n); j < i; j ++) {
                dec(b[i], 1ll * b[j] * a[i - j] % mod);
            }
            mul(b[i], inv);
        }
        return b;
    }

    // 多项式开方
    constexpr poly sqrt(int m) const {
        poly x{1}; // 默认常数项为 1
        int k = 1;
        while (k < m) {
            k <<= 1;
            x = (x + (modxk(k) * x.inv(k)).modxk(k)) / 2;
        }
        return x.modxk(m);
    }

    // 多项式带余除法
    constexpr friend std::pair<poly, poly> divmod(poly a, poly b) {
        int n = a.size(), m = b.size(), L = n - m + 1;
        if (n < m) return {poly{0}, a};
        poly q = (a.flip().modxk(L) * b.flip().inv(L)).modxk(L).flip();
        poly r = a - b * q; while (r.size() && !r.back()) r.pop_back();
        return {q, r};
    }

    // 多项式 ln
    constexpr poly ln(int m) const { // 需要保证常数项为 1
        return (deriv() * inv(m)).integ().modxk(m);
    }

    // 多项式 exp
    constexpr poly exp(int m) const { // 需要保证常数项为 0
        poly x{1};
        int k = 1;
        while (k < m) {
            k <<= 1;
            x = (x * (poly{1} - x.ln(k) + modxk(k))).modxk(k);
        }
        return x.modxk(m);
    }

    // 多项式快速幂
    constexpr poly pow(int k, int m) const {
        int i = 0;
        while (i < size() && (*this)[i] == 0) {
            i ++;
        }
        if (i == size() || 1ll * i * k >= m) {
            return poly(m);
        }
        int v = (*this)[i];
        poly f = divxk(i) * qpow(v, mod - 2, mod);
        return (f.ln(m - i * k) * k).exp(m - i * k).mulxk(i * k) * qpow(v, k, mod);
        /*
            当实际 k < mod 时,可以放心使用
            当实际 k >= mod 时,注意:
            - 当 i > 0 时(即常数项为 0),直接返回长度为 m 的全 0 多项式
            - 当 i = 0 时
              - 在多项式 ln 与多项式 exp 部分,k 需要对 mod 取模
              - 在 qpow(v, k, mod) 部分,k 需要对 mod - 1 取模
        */
    }
    // 少项式快速幂
    constexpr poly pow_bf(int k, int m) const {
        std::vector<int> v(m);
        v[1] = 1;
        for (int i = 2; i < m; i ++) {
            v[i] = 1ll * v[mod % i] * (mod - mod / i) % mod;
        }

        auto &&a = *this;
        int n = size() - 1, inv = qpow(a[0], mod - 2, mod);

        poly b(m);
        b[0] = qpow(a[0], k, mod);
        for (int i = 0; i + 1 < m; i ++) {
            b[i + 1] = 0;
            for (int j = std::max(0, i - n + 1); j <= i; j ++) {
                add(b[i + 1], 1ll * (i - j + 1) * a[i - j + 1] % mod * b[j] % mod);
            }
            mul(b[i + 1], k);
            for (int j = std::max(0, i - n); j < i; j ++) {
                dec(b[i + 1], 1ll * (j + 1) * b[j + 1] % mod * a[i - j] % mod);
            }
            mul(b[i + 1], v[i + 1]), mul(b[i + 1], inv);
        }
        return b;
    }

    // 多项式多点求值
    constexpr std::vector<int> eval(std::vector<int> x) const {
        if (size() == 0) {
            return std::vector<int>(x.size(), 0);
        }

        int n = std::max(x.size(), this->size());
        std::vector<poly> t(n * 4);
        std::vector<int> ans(x.size());
        x.resize(n);

        std::function<void(int, int, int)> build = [&] (int p, int l, int r) {
            if (l == r) {
                t[p] = poly{1, norm(-x[l])};
                return;
            }
            int mid = (l + r) >> 1;
            build(p * 2, l, mid), build(p * 2 + 1, mid + 1, r);
            t[p] = t[p * 2] * t[p * 2 + 1];
        };
        build(1, 0, n - 1);

        std::function<void(int, int, int, const poly &)> solve = [&] (int p, int l, int r, const poly &num) {
            if (l >= ans.size()) return;
            if (l == r) {
                ans[l] = num[0];
                return;
            }
            int mid = (l + r) >> 1;
            solve(p * 2, l, mid, num.mulT(t[p * 2 + 1]).modxk(mid - l + 1));
            solve(p * 2 + 1, mid + 1, r, num.mulT(t[p * 2]).modxk(r - mid));
        };
        solve(1, 0, n - 1, mulT(t[1].inv(n)));

        return ans;
    }

    // 多项式平移
    constexpr poly shift(int c) const {
        int n = size() - 1;

        std::vector<int> fact(n + 1), facv(n + 1);
        fact[0] = 1;
        for (int i = 1; i <= n; i ++) {
            fact[i] = 1ll * fact[i - 1] * i % mod;
        }
        facv[n] = qpow(fact[n], mod - 2, mod);
        for (int i = n - 1; i >= 0; i --) {
            facv[i] = 1ll * facv[i + 1] * (i + 1) % mod;
        }

        c = norm(c);
        poly a(n + 1), b(n + 1);
        for (int i = 0; i <= n; i ++) {
            a[i] = 1ll * (*this)[i] * fact[i] % mod;
        }
        for (int i = 0, w = 1; i <= n; i ++, mul(w, c)) {
            b[i] = 1ll * w * facv[i] % mod;
        }

        poly res = a.mulT(b);
        for (int i = 0; i <= n; i ++) {
            mul(res[i], facv[i]);
        }

        return res;
    }
};

// 多项式快速插值
constexpr poly lagrange(std::vector< std::pair<int, int> > seq) {
    int n = seq.size();
    std::vector<poly> t(n * 4);
    std::vector<int> x(n);
    for (int i = 0; i < n; i ++) {
        x[i] = seq[i].first;
    }

    std::function<void(int, int, int)> build = [&] (int p, int l, int r) {
        if (l == r) {
            t[p] = poly{norm(-x[l]), 1};
            return;
        }
        int mid = (l + r) >> 1;
        build(p * 2, l, mid), build(p * 2 + 1, mid + 1, r);
        t[p] = t[p * 2] * t[p * 2 + 1];
    };
    build(1, 0, n - 1);

    auto res = (t[1].deriv()).eval(x);
    std::function<poly(int, int, int)> solve = [&] (int p, int l, int r) {
        if (l == r) {
            int v = 1ll * seq[l].second * qpow(res[l], mod - 2, mod) % mod;
            return poly{v};
        }
        int mid = (l + r) >> 1;
        return solve(p * 2, l, mid) * t[p * 2 + 1] + solve(p * 2 + 1, mid + 1, r) * t[p * 2];
    };
    return solve(1, 0, n - 1);
}

// 连续点值平移
constexpr std::vector<int> shift_conti(std::vector<int> y, int c) {
    int n = y.size() - 1; // y 的下标从 0 开始

    c = norm(c);
    if (c == 0) return y;
    if (c <= n) {
        auto cur = std::vector<int>(y.begin() + c, y.end());
        auto net = shift_conti(y, n + 1);
        for (int i = 0; i < c; i ++) cur.push_back(net[i]);
        return cur;
    }

    std::vector<int> fact(n + 1), facv(n + 1);
    fact[0] = 1;
    for (int i = 1; i <= n; i ++) {
        fact[i] = 1ll * fact[i - 1] * i % mod;
    }
    facv[n] = qpow(fact[n], mod - 2, mod);
    for (int i = n - 1; i >= 0; i --) {
        facv[i] = 1ll * facv[i + 1] * (i + 1) % mod;
    }

    poly a(2 * n + 1), b(n + 1);
    for (int i = 0; i <= 2 * n; i ++) {
        a[i] = qpow(c - n + i, mod - 2, mod);
    }
    for (int i = 0; i <= n; i ++) {
        b[i] = 1ll * facv[i] * facv[n - i] % mod * y[i] % mod;
        if ((n - i) & 1) neg(b[i]);
    }

    poly res = a * b;

    int num = 1;
    for (int i = c - n; i <= c; i ++) mul(num, i);

    std::vector<int> ans(n + 1);
    for (int i = 0; i <= n; i ++) {
        ans[i] = 1ll * num * res[i + n] % mod;
        mul(num, c + i + 1), mul(num, qpow(c + i - n, mod - 2, mod));
    }
    return ans;
}

常系数齐次线性递推

常系数齐次线性递推:已知初值 \(a_0, \cdots, a_{k - 1}\) 与系数 \(c_1, \cdots, c_k\)(常数项为 \(0\)),对于一个 \(k\) 阶齐次线性递推数列 \(a_i = \sum_{j = 1}^k c_ja_{i - j}\),你需要求出第 \(n\) 项。

Fiduccia 算法

Fiduccia 算法:构造特征多项式 \(\Gamma(x) = x^k - \sum_{i = 1}^{k}c_ix^{k - i}\),记 \(A(x) = \sum_{i = 0}^{k - 1} a_ix^i\),则有

\[a_n = \left(x^n \bmod \Gamma(x) \right) \cdot A(x) \]

(其中 \(\cdot\) 代表点积)

于是应用多项式快速幂(倍增法)求出 \(x^n \bmod \Gamma(x)\),最后求出点积即可。

时间复杂度 \(\mathcal{O}(k \log k \log n)\)

Fiduccia 算法的简单解释:我们想要知道 \(a_n\) 具体可以被多少个 \(a_0, \cdots, a_{k - 1}\) 表示。相当于是初始有一个 \(a_n\),每次选出最高项 \(a_i\),根据公式 \(a_i = \sum_{j = 1}^kc_ja_{i - j}\)\(a_i\) 消成更低的项。

从生成函数的角度考虑,相当于是将 \(x^i\) 替换成 \(\sum_{j = 1}^kc_jx^{i - j}\),等价于减去 \(x^{i - k}\left( x^k - \sum_{j = 1}^kc_jx^{k - j} \right)\)整个过程等价于 \(x^n\)\(x^{k} - \sum_{j = 1}^k c_jx^{k - j}\) 取模

0x31 常系数齐次线性递推(Fiduccia).cpp

// 常系数齐次线性递推(Fiduccia)
int linearRecurrence(std::vector<int> c, std::vector<int> y, int n) {
    int k = y.size(); // k 阶
    if (n < k) {
        return y[n];
    }

    poly g(k + 1);
    g[k] = 1;
    for (int i = 1; i <= k; i ++) {
        g[k - i] = norm(-c[i]);
    }

    poly res{1}, a{0, 1};
    for (int b = n; b; b >>= 1) {
        if (b & 1) res = divmod(res * a, g).second;
        a = divmod(a * a, g).second;
    }

    int ans = 0;
    for (int i = 0; i < k; i ++) {
        add(ans, 1ll * y[i] * res[i] % mod);
    }
    return ans;
}

Bostan-Mori 算法

Bostan-Mori 算法(求解分式远处系数)该算法用于求解分式 \(\frac{P(x)}{Q(x)}\) 的第 \(n\)\([x^n]\frac{P(x)}{Q(x)}\)

考虑

\[\frac{P(x)}{Q(x)} = \frac{P(x)Q(-x)}{Q(x)Q(-x)} \]

\(V(x) = Q(x)Q(-x)\),注意到 \(V(x) = V(-x)\),则 \(V(x)\) 仅有偶次项有值。于是我们有

\[\frac{P(x)}{Q(x)} = \frac{C_{\mathrm{even}}(x^2)}{U(x^2)} + \frac{xC_{\mathrm{odd}}(x^2)}{U(x^2)}, \quad U(x^2) = Q(x)Q(-x) \]

由于左边只影响奇次项,右边只影响偶次项(因为 \(\frac{1}{U(x^2)} = \frac{1}{Q(x)Q(-x)}\) 也是偶函数,仅有偶次项有值),于是我们只需要根据 \(n\) 的奇偶递归到其中一边即可。

\(n\) 为偶数时

\[[x^n]\frac{P(x)}{Q(x)} = [x^\frac{n}{2}]\frac{C_{\mathrm{even}}(x)}{U(x)} \]

\(n\) 为奇数时

\[[x^n]\frac{P(x)}{Q(x)} = [x^{\frac{n - 1}{2}}]\frac{C_{\mathrm{odd}}(x)}{U(x)} \]

递归到 \(n = 0\) 时,有 \([x^0]\frac{f(x)}{g(x)} = \frac{f_0}{g_0}\)

常系数齐次线性递推与分式第 \(n\) 项的关系:对于数列 \(\{a_i\}\),我们想要构造多项式 \(P(x), Q(x)\) 使得 \([x^n]\frac{P(x)}{Q(x)} = a_n\)

\(Q(x) = \Gamma^R(x) = 1 - \sum_{i = 1}^k c_ix^i\) 以及 \(P(x) = ((\sum_{i = 0}^{k - 1} a_ix^i) \cdot Q(x)) \bmod x^k\),即可满足 \([x^n]\frac{P(x)}{Q(x)} = a_n\)

下面是检验,设 \(\frac{P(x)}{Q(x)} = \sum_{i = 0}^{\infty} h_ix^i\),根据多项式除法的定义:

  • \(i = 0\) 时,有 \(h_0 = \frac{p_0}{q_0} = a_0\)
  • \(1 \leq i < k\) 时,根据多项式 \(P, Q\) 的定义,显然有 \(h_i = a_i\)
  • \(i \geq k\) 时,有 \(h_i = -\frac{1}{q_0}\sum_{j = 1}^k q_j h_{i - j}\),化简得 \(h_i = \sum_{j = 1}^k c_jh_{i - j}\),发现这是 \(\{a_i\}\) 的递推式,归纳得 \(h_i = a_i\)

综上 \(h_i = a_i\),原命题成立

得出多项式 \(P, Q\) 后,套用上述算法求解 \([x^n]\frac{P(x)}{Q(x)}\) 即可。

时间复杂度 \(\mathcal{O}(k \log k \log n)\),但常数相较于 Fiduccia 算法更小。

0x31 常系数齐次线性递推(Bostan-Mori).cpp

// 常系数齐次线性递推(Bostan-Mori)
int linearRecurrence(std::vector<int> c, std::vector<int> y, int n) {
    int k = y.size();
    if (n < k) {
        return y[n];
    }

    // 求多项式 p/q 的第 n 项系数
    std::function<int(poly, poly, int)> solve = [&] (poly p, poly q, int n) {
        for (; n; n >>= 1) {
            poly nq = q;
            for (int i = 1; i < nq.size(); i += 2) {
                neg(nq[i]);
            }

            p *= nq, q *= nq;

            int i = n & 1;
            while (i < p.size()) {
                p[i / 2] = p[i], i += 2;
            }
            p.resize(i / 2);

            int j = 0;
            while (j < q.size()) {
                q[j / 2] = q[j], j += 2;
            }
            q.resize(j / 2);
        }

        return 1ll * p[0] * qpow(q[0], mod - 2, mod) % mod;
    };

    poly q = poly{1} - poly(c);
    poly p = (poly(y) * q).modxk(k);
    return solve(p, q, n);
}

整式递推

待填坑。

拉格朗日反演

待填坑。

0x32 数学知识 集合幂级数

FWT(快速沃尔什变换)

位运算卷积:已知序列 \(\{a_i\}, \{b_i\}\),求序列 \(\{c_i\}\) 满足 \(c_k = \sum_{i \oplus j = k} a_ib_j\)。其中 \(\oplus\) 表示一种位运算(or / and / xor)。

DWT

FFT 多项式乘法的核心思想:\(A, B\) 的系数转 \(A, B\) 的点值,\(A, B\) 的点值点积转 \(C\) 的点值,\(C\) 的点值转 \(C\) 的系数。

我们想要 DWT 与 IDWT 有着类似的功能。设 \(\mathrm{dwt}(a)\) 表示幂级数 \(a\) 经过 DWT 变换后得到的幂级数(点值),我们希望

\[a \times b = c \iff \mathrm{dwt}(a) \cdot \mathrm{dwt}(b) = \mathrm{dwt}(c) \]

由于 DFT 是线性变换,我们希望 DWT 也是线性变换。

记序列长度 \(n = 2^m\),设 \(\mathrm{dwt}(a)_i = \sum_{j = 0}^{m - 1} c(i, j)a_j\),其中变换系数 \(c(i, j)\) 表示变换前下标 \(j\) 对变换后下标 \(i\) 的贡献。

\(\mathrm{dwt}(a) \cdot \mathrm{dwt}(b) = \mathrm{dwt}(c)\),得

\[\mathrm{dwt}(c)_i = \mathrm{dwt}(a)_i \cdot \mathrm{dwt}(b)_i \\ \left( \sum_{p = 0}^{n - 1}c(i, p)c_p \right) = \left( \sum_{j = 0}^{n - 1}c(i, j)a_j \right) \cdot \left( \sum_{k = 0}^{n - 1}c(i, k)b_k \right) \\ \sum_{p = 0}^{n - 1}c(i, p)c_p = \sum_{j = 0}^{n - 1}\sum_{k = 0}^{n - 1}c(i, j)c(i, k)a_jb_k \]

\(a \times b = c\),得

\[\begin{aligned} \sum_{p = 0}^{n - 1} c(i, p)c_p & = \sum_{p = 0}^{n - 1} c(i, p) \sum_{t_1 \oplus t_2 = p} a_{t_1}b_{t_2} \\ & = \sum_{p = 0}^{n - 1} \sum_{t_1 \oplus t_2 = p} c(i, t_1 \oplus t_2) a_{t_1}b_{t_2} \\ & = \sum_{t_1 = 0}^{n - 1} \sum_{t_2 = 0}^{n - 1} c(i, t_1 \oplus t_2) a_{t_1}b_{t_2} \end{aligned} \]

对比上下两段的最后一个式子,发现只需要满足 \(c(i, j)c(i, k) = c(i, j \oplus k)\) 即可

另外,由于位运算可以按位考虑的特性,我们可以这样构造 \(c(i, j)\)先得到单个位的转移系数 \(c([0, 1], [0, 1])\),对于所有的 \(c(i, j)\),我们将 \(i, j\) 二进制分解,将对应位的转移系数相乘

\[c(i, j) = c(i_0, j_0)c(i_1, j_1)\cdots c(i_{m - 1}, j_{m - 1}) \]

其中 \(i_x, j_x\) 分别表示 \(i, j\) 的二进制下第 \(x\) 位。

现在考虑优化计算 DWT。考虑按最高位拆分,记 \(i', j'\) 表示 \(i, j\) 去掉最高位的值,记 \(i_h\) 表示 \(i\) 的最高位

\[\begin{aligned} \mathrm{dwt}(a)_i & = \sum_{j = 0}^{n - 1}c(i, j)a_j \\ & = \sum_{j = 0}^{n / 2 - 1}c(i, j)a_j + \sum_{j = n / 2}^{n - 1}c(i, j)a_j \\ & = c(i_h, 0) \left( \sum_{j = 0}^{n / 2 - 1}c(i', j')a_j \right) + c(i_h, 1)\left( \sum_{j = n / 2}^{n - 1}c(i', j')a_j \right) \end{aligned} \]

发现括号里的式子是一个子问题。记 \(a_0\) 表示幂级数 \(a\) 最高位为 \(0\) 的部分,\(a_1\) 表示幂级数 \(a\) 最高位为 \(1\) 的部分。

  • 对于 \(k < n / 2\),代入 \(i = k\)

\[\mathrm{dwt}(a)_k = c(0, 0) \cdot \mathrm{dwt}(a_0)_k + c(0, 1) \cdot \mathrm{dwt}(a_1)_k \]

  • 对于 \(k < n / 2\),代入 \(i = k + n / 2\)

\[\mathrm{dwt}(a)_{k + n / 2} = c(1, 0) \cdot \mathrm{dwt}(a_0)_k + c(1, 1) \cdot \mathrm{dwt}(a_1)_k \]

如果我们知道 \(\mathrm{dwt}(a_0)_i, \mathrm{dwt}(a_1)_i\)\(i = 0, 1, \cdots, n / 2 - 1\) 处的值,就可以 \(\mathcal{O}(n)\) 求出 \(\mathrm{dwt}(a)_i\)\(i = 0, 1, \cdots, n - 1\) 处的值。

这是一个分治的过程,不断分治直到仅剩一个项即可。

时间复杂度 \(\mathcal{O}(m2^m)\)

IDWT

在 DWT 中,我们只需要

\[c = \begin{bmatrix} c(0, 0) & c(0, 1) \\ c(1, 0) & c(1, 1) \end{bmatrix} \]

这四个数即可完成变换,记该矩阵为位矩阵 \(c\)

如果我们要进行 IDWT,则需要位矩阵 \(c\) 的逆矩阵 \(c^{-1}\)。类似地,有

\[a_i = \sum_{j = 0}^{n - 1} c^{-1}(i, j)\mathrm{dwt}(a)_j \]

套用 DWT 的模板即可。

在构造矩阵 \(c\) 的时候,我们需要小心一点,得确保矩阵 \(c\) 存在逆矩阵 \(c^{-1}\)

位运算卷积

or 卷积

or 运算的位矩阵

\[c = \begin{bmatrix} 1 & 0 \\ 1 & 1 \end{bmatrix} \]

逆矩阵

\[c^{-1} = \begin{bmatrix} 1 & 0 \\ -1 & 1 \end{bmatrix} \]

or 卷积的正变换等价于子集求和,因为 \(c(i, j) = [i \operatorname{and} j = j]\)

and 卷积

and 运算的位矩阵

\[c = \begin{bmatrix} 1 & 1 \\ 0 & 1 \end{bmatrix} \]

逆矩阵

\[c^{-1} = \begin{bmatrix} 1 & -1 \\ 0 & 1 \end{bmatrix} \]

and 卷积的正变换等价于超集求和,因为 \(c(i, j) = [i \operatorname{or} j = j]\)

xor 卷积

xor 运算的位矩阵

\[c = \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} \]

逆矩阵

\[c^{-1} = \frac{1}{2}\begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} \]

xor 卷积的正变换,其变换系数 \(c(i, j) = (-1)^{\mathrm{popcount}(i \operatorname{and} j)}\)

xor 卷积的 IDWT 相较于 DWT,每一位的变换系数需要乘以 \(\frac{1}{2}\),一共有 \(m\) 位,总共的影响为 \(\frac{1}{2^m} = \frac{1}{n}\)。所以 xor 卷积的 IDWT 只需套用 DWT 的模板,最后乘以 \(\frac{1}{n}\) 即可。

0x32 FWT.cpp

/* fwt-Or */
namespace bitOr {
    void dwt(std::vector<int> &a, int opt) {
        int n = a.size();
        for (int k = 1; k < n; k <<= 1) {
            for (int i = 0; i < n; i += (k << 1)) {
                for (int j = 0; j < k; j ++) {
                    opt > 0 ? add(a[i + j + k], a[i + j]) : dec(a[i + j + k], a[i + j]);
                }
            }
        }
    }
    std::vector<int> conv(std::vector<int> a, std::vector<int> b) {
        dwt(a, +1), dwt(b, +1);
        for (int i = 0; i < a.size(); i ++) {
            mul(a[i], b[i]);
        }
        dwt(a, -1);
        return a;
    }
}

/* fwt-And */
namespace bitAnd {
    void dwt(std::vector<int> &a, int opt) {
        int n = a.size();
        for (int k = 1; k < n; k <<= 1) {
            for (int i = 0; i < n; i += (k << 1)) {
                for (int j = 0; j < k; j ++) {
                    opt > 0 ? add(a[i + j], a[i + j + k]) : dec(a[i + j], a[i + j + k]);
                }
            }
        }
    }
    std::vector<int> conv(std::vector<int> a, std::vector<int> b) {
        dwt(a, +1), dwt(b, +1);
        for (int i = 0; i < a.size(); i ++) {
            mul(a[i], b[i]);
        }
        dwt(a, -1);
        return a;
    }
}

/* fwt-Xor */
namespace bitXor {
    void dwt(std::vector<int> &a, int opt) {
        int n = a.size();
        for (int k = 1; k < n; k <<= 1) {
            for (int i = 0; i < n; i += (k << 1)) {
                for (int j = 0; j < k; j ++) {
                    int u = a[i + j], v = a[i + j + k];
                    add(a[i + j] = u, v), dec(a[i + j + k] = u, v);
                }
            }
        }
        if (opt < 0) {
            int inv = qpow(n, mod - 2, mod);
            for (int i = 0; i < n; i ++) {
                mul(a[i], inv);
            }
        }
    }
    std::vector<int> conv(std::vector<int> a, std::vector<int> b) {
        dwt(a, +1), dwt(b, +1);
        for (int i = 0; i < a.size(); i ++) {
            mul(a[i], b[i]);
        }
        dwt(a, -1);
        return a;
    }
}

子集 dp

子集 dpSum Over Subsets DP,SOS DP):已知序列 \(\{a_i\}\),求序列 \(\{b_i\}\) 满足

\[b_i = \sum_{j \subseteq i} a_j \]

将一个二进制状态视作一个 \(m\) 维的坐标,第 \(i\) 维的取值等于二进制下第 \(i\) 位的值。

则二进制下的子集和,相当于是要求每个维度都小于等于当前坐标的所有坐标的权值和,运用高维前缀和即可。

时间复杂度 \(\mathcal{O}(m2^m)\)

0x32 子集 dp.cpp

// 注意:要先枚举维度 i,再枚举状态 S
for (int i = 0; i < m; i ++) {
    for (int S = 0; S < (1 << m); S ++) {
        if (S >> i & 1) {
            f[S] += f[S ^ (1 << i)];
        }
    }
}

子集卷积

子集卷积:已知序列 \(\{a_i\}, \{b_i\}\),求序列 \(\{c_i\}\) 满足

\[c_k = \sum_{i \operatorname{or} j = k, i \operatorname{and} j = \varnothing} a_ib_j \]

注意到 \(i \operatorname{and} j = \varnothing\) 时,必有 \(|i| + |j| = |i \operatorname{or} j|\)。于是引入序列 \(f\) 的占位多项式 \(F\)

\[F_{p, i} = \begin{cases} f_i & |i| = p\\ 0 & |i| \neq p \end{cases} \]

将占位多项式代入到子集卷积的式子中

\[C_{s, k} = \sum_{p + q = s} \sum_{i \operatorname{or} j = k}A_{p, i}B_{q, j} \]

从 or 卷积的角度来看,有

\[C_s = \sum_{p + q = s}A_p \operatorname{or} B_q \]

于是将 \(A_p\)\(B_q\) 进行 DWT,求出所有的 \(C_s\) 后再进行 IDWT 即可。

时空复杂度 \(\mathcal{O}(m^22^m)\)

0x32 子集卷积.cpp

/* 子集卷积 */
std::vector<int> subset_conv(std::vector<int> a, std::vector<int> b) {
    int n = a.size(), m = std::__lg(n);

    std::vector< std::vector<int> >
        A(m + 1, std::vector<int>(n, 0)),
        B(m + 1, std::vector<int>(n, 0)),
        C(m + 1, std::vector<int>(n, 0));

    for (int i = 0; i < n; i ++) {
        A[__builtin_popcount(i)][i] = a[i];
        B[__builtin_popcount(i)][i] = b[i];
    }

    for (int i = 0; i <= m; i ++) {
        bitOr::dwt(A[i], +1), bitOr::dwt(B[i], +1);
    }
    for (int i = 0; i <= m; i ++) {
        for (int j = 0; j <= i; j ++) {
            for (int k = 0; k < n; k ++) {
                add(C[i][k], 1ll * A[j][k] * B[i - j][k] % mod);
            }
        }
    }
    for (int i = 0; i <= m; i ++) {
        bitOr::dwt(C[i], -1);
    }

    std::vector<int> c(n);
    for (int i = 0; i < n; i ++) {
        c[i] = C[__builtin_popcount(i)][i];
    }
    return c;
}

0x33 数学知识 组合数学

排列组合

排列数\(A_{n}^m\),从 \(n\) 个有标号元素中,取出 \(m\) 个元素排成一列的方案数(需要考虑取数顺序)。

\[A_n^m = \frac{n!}{(n - m)!} = n^{\underline{m}} \]

排列数\(C_n^m\)\(\binom{n}{m}\),从 \(n\) 个有标号元素中,取出 \(m\) 个元素组成集合的方案数(不需要考虑取数顺序)。

\[\binom{n}{m} = \begin{cases} \frac{n^{\underline{m}}}{m!} & m \in \Z, m \geq 0 \\ 0 & m \in \Z, m < 0 \end{cases} \]

组合数常见恒等式

阶乘展开式\(n, m \in \Z\)\(n \geq m \geq 0\)

\[\binom{n}{m} = \frac{n!}{m!(n - m)!} \]

归纳恒等式\(m \in \Z\)

\[\binom{n}{m} = \binom{n - 1}{m} + \binom{n - 1}{m - 1} \]

对称恒等式\(n, m \in \Z\)\(n \geq 0\)

\[\binom{n}{m} = \binom{n}{n - m} \]

吸收恒等式\(m \in \Z\)\(m \neq 0\)

\[\begin{aligned} \binom{n}{m} & = \frac{n}{m}\binom{n - 1}{m - 1} \\ \binom{n}{m} & = \frac{n}{n - m}\binom{n - 1}{m} \\ \end{aligned} \]

上指标反转\(m \in \Z\)

\[\binom{n}{m} = (-1)^m \binom{m - n - 1}{m} \]

三项式版恒等式\(m, k \in \Z\)

\[\binom{n}{m}\binom{m}{k} = \binom{n}{k}\binom{n - k}{m - k} \]

二项式定理\(n \in \Z\)

\[(x + y)^n = \sum_{i = 0}^n \binom{n}{i}x^{i}y^{n - i} \]

一些常见特例:

  • \(x = 1\)\(y = a\)

\[\sum_{i = 0}^{n} \binom{n}{i}a^i = (1 + a)^n \]

  • \(x = 1\)\(y = -1\)

\[\sum_{i = 0}^n (-1)^i \binom{n}{i} = [n = 0] \]

广义二项式定理\(r \in \R\)

\[(x + y)^r = \sum_{k = 0}^{\infty} \frac{r^{\underline{k}}}{k!} x^{k}y^{r - k} \]

平行求和法\(m \in \Z\)

\[\sum_{0 \leq k \leq m} \binom{n + k}{k} = \binom{n + m + 1}{m} \]

上指标求和法\(m \in \Z\)\(n \geq 0\)

\[\sum_{0 \leq k \leq n}\binom{k}{m} = \binom{n + 1}{m + 1} \]

范德蒙德卷积\(m \in \Z\)

\[\sum_k \binom{s}{k}\binom{t}{m - k} = \binom{s + t}{m} \]

斜对角线求和法\(n \in \Z\)

\[\sum_{i = 0}^n \binom{n - i}{i} = \mathrm{Fib}_{n + 1} \]

等价于长度为 \(n - 1\) 的没有相邻 \(1\) 的二进制串个数。

组合数带权和\(n \in \Z\)

\[\sum_{i = 0}^n i\binom{n}{i} = n2^{n - 1} \]

组合数带权平方和\(n \in \Z\)

\[\sum_{i = 0}^n i^2\binom{n}{i} = n(n + 1)2^{n - 2} \]

插板法

问题 1:将 \(n\) 个完全相同的元素分成 \(k\) 组,保证每组至少有一个元素。等价于 \(\sum_{i = 1}^k x_i = n\)\(\forall x_i \geq 1\) 的整数解个数。

相当于将 \(k - 1\) 块板,插入 \(n - 1\) 个间隙中。

\[\binom{n - 1}{k - 1} \]

问题 2:将 \(n\) 个完全相同的元素分成 \(k\) 组,允许组内元素为空。等价于 \(\sum_{i = 1}^k x_i = n\)\(\forall x_i \geq 0\) 的整数解个数。

\(k\) 组分别补一个元素,转化为问题 1。

\[\binom{n + k - 1}{k - 1} \]

问题 3:给 \(n\) 个完全相同的元素分成 \(k\) 组,保证第 \(i\) 组至少分到 \(a_i\) 个元素。

给第 \(i\) 组提前安排 \(a_i\) 个元素,转化为问题 2。

\[\binom{n - \sum a_i + k - 1}{k - 1} \]

Lucas 定理

Lucas 定理:对于质数 \(p\)

\[\binom{n}{m} \equiv \binom{\lfloor n / p \rfloor}{\lfloor m / p \rfloor}\binom{n \bmod p}{m \bmod p} \pmod p \]

相当于是将 \(n, m\) 转化为 \(p\) 进制,将每一位上对应的组合数 \(\binom{n_i}{m_i}\) 相乘后对 \(p\) 取模。

组合数 mod 2:当 \(p = 2\) 时,\(\binom{n}{m} \equiv \binom{\lfloor n / 2 \rfloor}{\lfloor m / 2 \rfloor}\binom{n \bmod 2}{m \bmod 2} \pmod 2\)

\(\binom{n}{m} \bmod 2 = 1\),当且仅当在二进制下 \(m\)\(n\) 的子集

组合数常见求法

求法 1:递推

适用情况:对模数不做要求,可以承受平方时间的预处理(\(n \leq 5000\))。

时间复杂度:预处理 \(\mathcal{O}(n^2)\),查询 \(\mathcal{O}(1)\)

初值 \(\binom{n}{0} = \binom{n}{n} = 1\),递推式 \(\binom{n}{m} = \binom{n - 1}{m} + \binom{n - 1}{m - 1}\)

for (int i = 0; i <= n; i ++) {
    C[i][0] = C[i][i] = 1;
    for (int j = 1; j < i; j ++) {
        C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
    }
}

求法 2:预处理阶乘与阶乘逆元

适用情况:要求模数为质数,可以承受线性时间的预处理(\(n \leq 10^6\))。

时间复杂度:预处理 \(\mathcal{O}(n)\),查询 \(\mathcal{O}(1)\)

将组合数使用阶乘形式表达

\[\binom{n}{m} = \frac{n!}{m!(n - m)!} \]

考虑预处理出阶乘与阶乘逆元。

\(\mathrm{fact}_i = i!\),则有初值 \(\mathrm{fact}_0 = 1\),递推式 \(\mathrm{fact}_i =\mathrm{fact}_{i - 1} \times i\)

\(\mathrm{facv} = \frac{1}{i!}\),则有初值 \(\mathrm{facv}_n = \mathrm{fact}_n^{-1}\),递推式 \(\mathrm{facv}_i = \mathrm{facv}_{i + 1} \times(i + 1)\)

于是先正着递推一遍得到 \(\mathrm{fact}_{0, \cdots, n}\),然后通过一次求逆元得到 \(\mathrm{facv}_n\),最后再反着递推一遍得到 \(\mathrm{facv}_{n, \cdots, 0}\)

0x33 组合数(预处理阶乘与阶乘逆元).cpp

/* 组合数(预处理阶乘与阶乘逆元) */
struct BinomCoef {
    std::vector<int> fact, facv;

    BinomCoef() {}
    BinomCoef(int n) {
        init(n);
    }

    void init(const int &n) {
        fact.resize(n + 1), facv.resize(n + 1);

        fact[0] = 1;
        for (int i = 1; i <= n; i ++) {
            fact[i] = 1ll * fact[i - 1] * i % mod;
        }
        
        facv[n] = qpow(fact[n], mod - 2, mod);
        for (int i = n - 1; i >= 0; i --) {
            facv[i] = 1ll * facv[i + 1] * (i + 1) % mod;
        }
    }

    int binom(int n, int m) {
        if (n < m || m < 0) {
            return 0;
        }
        return 1ll * facv[m] * facv[n - m] % mod * fact[n] % mod;
    }
} bc;

求法 3:暴力统计

适用范围:要求模数为质数,\(m\) 的范围可以承受线性枚举。

时间复杂度:无预处理,查询 \(\mathcal{O}(m)\)

将组合数表达为

\[\binom{n}{m} = \frac{n^{\underline{m}}}{m!} \]

注意到分子分母均只有 \(m\) 项,当 \(n\) 很大但 \(m\) 很小时,可以暴力枚举。

具体实现时,可以先分别求出分子与分母,然后再对分母求一次逆元。

求法 4:Lucas 定理

适用范围:要求模数为质数,模数以内的组合数可以预处理。

时间复杂度:预处理 \(\mathcal{O}(p)\),查询 \(\mathcal{O}(\log_{p} n)\)

由 Lucas 定理,得

\[\binom{n}{m} \equiv \binom{\lfloor n / p \rfloor}{\lfloor m / p \rfloor}\binom{n \bmod p}{m \bmod p} \pmod p \]

先预处理出模数以内的组合数。然后不断地使用 Lucas 定理,直到 \(n, m\) 递降至模数以内。

求法 5:exLucas 定理

待填坑。

求法 6:分段打表

适用范围:要求模数为固定质数,代码长度限制不小,可以承受段长 \(\mathcal{O}(W)\) 时间的查询。

(正常数据范围 \(n \leq 10^9\),配合 Lucas 定理可以做到 \(\mathrm{mod} \leq 10^9\)\(n \leq 10^{18}\)

时间复杂度:打表预处理,查询 \(\mathcal{O}(W)\)

仍然将组合数使用阶乘形式表达,每次查询相当于要得到三个阶乘的值。

考虑分段打表。记段长为 \(W\)(取 \(10^5 \sim 10^6\) 相对合适),先通过打表预处理出所有 \(W\) 的倍数的阶乘。

每次查询 \(n!\) 的时候,就找到最近的 \(W\) 的倍数,根据该答案继续向上乘。

邪修:快速阶乘算法

适用范围:要求模数为不固定质数(\(n \leq 10^9\))。

时间复杂度:无预处理,查询 \(\mathcal{O}(\sqrt{n} \log n)\)

直接使用快速阶乘算法!得到三个阶乘的值。

求法 7:阶乘分解质因数

适用范围 1:对模数不做要求,可以承受线性时间的查询(\(n \leq 10^6\))。

时间复杂度:预处理 \(\mathcal{O}(n)\),查询 \(\mathcal{O}(n)\)

适用范围 2:高精度组合数(\(n \leq 5000\))。

仍然将组合数适用阶乘形式表达,我们考虑 \(1 \sim n\) 中的每个质数出现的次数。

首先先将 \(1 \sim n\) 中所有的质数提前筛出来,大约有 \(\frac{n}{\ln n}\) 个质数。对于质数 \(p\),在阶乘 \(n!\)\(p\) 的出现次数为

\[\sum_{i = 1}^{\lfloor \log_p n \rfloor} \left\lfloor \frac{n}{p^i} \right\rfloor \]

于是将三个阶乘都进行质因数分解,得出每个质数的出现次数,然后将所有质数的贡献相乘即可。

时间复杂度 \(\mathcal{O}(n)\)(高精度组合数的复杂度另算)。

斯特林数

第一类斯特林数

第二类斯特林数

卡特兰数

卡特兰数\(\mathrm{Cat}_n\),从原点 \((0, 0)\) 出发,每次只能向 \(x\) 轴或 \(y\) 轴正方向走,且不能越过直线 \(y = x\),到达 \((n, n)\) 的方案数。

下标从 \(0\) 开始的若干项:

\[1, 1, 2, 5, 14, 42, 132, 429, 1430, \cdots \]

卡特兰数通项公式

\[\begin{aligned} \mathrm{Cat}_n & = \binom{2n}{n} - \binom{2n}{n - 1} \\ & = \frac{\binom{2n}{n}}{n + 1} \end{aligned} \]

简单推导:越过直线 \(y = x\),相当于是触碰直线 \(y = x + 1\)

将触碰 \(y = x + 1\) 的第一个点以后的路径,关于 \(y = x + 1\) 对称,就可以得到一条从 \((0, 0)\)\((n - 1, n + 1)\) 的路径。

于是「\((0, 0) \to (n, n)\) 且越过 \(y = x\) 的路径」与「\((0, 0) \to (n - 1, n + 1)\) 的路径」是一一对应的,方案数均为 \(\binom{2n}{n - 1}\)

正难则反,不越过直线 \(y = x\) 的方案数,即为总方案数 \(\binom{2n}{n}\) 减去越过直线的方案数 \(\binom{2n}{n - 1}\)

卡特兰数递推公式

\[\mathrm{Cat}_0 = 1 \\ \mathrm{Cat}_n = \sum_{i = 0}^{n - 1}\mathrm{Cat}_i\mathrm{Cat}_{n - i - 1} \]

简单推导:枚举第一次触碰直线 \(y = x\) 的位置 \((i, i)\),其中 \(i \geq 1\)

由于在 \((i, i)\) 之前不能触碰直线 \(y = x\),相当于是 \((1, 0) \to (i, i - 1)\) 不越过直线 \(y = x - 1\),于是这部分的方案数为 \(\mathrm{Cat}_{i - 1}\mathrm{Cat}_{n - i}\)

卡特兰数递推公式 2

\[\mathrm{Cat}_0 = 1\\ \mathrm{Cat}_n = \frac{4n - 2}{n + 1}\mathrm{Cat}_{n - 1} \]

常见卡特兰数问题

  • 从原点 \((0, 0)\) 出发,每次只能向 \(x\) 轴或 \(y\) 轴正方向走,且不能越过直线 \(y = x\),到达 \((n, n)\) 的方案数 \(\mathrm{Cat}_n\)
  • \(1, 2, \cdots, n\) 作为 \(n\) 个元素的进栈序列时,出栈序列的方案数 \(\mathrm{Cat}_n\)
  • \(n\) 对括号组成的合法括号序列的方案数 \(\mathrm{Cat}_n\)
  • \(n\) 个节点构造无标号、左右儿子区分的二叉树个数 \(\mathrm{Cat}_n\)
  • \(n\) 个节点构造无标号、儿子区分的有根树个数 \(\mathrm{Cat}_{n - 1}\)
  • 以圆周上的 \(2n\) 个点为端点,连互不相交的 \(n\) 条弦的方案数 \(\mathrm{Cat}_n\)
  • 将凸 \(n\) 边形用其中 \(n - 3\) 条对角线分割为互不重叠的 \(n - 2\) 个三角形的方案数 \(\mathrm{Cat}_{n - 2}\)
  • \(1 \sim 2n\) 中的每个数,不重不漏地填入一个 \(2 \times n\) 的矩阵,每个元素大于其左方、上方元素的方案数 \(\mathrm{Cat}_n\)

贝尔数

贝尔数\(B_n\),表示将 \(n\) 个两两不同的元素,划分成若干个互不区分的非空子集的方案数。

下标从 \(0\) 开始的若干项:

\[1, 1, 2, 5, 15, 52, 203, 877, 4140, \cdots \]

贝尔数递推公式

\[B_0 = 1 \\ B_n = \sum_{i = 0}^{n - 1} \binom{n - 1}{i}B_i \]

简单推导:枚举与元素 \(n\) 在同一集合的元素个数 \(i\),共有 \(\binom{n - 1}{i}\) 种选法,剩下的元素继续划分,于是这部分的方案数为 \(\binom{n - 1}{i}B_{n - 1 - i}\)

贝尔数级别的暴搜:当我们需要将 \(n\) 个两两不同的元素,划分成若干个互不区分的非空子集时。\(n\) 较小可以直接暴搜,复杂度为 \(B_n\) 级别

\(s_1, \cdots, s_{t}\) 表示当前时刻已经存在的集合,每次加入一个新元素 \(x\) 时有两种转移

  • 选择一个已经存在的集合 \(s_i\),将 \(x\) 加入 \(s_i\)
  • 新开一个集合 \(s_{t + 1}\),将 \(x\) 加入 \(s_{t + 1}\)

贝尔数与第二类斯特林数的关系

\[B_n = \sum_{i = 0}^n {n \brace i} \]

因为第二类斯特林数 \({n \brace i}\) 表示将 \(n\) 个两两不同的元素,划分成 \(i\) 个互不区分的非空子集的方案数。

斐波那契数列

斐波那契数列\(\mathrm{Fib}_n\)

斐波那契数列递推公式

\[\mathrm{Fib}_0 = 0, \mathrm{Fib}_1 = 1\\ \mathrm{Fib}_n = \mathrm{Fib}_{n - 1} + \mathrm{Fib}_{n - 2} \]

斐波那契数列递推公式(矩阵形式)

\[\begin{bmatrix} \mathrm{Fib}_n & \mathrm{Fib}_{n - 1} \end{bmatrix} = \begin{bmatrix} \mathrm{Fib}_{n - 1} & \mathrm{Fib}_{n - 2} \end{bmatrix} \times \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \]

\[\begin{bmatrix} \mathrm{Fib}_n & \mathrm{Fib}_{n - 1} \end{bmatrix} = \begin{bmatrix} 1 & 0 \end{bmatrix} \times \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n - 1} \]

该转移矩阵可以求逆,从而实现逆向转移。

斐波那契数列通项公式

\[\mathrm{Fib}_n = \frac{\left( \frac{1 + \sqrt{5}}{2} \right)^n - \left( \frac{1 - \sqrt{5}}{2} \right)^n}{\sqrt{5}} \]

\(5\) 是模 \(p\) 意义下的二次剩余时(一个常见模数是 \(10^9 + 9\)),可以直接暴力预先找出 \(\sqrt{5}\),然后使用通项公式计算。

否则,考虑使用扩展数域进行计算,定义扩展数域 \(\mathbb{F}_p(\sqrt{5})\)

\[\{ a + b\sqrt{5} \mid a, b \in \mathbb{F}_p \} \]

可以验证,\(\mathbb{F}_p(\sqrt{5})\) 关于加、减、乘、除封闭。记 \((a, b)\) 表示 \(a + b\sqrt{5}\),则有

\[\begin{aligned} (a, b) \pm (c, d) & = (a\pm c, b\pm d) \\ (a, b) \times (c, d) & = (ac + 5bd, ad + bc) \\ (a, b) \div (c, d) & = \left( \frac{ac - 5bd}{c^2 - 5d^2} , \frac{bc - ad}{c^2 - 5d^2} \right) \end{aligned} \]

广义斐波那契数列:将递推公式反过来写,有 \(\mathrm{Fib}_n = \mathrm{Fib}_{n + 2} - \mathrm{Fib}_{n + 1}\)

考虑将斐波那契数列扩展到负数域,经过推导可得

\[\mathrm{Fib}_{-n} = (-1)^{n - 1}\mathrm{Fib}_n \]

类斐波那契数列:已知数列 \(G_0 = a, G_1 = b\)\(G_n = G_{n - 1} + G_{n - 2}\),则有

\[G_n = \mathrm{Fib}_{n - 1} \times a + \mathrm{Fib}_n \times b \]

类斐波那契数列相加,仍然是类斐波那契数列。只需将相应的 \(a, b\) 相加即可。

斐波那契数列常见恒等式

斐波那契数列求和

\[\sum_{i = 1}^n \mathrm{Fib}_i = \mathrm{Fib}_{n + 2} - 1 \]

斐波那契数列平方和

\[\sum_{i = 1}^n \mathrm{Fib}_i^2 = \mathrm{Fib}_n\mathrm{Fib}_{n + 1} \]

卡西尼性质

\[\mathrm{Fib}_{n - 1}\mathrm{Fib}_{n + 1} - \mathrm{Fib}_n^2 = (-1)^n \]

简单推导:\(\mathrm{Fib}_{n - 1}\mathrm{Fib}_{n + 1} - \mathrm{Fib}_n^2 = \det\begin{pmatrix} \mathrm{Fib}_{n + 1} & \mathrm{Fib}_n \\\mathrm{Fib}_n & \mathrm{Fib}_{n - 1} \end{pmatrix} = \det\begin{pmatrix} 1 & 1 \\1 & 0 \end{pmatrix}^n = (-1)^n\)

附加性质

\[\mathrm{Fib}_{n + m} = \mathrm{Fib}_{n + 1}\mathrm{Fib}_m + \mathrm{Fib}_n\mathrm{Fib}_{m - 1} \]

简单推导:可以使用 "类斐波那契数列" 的知识解释。

\(G_0 = \mathrm{Fib}_n, G_1 = \mathrm{Fib}_{n + 1}\)\(G_n = G_{n - 1} + G_{n - 2}\),则有 \(\mathrm{Fib}_{n + m} = G_m\)

斐波那契数列相邻项互质性质

\[\gcd(\mathrm{Fib}_n, \mathrm{Fib}_{n + 1}) = 1 \]

斐波那契数列 gcd 性质

\[\gcd(\mathrm{Fib}_n, \mathrm{Fib}_m) = \mathrm{Fib}_{\gcd(n, m)} \]

简单推导:不妨设 \(n < m\),则有

\[\begin{aligned} \gcd(\mathrm{Fib}_n, \mathrm{Fib}_m) & = \gcd(\mathrm{Fib}_n, \mathrm{Fib}_{n + (m - n)}) \\ & = \gcd(\mathrm{Fib}_n, \mathrm{Fib}_{n + 1}\mathrm{Fib}_{m - n} + \mathrm{Fib}_n\mathrm{Fib}_{m - n - 1}) \\ & = \gcd(\mathrm{Fib}_n, \mathrm{Fib}_{n + 1}\mathrm{Fib}_{m - n}) \end{aligned} \]

由上文提到的 \(\mathrm{Fib}_{n + 1}\)\(\mathrm{Fib}_n\) 互质。则有 \(\gcd(\mathrm{Fib}_n, \mathrm{Fib}_m) = \gcd(\mathrm{Fib}_n, \mathrm{Fib}_{m - n})\)。该式子与辗转相除法相似,得证。

斐波那契数列整除性质

\[\mathrm{Fib}_n \mid \mathrm{Fib}_m \iff n \leq 2 \lor n \mid m \]

斐波那契数列模意义下的循环节

在模意义下,二元组 \((\mathrm{Fib}_n \bmod p, \mathrm{Fib}_{n + 1} \bmod p)\) 仅有 \(p^2\) 种取值。因此,当 \(n\) 足够大时,必定会出现循环节

根据递推式,有 \(\mathrm{Fib}_n = \mathrm{Fib}_{n + 2} - \mathrm{Fib}_{n + 1}\)也就是说 \(\mathrm{Fib}_{n + 1}, \mathrm{Fib}_{n + 2}\) 的前面必为 \(\mathrm{Fib}_n\)

根据这两个事实我们可以得知,斐波那契数列在模意义下必有循环节,并且一定是纯循环的(循环节的起点为 \(\mathrm{Fib}_0, \mathrm{Fib}_1\)

最小循环节(皮萨诺周期):定义斐波那契数列模 \(p\) 意义下的最小循环节 \(\pi(p)\) 为最小的正整数 \(m\),使得 \(\mathrm{Fib}_k \equiv 0 \pmod p\)\(\mathrm{Fib}_{k + 1} \equiv 1 \pmod p\)

注意:周期一定是最小循环节的倍数,即

\[\begin{cases} \mathrm{Fib}_k \equiv 0 \pmod p \\ \mathrm{Fib}_{k + 1} \equiv 1 \pmod p \end{cases} \iff \pi(p) \mid k \]

很多时候我们只需求出周期即可。

最小循环节的上界斐波那契数列 \(\operatorname{mod} m\) 的最小正周期不超过 \(6m\),即 \(\pi(m) \leq 6m\)

求斐波那契数列循环节 - 结论法

  • 质数情景:
    • \(\pi(2) = 3\)\(\pi(5) = 20\)
    • \(p\) 为奇质数且 \(p \equiv \pm 1 \pmod 5\) 时,\(\pi(p) \mid p - 1\)
    • \(p\) 为奇质数且 \(p \equiv \pm 2 \pmod 5\) 时,\(\pi(p) \mid 2p + 2\)
  • 一般情景:
    • 对于质数 \(p\) 以及任意正整数 \(k\),有 \(\pi(p^k) \mid p^{k - 1}\pi(p)\)
    • 对于 \(m = \prod p_i^{c_i}\),有 \(\pi(m) = \mathrm{lcm}\{ \pi(p_i^{c_i}) \}\),同时有 \(\pi(m) \mid \mathrm{lcm}\{ p_i^{c_i - 1}\pi(p_i) \}\)

在一般应用中,通常只需要求出一个循环节即可。

对于质数 \(p\),记

\[g(p) = \begin{cases} 3 & p = 2 \\ 20 & p = 5 \\ p - 1 & p \equiv \pm 1 \pmod 5\\ 2p + 2 & p \equiv \pm 2 \pmod 5 \end{cases} \]

然后将模数 \(m\) 质因数分解成 \(\prod p_i^{c_i}\),使用 \(\mathrm{lcm}\{ p_i^{c_i - 1}g(p_i) \}\) 作为循环节即可(注意这并不是最小循环节),这个值在多数情况下已经够用了。

求斐波那契数列循环节 - 矩阵 BSGS

适用场景:最小循环节的上界确定,转移矩阵可逆。

考虑斐波那契数列的矩阵表示

\[\begin{bmatrix} \mathrm{Fib}_n & \mathrm{Fib}_{n - 1} \end{bmatrix} = \begin{bmatrix} 1 & 0 \end{bmatrix} \times \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n - 1} \]

记转移矩阵 \(A = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}\),那么循环节即为满足 \(A^m \equiv I \pmod p\) 的正整数 \(m\)

在转移矩阵可逆的情况下,套用 BSGS 的流程:取 \(S\) 表示上界的根号,设 \(m = iS - j\)(其中 \(i \geq 1\)\(0 \leq j < S\)),则有

\[A^{iS} \times A^{-j} \equiv I \pmod p \\ A^{iS} \equiv A^j \pmod p \]

预处理 \(0 \leq j < S\)\(A^j\) 存入哈希表,每次在哈希表中查询 \(A^{iS}\) 即可。

时间复杂度 \(\mathcal{O}(\sqrt{p})\)

求斐波那契数列循环节 - 随机化(生日悖论)

若存在两个数 \(0 \leq i, j \leq 6p\),满足 \((\mathrm{Fib}_i, \mathrm{Fib}_{i + 1}) \equiv (\mathrm{Fib}_j, \mathrm{Fib}_{j + 1}) \pmod p\),则 \(|i - j|\) 为一个循环节(注意这并不是最小循环节)。

每次新随机一个数 \(x\),将二元组 \((\mathrm{Fib}_x, \mathrm{Fib}_{x + 1})\) 存入哈希表中,并查询哈希表中有没有与之相同的 \((\mathrm{Fib}_y, \mathrm{Fib}_{y + 1})\)

发现这与生日悖论的形式很像,于是时间复杂度正确。

生日悖论:在 \([1, n]\) 中随机选择正整数,选出两个相同数的期望轮数为 \(\mathcal{O}(\sqrt{n})\) 级别(近似 \(\sqrt{\frac{\pi n}{2}}\))。

实际取数过程可能需要在 \([0, 12p]\) 中随机以保证期望次数。求 \(\mathrm{Fib}_{i}\) 需要用到 "光速幂" 的技巧。

0x34 数学知识 初等数论

质数

质数:若一个正整数 \(n\) 无法被除了 \(1\) 和它自身以外的任何正整数整除,则称该数为质数(或素数)。否则称该数为合数。

质数分布函数\(\pi(n)\) 表示 \(1 \sim n\) 中的质数个数。当 \(n\) 足够大时,\(\pi(n) \sim \frac{n}{\ln n}\)

唯一分解定理:任意一个大于 \(1\) 的正整数 \(n\),都可以唯一分解成有限个质数的乘积,记作

\[n = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t} \]

其中 \(p_i\) 均为质数且满足 \(p_1 < p_2 < \cdots < p_t\)\(c_i\) 均为正整数。

质因数的根号分治:一个正整数 \(n\),大于 \(\sqrt{n}\) 的质因子最多只有 \(1\) 个。

质数判定

若一个正整数 \(n\) 为合数,则至少存在一个正整数 \(d\),其中 \(2 \le d \leq \sqrt{n}\)

于是我们只需枚举 \(2 \sim \sqrt{n}\) 之间的所有整数,依次检查是否能够整除 \(n\) 即可。

时间复杂度 \(\mathcal{O}(\sqrt{n})\)

0x34 质数判定.cpp

/* 质数判定 */
bool isPrime(int n) {
    // assert(n > 1);
    for (int i = 2; 1ll * i * i <= n; i ++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

质因数分解

尝试枚举 \(2 \sim \sqrt{n}\) 之间的所有整数 \(d\)。若 \(d\) 能够整除 \(n\),则从 \(n\) 之中删除 \(d\) 的所有因子,并且累计 \(d\) 的个数。

由于 \(n\) 的合数因子,必定在枚举该因子之前就从 \(n\) 中删除掉了。所以上述过程中枚举到的因数 \(d\) 必定是质因数

若最后的 \(n\) 没有分解完毕(仍有 \(n > 1\)),则剩下的 \(n\) 也为质因数。

时间复杂度 \(\mathcal{O}(\sqrt{n})\)

0x34 质因数分解.cpp

/* 质因数分解 */
auto PrimeFactor(int n) {
    std::vector< std::pair<int, int> > seq;
    for (int i = 2; 1ll * i * i <= n; i ++) {
        if (n % i == 0) {
            int c = 0;
            while (n % i == 0) {
                n /= i, c ++;
            }
            seq.push_back({i, c});
        }
    }
    if (n > 1) {
        seq.push_back({n, 1});
    }
    return seq;
}

阶乘的质因数分解

对于阶乘 \(n! = 1 \times 2 \times \cdots \times n\),由于每一项均为 \([1, n]\) 中的正整数,故分解出的质因数也均为 \([1, n]\) 中的质数。

筛出 \(1 \sim n\) 中的所有质数,对于每个质数 \(p\),考虑 \(n!\) 中一共包含了多少个质因子 \(p\)

共有大约 \(\frac{n}{\ln n}\) 个质数,每个质数 \(p\) 的求解需要 \(\log_p n\) 的开销,故时间复杂度 \(\mathcal{O}(n)\)

阶乘 \(n!\) 中包含质数 \(p\) 的次数

\[\sum_{i = 1}^{\lfloor \log_p n \rfloor} \left\lfloor \frac{n}{p^i} \right\rfloor \]

简单推导:设 \(c_i\) 表示 \(i\) 包含的质因子 \(p\) 个数,则阶乘 \(n!\) 包含 \(p\) 的个数为 \(\sum_{j = 1}^{\infty} \sum_{i = 1}^n j[c_i = j]\),进一步得 \(\sum_{j = 1}^{\infty}\sum_{i = 1}^n [c_i \geq j]\),不难发现其中 \(\sum_{i = 1}^n [c_i \geq j] = \left\lfloor \frac{n}{p^j} \right\rfloor\)

阶乘 \(n!\) 中包含合数 \(m\) 的次数:将 \(m\) 唯一分解成 \(m = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\) 的形式。

\(v_p(n) = \sum_{i = 1}^{\lfloor \log_p n \rfloor} \left\lfloor \frac{n}{p^i} \right\rfloor\),答案为

\[\min_{1 \leq i \leq t} \left\lfloor \frac{v_{p_i}(n)}{c_i} \right\rfloor \]

埃氏筛

注意到,任意整数 \(x(x > 1)\) 的倍数 \(2x, 3x, \cdots\) 都不是质数。

\(2\)\(n\) 依次扫描每一个数 \(x\),若 \(x\) 尚未被标记,则说明 \(x\) 为质数,并且将其倍数 \(2x, 3x, \cdots, \left\lfloor \frac{n}{x} \right\rfloor x\) 均标记为合数。

时间复杂度 \(\mathcal{O}(n \log \log n)\)

在开 O2 优化的情况下,std::bitset 连续读写效率较高,可以加以优化埃氏筛。

埃氏筛的时间复杂度证明:相当于是证明 \(1 \sim n\) 中所有质数的倒数之和为 \(\mathcal{O}(\log \log n)\) 级别

由质数分布函数,可以推测第 \(n\) 个质数的大小为 \(\Theta(n \log n)\),于是有

\[\begin{aligned} \sum_{k = 1}^{\pi(n)} \frac{1}{p_k} & = \mathcal{O}\left( \sum_{k = 2}^{\pi(n)} \frac{1}{k \log k} \right) \\ & = \mathcal{O}\left(\int_2^{\pi(n)} \frac{1}{x \log x} \mathrm{dx} \right) \\ & = \mathcal{O}(\log \log \pi(n)) \\ & = \mathcal{O}(\log \log n) \end{aligned} \]

0x34 埃氏筛.cpp

std::bitset<MaxV> vis;

/* 埃氏筛 */
void sieve(const int &n) {
    for (int i = 2; i <= n; i ++) {
        if (vis[i]) {
            continue;
        }
        for (int j = 2; j <= n / i; j ++) {
            vis[i * j] = 1;
        }
    }
}

线性筛

为了避免埃氏筛重复标记合数。在线性筛中,我们希望确定一个合数的方式能够唯一。

\(2\)\(n\) 依次扫描每一个数 \(x\),在标记合数时,我们只在 \(x\) 的基础上乘上一个质因子 \(p\),并且保证 \(p\)\(xp\) 的最小质因子。这样做,每个合数只会被自己的最小质因子标记一次。并且顺带还能筛出每个数的最小质因子。

时间复杂度 \(\mathcal{O}(n)\)

由于线性筛每次只扩展一个最小质因子的结构,可以额外处理很多信息(例如筛出积性函数)。

0x34 线性筛.cpp

int primeCount, prime[MaxV], fac[MaxV];

/* 线性筛 */
void sieve(const int &n) {
    for (int i = 2; i <= n; i ++) {
        if (!fac[i]) {
            prime[++ primeCount] = i, fac[i] = i;
        }
        for (int j = 1; j <= primeCount; j ++) {
            if (prime[j] > fac[i] || prime[j] > n / i) break;
            fac[i * prime[j]] = prime[j];
        }
    }
}

Miller-Rabin 素性测试

回顾一些内容:

费马小定理:设 \(p\) 是素数,对于任意整数 \(a\)\(a\) 不能是 \(p\) 的倍数),满足 \(a^{p - 1} \equiv 1 \pmod p\)

Fermat 伪素数:若满足 \(a^n \equiv 1 \pmod n\),但 \(n\) 不是素数,则称 \(n\) 为以 \(a\) 为底的 Fermat 伪素数。

对于任何固定的基底 \(a\),都存在无穷多个的 Fermat 伪素数

Fermat 素性测试:不断地选取在 \([2, n - 1]\) 中的基底 \(a\),并检验是否每次都满足 \(a^{n - 1} \equiv 1\pmod n\)

即使检查了所有与 \(n\) 互质的基底 \(a\),依然无法保证 \(n\) 是素数。这样的数 \(n\) 称为 Carmichael 数,有无穷多个。

二次探测定理:设 \(p\) 是奇素数,则同余方程 \(x^2 \equiv 1 \pmod p\) 的解为 \(x \equiv 1 \pmod p\)\(x \equiv p - 1\pmod p\)


Miller-Rabin 素性测试

Miller-Rabin 素性测试,结合了费马小定理和二次探测定理,是一种概率性素性测试。

实际上,没有已知的数字通过了 Miller-Rabin 素性测试但实际上是合数。可以放心使用。

先将 \(a^{n - 1} \equiv 1 \pmod n\) 中的指数 \(n - 1\),分解成 \(n - 1 = u \times 2^t\) 的形式。

每次随机一个基底 \(a\),进行如下的判断:

  • 先使用快速幂求出 \(v = a^u \bmod n\)。如果 \(v = 1\)\(v = p - 1\),则该轮测试通过。
  • 然后对这个值进行 \(t - 1\) 次平方操作,途中如果该值等于 \(p - 1\),则该轮测试通过。
  • 否则该轮测试不通过。

在不考虑乘法的开销时,对 \(n\) 进行 \(k\) 轮测试的时间复杂度为 \(\mathcal{O}(k \log n)\)

可以证明,奇合数 \(n(n > 9)\) 通过随机选取的一个基底的概率不超过 \(\frac{1}{4}\)因此随机选取 \(k\) 个基底后,仍然误判的概率不超过 \(\frac{1}{4^k}\)

对于 \([2, 2^{64})\) 范围内的数,工业上通常选用 \(\{ 2, 325, 9375, 28178, 450775, 9780504, 1795265022 \}\) 作为检验的基底。为了方便也可以选择前 \(12\) 个质数作为基底。

Pollard-Rho 质因数分解

Pollard-Rho:用于找出合数 \(n\) 的某一个非平凡因数 \(d\)(非平凡因数:非 \(1\) 且非 \(n\) 的因数)。

若一个正整数 \(n\) 为合数,那么 \(n\) 的最小质因子 \(p\) 满足 \(p \leq \sqrt{n}\)

如果随机生成 \(\mathcal{O}(\sqrt{p})\) 个正整数,由生日悖论,可能存在两个正整数 \(a, b\) 满足 \(a \equiv b \pmod p\),此时 \(|a - b|\)\(p\) 的倍数,于是查询 \(\gcd(|a - b|, n)\) 即可得到一个 \(n\) 的一个因子。

生日悖论:在 \([1, n]\) 中随机选择正整数,选出两个相同数的期望轮数为 \(\mathcal{O}(\sqrt{n})\) 级别(近似 \(\sqrt{\frac{\pi n}{2}}\))。

但这样做,枚举数对的时间复杂度还是太高。

记映射 \(f(x) = (x^2 + c) \bmod n\),其中 \(c\) 是一个随机选取的常数(\(c \neq 0, -2\))。Pollard 构造了这样的一个伪随机序列 \(\{x_i\}\),满足 \(x_i = f(x_{i - 1})\)

考虑序列 \(\{ x_i \bmod p \}\),由于 \(x_i\) 仅仅与 \(x_{i - 1}\) 有关,且 \(x_i \bmod p \in [0, p)\)所以 \(\{ x_i \bmod p \}\) 是一个混循环序列

建图后形似 \(\rho\) 形(rho),由生日悖论可得尾长与环长均为 \(\mathcal{O}(\sqrt{p})\) 级别

我们发现,\(x \equiv y \pmod p\),必有 \(f(x) \equiv f(y) \pmod p\)。由此可得在模 \(p\) 的环上,只要某个相距 \(d\) 步的点对满足条件,则所有相距 \(d\) 步的点对均满足条件。所以我们每次最好要检查不同的距离。

Brent 倍增迭代:初始有两个点 \(a, b\),在第 \(k\) 轮,\(a\) 原地不动,\(b\) 不断地向前共计 \(2^k\) 步,最后再让 \(a\) 直接跳到 \(b\) 的位置。\(b\) 的每一步移动,我们都计算 \(d = \gcd(|a - b|, n)\),当第一次出现 \(d > 1\)

  • \(1 < d < n\),则我们已经找到了一个非平凡因数,直接返回 \(d\)
  • \(d = n\),则说明我们已经完整地跑完了一个环,继续在环上跑也没有意义。
    此时我们应该换个映射,或者换一个初始值继续做。

进一步,我们想要降低求解 \(\gcd\) 的次数。注意到,如果 \(\gcd(a, n) > 1\),则对于任意正整数 \(b\) 均有 \(\gcd(ab \bmod n, n) = \gcd(ab, n) > 1\)

也就是说,如果 \(\gcd\left(\prod |x_i - x_j| \bmod n, n\right) > 1\),那么其中必有一对 \(x_i, x_j\) 满足 \(\gcd(|x_i - x_j|, n) > 1\)

于是我们每 \(k\)\(a, b\) 就计算一次 \(\gcd\)。时间复杂度 \(\mathcal{O}(\sqrt{p} + k^{-1}\sqrt{p} \log n)\),当 \(k\)\(\log n\) 大致相等时,可以得到 \(\mathcal{O}(\sqrt{p})\) 的理论复杂度。具体实现中,一般取 \(k = 128\)

Pollard-Rho 质因数分解:如果要对 \(n\) 进行质因数分解,先使用 Miller-Rabin 判断 \(n\) 是否是质数,若 \(n\) 为合数,则使用 Pollard-Rho 找出 \(n\) 的一个非平凡因数 \(d\),得到两个规模分别为 \(d\)\(\frac{n}{d}\) 的两个子问题,递归进行求解。

0x34 素性测试与质因数分解(Miller-Rabin + Pollard-Rho).cpp

/* Miller-Rabin + Pollard-Rho */
template <class T, auto qmul>
struct MRPR {
    constexpr T qpow(T a, T b, T p) {
        T ans = 1;
        for (; b; b >>= 1) {
            if (b & 1) ans = qmul(ans, a, p);
            a = qmul(a, a, p);
        }
        return ans;
    }

    std::mt19937_64 mtrand{std::random_device{}()};
    constexpr T rand(T l, T r) {
        std::uniform_int_distribution<T> range(l, r);
        return range(mtrand);
    }

    /* Miller-Rabin 素性测试 */
    bool isPrime(T n) {
        // assert(n > 1);
        if (n % 2 == 0) return n == 2;
        if (n % 3 == 0) return n == 3;

        T t = __builtin_ctzll(n - 1);
        T u = (n - 1) >> t;

        int test = 6;
        while (test --) {
            T a = rand(2, n - 1);
            T v = qpow(a, u, n);
            if (v == 1 || v == n - 1) {
                continue;
            }
            for (int i = 1; i < t; i ++) {
                v = qmul(v, v, n);
                if (v == n - 1) {
                    break;
                }
            }
            if (v != n - 1) {
                return 0;
            }
        }
        return 1;
    }

    T walk(T n) {
        T c = rand(1, n - 1);
        T x = rand(1, n - 1), y, v, d;
        for (int k = 1;; k <<= 1) {
            y = x, v = 1;
            for (int i = 1; i <= k; i ++) {
                x = qmul(x, x, n) + c; if (x >= n) x -= n;
                v = qmul(v, x - y, n);
                if (i % 127 == 0 || i == k) {
                    if (v < 0) v += n;
                    d = std::gcd(v, n);
                    if (d > 1) return d;
                }
            }
        }
    }

    /* Pollard-Rho 质因数分解 */
    auto PrimeFactor(T n) {
        std::map<T, int> buc;
        std::function<void(T)> solve = [&] (T n) {
            if (n <= 10000) {
                for (int i = 2; i * i <= n; i ++) {
                    while (n % i == 0) {
                        n /= i;
                        buc[i] ++;
                    }
                }
                if (n > 1) {
                    buc[n] ++;
                }
                return;
            }
            if (isPrime(n)) {
                buc[n] ++;
                return;
            }

            T d = 0;
            while (d = walk(n), d == n);

            solve(d), solve(n / d);
        };

        solve(n);

        std::vector< std::pair<T, int> > seq;
        for (auto dat : buc) {
            seq.push_back(dat);
        }
        return seq;
    }
};

// MRPR<int, [] (int a, int b, int p) { return 1ll * a * b % p; }> mrpr;
// MRPR<s64, [] (s64 a, s64 b, s64 p) { return static_cast<__int128>(a) * b % p; }> mrpr;

约数

整除:设 \(d, n \in \Z\)\(d \neq 0\),如果 \(\exist q \in \Z\) 使得 \(qd = n\),那么称 \(d\) 整除 \(n\)(记作 \(d \mid n\))。

整除的简单性质

  • \(a \mid b \land b \mid c \Longrightarrow a\mid c\)
  • \(a \mid b \land a \mid c \Longrightarrow \forall x, y \in \Z, a \mid (xb + yc)\)
  • \(m \neq 0\),则 \(a \mid b \Longrightarrow ma \mid mb\)
  • \(\gcd(a, m) = 1\),则 \(a \mid mb \Longrightarrow a\mid b\)

单个数的约数集合

\(d \geq \sqrt{n}\)\(n\) 的约数,则 \(\frac{n}{d} \leq \sqrt{n}\) 也一定是 \(n\) 的约数。

也就是说,约数 \(d\)\(\frac{n}{d}\) 一定是成对出现的(除了完全平方数,\(\sqrt{n}\) 单独出现),并且其中一个 \(\leq \sqrt{n}\),另一个 \(> \sqrt{n}\)

于是我们只需枚举 \(2 \sim \sqrt{n}\) 之间的所有整数 \(d\)。若 \(d \mid n\),则将 \(d\)\(\frac{n}{d}\) 一起加入 \(n\) 的约数集合。

时间复杂度 \(\mathcal{O}(\sqrt{n})\)

由上述分析,可知 \(n\) 的约数个数必定不超过 \(2\sqrt{n}\)。实际上很难达到该上界,在算法竞赛中可以简单认为 \(10^6 \sim 10^{18}\) 范围内约数个数为 \(\mathcal{O}(\sqrt[3]{n})\) 级别

如果我们想要得到排序后的结果。注意到枚举的 \(d\) 是升序的,\(\frac{n}{d}\) 是降序的,并且 \(\frac{n}{d}\) 的部分一定比 \(d\) 的部分数值更大。

于是我们只需将 \(\frac{n}{d}\) 的部分反转后插入 \(d\) 的部分的末尾即可。

0x34 单个数的约数集合.cpp

/* 单个数的约数集合 */
auto Factor(int n) {
    std::vector<int> seq;
    for (int i = 1; 1ll * i * i <= n; i ++) {
        if (n % i == 0) {
            seq.push_back(i);
            if (i * i < n) {
                seq.push_back(n / i);
            }
        }
    }
    return seq;
}

0x34 单个数的约数集合(排序).cpp

/* 单个数的约数集合(排序) */
auto Factor(int n) {
    std::vector<int> lhs, rhs;
    for (int i = 1; 1ll * i * i <= n; i ++) {
        if (n % i == 0) {
            lhs.push_back(i);
            if (i * i < n) {
                rhs.push_back(n / i);
            }
        }
    }
    for (int i = (int)rhs.size() - 1; i >= 0; i --) {
        lhs.push_back(rhs[i]);
    }
    return lhs;
}

1~n 的约数集合

反过来考虑 \(i\) 可以作为哪些数的约数,显然 \(i\) 可以作为 \(i, 2i, \cdots, \left\lfloor \frac{n}{i} \right\rfloor i\) 的约数。

时间复杂度 \(\sum_{i = 1}^n \frac{n}{i} = \mathcal{O}(n \log n)\)

由上述分析,可知 \(1 \sim n\) 的约数个数总和为 \(\mathcal{O}(n \log n)\) 级别

0x34 1~n 的约数集合.cpp

std::vector<int> factor[MaxV];

/* 1~n 的约数集合 */
void FactorInit(const int &n) {
    for (int i = 1; i <= n; i ++) {
        for (int j = i; j <= n; j += i) {
            factor[j].push_back(i);
        }
    }
}

最大公约数与最小公倍数

最大公约数:最大公共约数(Greatest Common Divisor,简写 GCD)。

最小公倍数:最小公共倍数(Least Common Multiple,简写 LCM)。

唯一分解角度下的 gcd 与 lcm:设 \(a = p_1^{c_1}p_2^{c_2}\cdots\)\(b = p_1^{d_1}p_2^{d_2} \cdots\),则有

\[\gcd(a, b) = p_1^{\min(c_1, d_1)}p_2^{\min(c_2, d_2)} \cdots \\ \mathrm{lcm}(a, b) = p_1^{\max(c_1, d_1)}p_2^{\max(c_2, d_2)} \cdots \]

在唯一分解角度下,gcd 相当于是对每个指数取 min,lcm 相当于是对每个指数取 max

gcd 的简单性质

  • \(\gcd(a_1, \cdots, a_n) = \gcd(|a_1|, \cdots, |a_n|)\)
  • \(\gcd(a, 0) = \gcd(a, a) = |a|\)
  • \(\gcd(bq + r, b) = \gcd(r, b)\)
  • \(\gcd(a, m) = 1\),则 \(\gcd(a, mb) = \gcd(a, b)\)
  • \(\gcd(ma_1, \cdots, ma_n) = |m| \gcd(a_1, \cdots, a_n)\)

lcm 的简单性质

  • \(\mathrm{lcm}(a_1, \cdots, a_n) = \mathrm{lcm}(|a_1|, \cdots, |a_n|)\)
  • \(\mathrm{lcm}(a, 1) = \mathrm{lcm}(a, a) = |a|\)
  • \(\mathrm{lcm}(ma_1, \cdots, ma_n) = |m| \operatorname{lcm}(a_1, \cdots, a_n)\)

gcd 和 lcm 的常见恒等式

gcd-lcm 恒等式

\[\gcd(a, b) \operatorname{lcm}(a, b) = ab \]

请注意!该式子扩展到三元以上几乎不成立!

事实上当 \(n \geq 3\) 时,\(\gcd(a_1, \cdots, a_n) \operatorname{lcm}(a_1, \cdots, a_n) = a_1\cdots a_n\) 当且仅当 \(a_1, \cdots, a_n\) 两两互质。

gcd 和 lcm 的结合律

\[\gcd(a, b, c) = \gcd(\gcd(a, b), c) \\ \mathrm{lcm}(a, b, c) = \mathrm{lcm}(\mathrm{lcm}(a, b), c) \]

该式子扩展到任意多元仍然成立。

于是可以使用恒等式 \(\mathrm{lcm}(a, b) = \frac{ab}{\gcd(a, b)}\),用来求解 \(\mathrm{lcm}(a, b, c)\),以及 \(\mathrm{lcm}(a_1, \cdots, a_n)\)

gcd 对乘法的分配律

\[\gcd(a, bc) = \gcd(a, b) \gcd\left(\frac{a}{\gcd(a, b)}, c\right) \]

根据对乘法的分配律计算 \(\gcd(a, b_1\cdots b_n)\)。每次处理 \(b_i\) 时,计算 \(t = \gcd(a, b_i)\),然后将 \(a\) 除以 \(t\)(相当于是将已计算的质因数贡献去掉),答案乘以 \(t\)

gcd 的差分律

\[\gcd(a_1, a_2, \cdots, a_n) = \gcd(a_1, a_2 - a_1, \cdots, a_n - a_{n - 1}) \]

通过差分律,我们可以维护差分数组的 gcd,以实现 "区间加" 与 "区间 gcd"。

斐波那契数列 gcd 恒等式

\[\gcd(\mathrm{Fib}_n, \mathrm{Fib}_m) = \mathrm{Fib}_{\gcd(n, m)} \]

幂差 gcd 恒等式不妨设 \(a \geq b\),当 \(\gcd(a, b) = 1\),有

\[\gcd(a^n - b^n, a^m - b^m) = a^{\gcd(n, m)} - b^{\gcd(n, m)} \]

简单推导:

右边整除左边:可以证明 \(x - y \mid x^k - y^k\)

\(k\) 次方差公式:\(x^k - y^k = (x - y)(x^{k - 1} + x^{k - 2}y + \cdots + xy^{k - 2} + y^{k - 1})\)

对于 \(n, m\) 的公约数 \(d\) 来说,可以证得 \(a^d - b^d \mid a^n - b^n\)\(a^d - b^d \mid a^m - b^m\)

于是 \(a^d - b^d \mid \gcd(a^n - b^n, a^m - b^m)\)。所以

\[a^{\gcd(n, m)} - b^{\gcd(n, m)} \mid \gcd(a^n - b^n, a^m - b^m) \]

左边整除右边

"降幂引理":不妨设 \(r \geq s\),当 \(\gcd(a, b) = 1\) 时,有

\[\gcd(a^r - b^r, a^s - b^s) \mid a^{r - s} - b^{r - s} \]

证明:设 \(d\)\(a^r - b^r, a^s - b^s\) 的公约数。

因为 \((a^r - b^r) - a^{r - s}(a^s - b^s) = b^s(a^{r - s} - b^{r - s})\),可以证得 \(d \mid b^s(a^{r - s} - b^{r - s})\);同理可得 \(d \mid a^s(a^{r - s} - b^{r - s})\)

又由于 \(\gcd(a, b) = 1\),于是 \(d \mid a^{r - s} - b^{r - s}\),"降幂引理" 得证。

\(\gcd(a^n - b^n, a^m - b^m)\) 反复应用 "降幂引理",过程等价于辗转相除法。所以

\[\gcd(a^n - b^n, a^m - b^m) \mid a^{\gcd(n, m)} - b^{\gcd(n, m)} \]

最大公约数常见求法

已有的轮子:std::gcd(a, b)

求法 1:辗转相除法

(也称之为欧几里得算法)

对于 \(\forall a, b \in \N\)\(b \neq 0\),有

\[\gcd(a, b) = \gcd(b, a \bmod b) \]

  • \(a < b\) 时,一次迭代后就会转化为 \(a \geq b\)
  • \(a \geq b\) 时,由于 \(a \bmod b \leq \frac{a}{2}\),这样的迭代不超过 \(\log_2 a\) 次。

时间复杂度 \(\mathcal{O}(\log \max(a, b))\)

使用辗转相除法求解斐波那契数列相邻两项的 gcd,会让该算法达到最坏复杂度。

0x34 gcd(辗转相除法).cpp

/* 辗转相除法 */
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

求法 2:更相减损术

(也称之为 Stein 算法)

对于 \(\forall a, b \in \N\),不妨设 \(a\geq b\),有

\[\gcd(a, b) = \gcd(a, a - b) = \gcd(b, a - b) \\ \gcd(2a, 2b) = 2\gcd(a, b) \]

  • \(a = b\) 时,\(\gcd(a, b) = a\)
  • "两个偶数":\(\gcd(a, b) = 2\gcd(\frac{a}{2}, \frac{b}{2})\)
  • "一奇一偶":不妨设 \(a\) 为奇数,\(\gcd(a, b) = \gcd(\frac{a}{2}, b)\)
  • "两个奇数":不妨设 \(a \geq b\)\(\gcd(a, b) = \gcd(a - b, b)\)

当至少有一个偶数的时候,每次迭代至少会将 \(a, b\) 之一减半。而两个奇数相减会得到偶数。

时间复杂度 \(\mathcal{O}(\log \max(a, b))\)

更相减损术常用于计算高精度 gcd(因为 "高精度取模" 复杂度较高,"高精度减法" 以及 "高精除低精" 复杂度较低)。

0x34 gcd(更相减损术).cpp

/* 更相减损术 */
int gcd(int a, int b) {
    if (a == b) {
        return a;
    }
    if (a == 0 || b == 0) {
        return a ^ b;
    }
    int az = __builtin_ctz(a), bz = __builtin_ctz(b), z = az < bz ? az : bz, v = 0;
    a >>= az, b >>= bz;
    while (b) {
        v = b - a;
        a = a < b ? a : b;
        b = v < 0 ? -v : v;
        if (b) b >>= __builtin_ctz(b);
    }
    return a << z;
}

求法 3:递推

初值 \(\gcd(i, 0) = \gcd(0, i) = i\),递推式 \(\gcd(i, j) = \gcd(i - j, j)\)

时间复杂度:预处理 \(\mathcal{O}(n^2)\),查询 \(\mathcal{O}(1)\)

for (int i = 0; i <= n; i ++) {
    g[i][0] = g[0][i] = i;
    for (int j = 1; j <= i; j ++) {
        g[i][j] = g[j][i] = g[i - j][j];
    }
}

求法 4:基于值域预处理的快速 gcd

引理:对于任意正整数 \(n\),一定可以被拆分成三个正整数 \(a, b, c(a \leq b \leq c)\) 的乘积,其中 \(a, b\leq \sqrt{n}\),且 \(c \leq \sqrt{n}\)\(c \in \mathbb{P}\) 至少有一个成立。

证明 1:假设 \(c > \sqrt{n}\)\(c \notin \mathbb{P}\)。因为 \(c > \sqrt{n}\)\(abc = n\),所以 \(ab \leq \sqrt{n}\)。由于 \(c\) 不是质数,设 \(c = xy\),显然 \(x, y\) 不可能同时 \(>\sqrt{n}\),不妨设 \(x \leq y\),则必有 \(x \leq \sqrt{n}\)。此时 \(n\) 可以被拆分成 \(x, ab, y\) 的乘积。不断对 \(y\) 进行相同的讨论。

由于数值不断递减,迭代到最后 \(c \leq \sqrt{n}\)\(c \in \mathbb{P}\) 至少有一个成立。

证明 2:考虑归纳。当 \(n = 1\) 时显然成立。当 \(n > 1\) 时,设 \(p\)\(n\) 的最小质因数,设 \(a', b', c'(a' \leq b' \leq c')\)\(\frac{n}{p}\) 的一组合法拆分。

  • \(p \leq \sqrt[4]{n}\) 时。由于 \(a' \leq \sqrt[3]{\frac{n}{p}}\),所以 \(pa' \leq \sqrt[3]{np^2} \leq \sqrt{n}\)。此时 \(pa', b', c'\) 为一组合法拆分。
  • \(p > \sqrt[4]{n}\) 时。假设 \(a' \geq p\),则 \(n = pa'b'c' \geq p^4 > n\) 矛盾,故 \(a' < p\)。由于 \(p\)\(n\) 的最小质因数,所以 \(a' = 1\)。此时 \(p, b', c'\) 为一组合法拆分。

预处理:通过线性筛得出 \(1 \sim n\) 所有数的最小质因数。然后根据证明 2,递推得出 \(1 \sim n\) 所有数的合法拆分,对于 \(i > 1\),设 \(p\)\(i\) 的最小质因数,\(\frac{i}{p}\) 的合法拆分中的最小数乘以 \(p\) 即可得到 \(i\) 的合法拆分。最后预处理出所有 \(0 \leq x, y \leq \sqrt{n}\)\(\gcd(x, y)\)

查询:当询问 \(\gcd(p, q)\) 时,根据 gcd 对乘法的分配律,依次考虑 \(q\) 的合法拆分 \(a, b, c\),设当前考虑到的数为 \(x\)

  • 计算 \(t = \gcd(p, x)\),具体地:
    • \(x\) 为合数(此时 \(x \leq \sqrt{n}\)),则 \(\gcd(p, x) = \gcd(x, p \bmod x)\)。使用预处理的 gcd 值回答即可。
    • \(x\) 为质数,则当且仅当 \(p\)\(x\) 的倍数时 \(\gcd(p, x) = x\),否则 \(\gcd(p, x) = 1\)
  • 然后将 \(p\) 除以 \(t\),答案乘以 \(t\)

时间复杂度:预处理 \(\mathcal{O}(n)\),查询 \(\mathcal{O}(1)\)

0x34 gcd(基于值域预处理的快速 gcd).cpp

/* O(n)-O(1) gcd */
struct flashGCD {
    int b;
    std::vector<int> prime, fac;
    std::vector< std::array<int, 3> > sp;
    std::vector< std::vector<int> > g;

    flashGCD() {}
    flashGCD(int n) {
        init(n);
    }

    void init(int n) {
        b = sqrt(n);
        prime.clear(), fac.assign(n + 1, 0);
        sp.assign(n + 1, {0, 0, 0});
        g.assign(b + 1, std::vector<int>(b + 1, 0));

        for (int i = 2; i <= n; i ++) {
            if (!fac[i]) {
                prime.push_back(i), fac[i] = i;
            }
            for (int p : prime) {
                if (p > fac[i] || p > n / i) break;
                fac[i * p] = p;
            }
        }

        sp[1] = {1, 1, 1};
        for (int i = 2; i <= n; i ++) {
            sp[i] = sp[i / fac[i]];
            sp[i][0] *= fac[i];

            if (sp[i][0] > sp[i][1]) {
                std::swap(sp[i][0], sp[i][1]);
            }
            if (sp[i][1] > sp[i][2]) {
                std::swap(sp[i][1], sp[i][2]);
            }
        }

        for (int i = 0; i <= b; i ++) {
            g[i][0] = g[0][i] = i;
            for (int j = 1; j <= i; j ++) {
                g[i][j] = g[j][i] = g[i - j][j];
            }
        }
    }

    int gcd(int p, int q) {
        if (p == 0 || q == 0) {
            return p ^ q;
        }
        int ans = 1, t;
        for (int x : sp[q]) {
            if (x <= b) {
                t = g[x][p % x];
            } else {
                t = p % x ? 1 : x;
            }
            p /= t, ans *= t;
        }
        return ans;
    }
} fg;

类欧几里得算法

类欧几里得算法:用来解决一类形如 \(\left\lfloor \frac{ai + b}{c} \right\rfloor\) 结构的求和问题。

基本模型为

\[f(a, b, c, n) = \sum_{i = 0}^n \left\lfloor \frac{ai + b}{c} \right\rfloor \]

首先,可以转化为 \(0 \leq a, b < c\) 的情况

\[\begin{aligned} f(a, b, c, n) & = \sum_{i = 0}^n \left\lfloor \frac{ai + b}{c} \right\rfloor \\ & = \sum_{i = 0}^n \left\lfloor \frac{\left(\left\lfloor \frac{a}{c} \right\rfloor c + (a \bmod c) \right)i + \left(\left\lfloor \frac{b}{c} \right\rfloor c + (b \bmod c)\right)}{c} \right\rfloor \\ & = \sum_{i = 0}^n \left( \left\lfloor \frac{a}{c} \right\rfloor i + \left\lfloor \frac{b}{c} \right\rfloor + \left\lfloor \frac{(a \bmod c) i + (b \bmod c)}{c} \right\rfloor \right) \\ & = \frac{n(n + 1)}{2} \left\lfloor \frac{a}{c} \right\rfloor + (n + 1)\left\lfloor \frac{b}{c} \right\rfloor + f(a \bmod c, b \bmod c, c, n) \end{aligned} \]

然后,令 \(m = \left\lfloor \frac{an + b}{c} \right\rfloor\)

\[\begin{aligned} f(a, b, c, n) & = \sum_{i = 0}^n \left\lfloor \frac{ai + b}{c} \right\rfloor \\ & = \sum_{i = 0}^n \sum_{j = 0}^{m - 1} \left[ j < \left\lfloor \frac{ai + b}{c} \right\rfloor \right] \\ & = \sum_{i = 0}^n \sum_{j = 0}^{m - 1} \left[ j + 1 \leq \frac{ai + b}{c} \right] \\ & = \sum_{i = 0}^n \sum_{j = 0}^{m - 1} \left[ cj + c - b - 1 < ai \right] \\ & = \sum_{j = 0}^{m - 1} \sum_{i = 0}^n \left[ \left\lfloor \frac{cj + c - b - 1}{a} \right\rfloor < i \right] \\ & = \sum_{j = 0}^{m - 1} \left( n - \left\lfloor \frac{cj + c - b - 1}{a} \right\rfloor \right) \\ & = nm - f(c, c - b - 1, a, m - 1) \end{aligned} \]

结合这两部操作,二元组 \((a, c) \to (c, a \bmod c)\),相当于对 \((a, c)\) 进行辗转相除法,故称之为类欧几里德算法。

时间复杂度 \(\mathcal{O}(\log \min(a, c))\)

0x34 类欧几里德算法.cpp

/* 类欧几里德算法(要求 a, b >= 0 且 c > 0) */
s64 euclid(s64 a, s64 b, s64 c, s64 n) {
    if (a == 0) {
        return (n + 1) * (b / c);
    }
    if (a >= c || b >= c) {
        return euclid(a % c, b % c, c, n) +
        (n * (n + 1) / 2) * (a / c) + (n + 1) * (b / c);
    }
    s64 m = (a * n + b) / c;
    if (m == 0) {
        return 0;
    }
    return n * m - euclid(c, c - b - 1, a, m - 1);
}

/* 类欧几里德算法 */
s64 euclid_full(s64 a, s64 b, s64 c, s64 n) {
    if (c < 0) {
        a = -a, b = -b, c = -c;
    }
    s64 res = 0;
    if (a < 0) {
        s64 t = (a - c + 1) / c;
        a -= t * c, res += (n * (n + 1) / 2) * t;
    }
    if (b < 0) {
        s64 t = (b - c + 1) / c;
        b -= t * c, res += (n + 1) * t;
    }
    return res + euclid(a, b, c, n);
}

取整

向下取整\(\lfloor x \rfloor\),表示不超过 \(x\) 的最大整数(floor)。

向上取整\(\lceil x \rceil\),表示不小于 \(x\) 的最小整数(ceiling)。

注意:C++ 中的整数除法结果向零取整,详见 "0x11 整数除法上下取整"。

取整的简单性质

  • \(x - 1 < \lfloor x \rfloor \leq x\)\(x \leq \lceil x \rceil < x + 1\)
  • \(\lfloor -x \rfloor = - \lceil x \rceil\)\(\lceil -x \rceil = - \lfloor x \rfloor\)

取整的常见恒等式

上下取整的相互转化\(n, m \in \Z\)\(m > 0\)

\[\left\lceil \frac{n}{m} \right\rceil = \left\lfloor \frac{n + m - 1}{m} \right\rfloor = \left\lfloor \frac{n - 1}{m} \right\rfloor + 1 \\ \left\lfloor \frac{n}{m} \right\rfloor = \left\lceil \frac{n - m + 1}{m} \right\rceil = \left\lceil \frac{n + 1}{m} \right\rceil - 1 \]

上下取整的嵌套恒等式\(a, b, c \in \N_+\)

\[\left\lfloor \frac{ \left\lfloor \frac{a}{b} \right\rfloor }{c} \right\rfloor = \left\lfloor \frac{a}{bc} \right\rfloor \\ \left\lceil \frac{ \left\lceil \frac{a}{b} \right\rceil }{c} \right\rceil = \left\lceil \frac{a}{bc} \right\rceil \]

上下取整对加法的恒等式\(A, B, C, D \in \N_+\)

\[\left\lfloor \frac{ \left\lfloor \frac{A}{B} \right\rfloor + C }{D} \right\rfloor = \left\lfloor \frac{A + BC}{BD} \right\rfloor \\ \left\lceil \frac{ \left\lceil \frac{A}{B} \right\rceil + C }{D} \right\rceil = \left\lceil \frac{A + BC}{BD} \right\rceil \]

数论分块

也称之为整除分块。

下取整数论分块

给出一个正整数 \(n\),记 \(D_n\) 表示 \(\left\lfloor \frac{n}{d} \right\rfloor\) 所有可能的取值构成的集合,即 \(D_n = \{ \left\lfloor \frac{n}{d} \right\rfloor \mid 1 \leq d \leq n \}\)

性质 1\(\left\lfloor \frac{n}{d} \right\rfloor\) 的取值不超过 \(2\sqrt{n}\) 种,即 \(|D_n| \leq 2\sqrt{n}\)

简单推导:

  • \(d \leq \sqrt{n}\) 时,因为 \(d\) 的取值不超过 \(\sqrt{n}\) 种,所以此时 \(\left\lfloor \frac{n}{d} \right\rfloor\) 的取值不超过 \(\sqrt{n}\) 种。
  • \(d > \sqrt{n}\) 时,因为 \(\left\lfloor \frac{n}{d} \right\rfloor \leq \sqrt{n}\),所以此时 \(\left\lfloor \frac{n}{d} \right\rfloor\) 的取值不超过 \(\sqrt{n}\) 种。

性质 2:若 \(m \in D_n\),则 \(D_m \subseteq D_n\)

简单推导:设 \(m = \left\lfloor \frac{n}{k} \right\rfloor\),此时对于任意 \(d \in \N_+\)

\[\left\lfloor \frac{m}{d} \right\rfloor = \left\lfloor \frac{\left\lfloor \frac{n}{k} \right\rfloor}{d} \right\rfloor = \left\lfloor \frac{n}{kd} \right\rfloor \in D_n \]

这意味着,如果我们想要递归地应用数论分块(类似杜教筛),在计算过程中涉及到的取值集合均为 \(D_n\)

下取整数论分块:快速地找出 \(\left\lfloor \frac{n}{d} \right\rfloor\) 取值相同的所有区间。

由于函数 \(\frac{n}{x}\) 在区间 \((0, +\infty)\) 上单调递减,所以 \(\left\lfloor \frac{n}{x} \right\rfloor\) 取值相同的所有自变量 \(x\) 构成一段连续的区间。并且由上述分析,这样的区间不超过 \(2\sqrt{n}\) 个。

对于任意 \(i(1 \leq i \leq n)\),需要找到一个最大的 \(j(i \leq j \leq n)\) 满足 \(\left\lfloor \frac{n}{i} \right\rfloor = \left\lfloor \frac{n}{j} \right\rfloor\)。由不等式 \(\left\lfloor \frac{n}{i} \right\rfloor \leq \frac{n}{j}\) 可得 \(\boldsymbol{j = \left\lfloor \frac{n}{\left\lfloor \frac{n}{i} \right\rfloor} \right\rfloor}\)

根据该公式,我们就可以根据区间的左端点计算出右端点。于是依次遍历所有区间即可。

for (int x = 1, nx; x <= n; x = nx + 1) {
    nx = n / (n / x);
    // 区间 [x, nx] 内,下取整的值均为 n / x
}

By the way:数论分块套数论分块的时间复杂度为 \(\mathcal{O}(n^{\frac{3}{4}})\)

上取整数论分块

上取整数论分块:快速地找出 \(\lceil \frac{n}{d} \rceil\) 取值相同的所有区间。

由恒等式 \(\left\lceil \frac{n}{m} \right\rceil = \left\lfloor \frac{n - 1}{m} \right\rfloor + 1\) 可知,\(n\) 的上取整数论分块与 \(n - 1\) 的下取整数论分块,分出的区间是一样的。

于是套用 \(n - 1\) 的下取整数论分块,注意统计 \(d = n\) 的特殊情况

for (int x = 1, nx; x <= n - 1; x = nx + 1) {
    nx = (n - 1) / ((n - 1) / x);
    // 区间 [x, nx] 内,上取整的值均为 (n - 1) / x + 1
}
// 注意统计 d = n 时的特殊情况

多维数论分块

二维数论分块:快速地找出 \(\left\lfloor \frac{n}{d} \right\rfloor, \left\lfloor \frac{m}{d} \right\rfloor\) 取值均相同的所有区间。

显然,这样的区间不超过 \(2\sqrt{n} + 2\sqrt{m}\) 个。

对于任意 \(i\),需要找到一个最大的 \(j\) 满足 \(\left\lfloor \frac{n}{i} \right\rfloor = \left\lfloor \frac{n}{j} \right\rfloor, \left\lfloor \frac{m}{i} \right\rfloor = \left\lfloor \frac{m}{j} \right\rfloor\)。则有 \(\boldsymbol{j = \min\left\{ \left\lfloor \frac{n}{ \left\lfloor \frac{n}{i} \right\rfloor } \right\rfloor, \left\lfloor \frac{m}{ \left\lfloor \frac{m}{i} \right\rfloor } \right\rfloor \right\}}\)

扩展到多维也是一样。

积性函数与迪利克雷卷积

积性函数:若数论函数 \(f(n)\),对于任意互质正整数 \(x, y\),都有 \(f(xy) = f(x)f(y)\),则称 \(f(n)\) 为积性函数。

完全积性函数:若数论函数 \(f(n)\),对于任意正整数 \(x, y\),都有 \(f(xy) = f(x)f(y)\),则称 \(f(n)\) 为完全积性函数。

积性函数的简单性质

  • 对于任意积性函数 \(f(x)\)(除了恒为 \(0\) 的函数),有 \(f(1) = 1\)
  • 对于任意积性函数 \(f(x), g(x)\)以下的函数仍为积性函数

\[f(x^p) \\ f^p(x) \\ f(x)g(x) \\ \sum_{d \mid x} f(d)g\left( \frac{x}{d} \right) \]

  • 对于任意积性函数 \(f(x)\),将 \(x\) 唯一分解成 \(x = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\),有 \(f(x) = \prod_{i = 1}^t f(p_i^{c_i})\)

常见积性函数(及性质)

单位函数(完全积性)

\[\epsilon(n) = [n = 1] \]

恒等函数(完全积性)

\[\mathrm{ID}_k(n) = n^k \]

\(k = 1\) 时,亦可简写为 \(\mathrm{ID}(n) = n\)

常数函数(完全积性)

\[1(n) = 1 \]

约数函数(积性)

约数函数

\[\sigma_k(n) = \sum_{d \mid n} d^k \]

\(k = 0\) 时,\(\sigma_0(n)\) 表示 \(n\) 的约数个数函数。

约数个数函数的前缀和

\[\sum_{i = 1}^n \sigma_0(i) = \sum_{i = 1}^n \left\lfloor \frac{n}{i} \right\rfloor \]

乘积的约数个数函数

\[\sigma_0(ij) = \sum_{x \mid i} \sum_{y \mid j} [\gcd(x, y) = 1] \\ \sigma_0(ijk) = \sum_{a \mid i} \sum_{b \mid j} \sum_{c \mid k} [\gcd(a, b) = \gcd(b, c) = \gcd(a, c) = 1] \]

简单推导(仅证二维):从唯一分解的角度,对每个质因子 \(p\) 单独考虑。\(p\)\((x, y)\) 中的出现情况数,为 \(i, j\) 中质因子 \(p\) 的个数之和再加 \(1\)。将所有质因子 \(p\) 的贡献相乘,这正是 \(\sigma(ij)\) 的表达式。

可以扩展至更多维。

欧拉函数(积性)

欧拉函数\(\varphi(n)\),表示 \(1 \sim n\) 中与 \(n\) 互质的数的个数。将 \(n\) 唯一分解成 \(n = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\),则欧拉函数为

\[\varphi(n) = n \prod_{i = 1}^t (1 - \frac{1}{p_i}) \]

乘积的欧拉函数

\[\varphi(ij) = \frac{\varphi(i) \varphi(j) \gcd(i, j)}{\varphi(\gcd(i, j))} \]

简单推导:设 \(P(n)\) 表示 \(n\) 的质因数集合,则有 \(P(ij) = P(i) \cup P(j)\)\(P(\gcd(i, j)) = P(i) \cap P(j)\)

由容斥原理得

\[\prod_{p \mid ij} \left(1 - \frac{1}{p}\right) = \frac{\prod_{p \mid i} \left(1 - \frac{1}{p}\right) \prod_{p \mid j} \left(1 - \frac{1}{p}\right)}{\prod_{p \mid \gcd(i, j)} \left(1 - \frac{1}{p}\right)} \iff \frac{\varphi(ij)}{ij} = \frac{ \frac{\varphi(i)}{i} \frac{\varphi(j)}{j} }{\frac{\varphi(\gcd(i, j))}{\gcd(i, j)}} \]

莫比乌斯函数(积性)

莫比乌斯函数\(\mu(n)\),将 \(n\) 唯一分解成 \(n = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\),则莫比乌斯函数有如下的定义

\[\mu(n) = \begin{cases} (-1)^t & \forall i\in[1, t], c_i = 1 \\ 0 & \exist i\in[1, t], c_i > 1 \end{cases} \]

特别地,\(\mu(1) = 1\)

线性筛筛积性函数

对于积性函数 \(f(x)\),若质数幂的函数值 \(f(p^c)\) 便于求出。则 \(f(x)\) 的前 \(n\) 项函数值可以通过线性筛得出。

具体地,记 \(\mathrm{low}_i\) 表示:设 \(i\) 的最小质因子为 \(p_1\),其次数为 \(c_1\),则 \(\mathrm{low}_i = p_1^{c_1}\)

在线性筛的过程中,额外进行如下的维护

  • 维护 \(\mathrm{low}_i\) 数组。
  • 每当筛出一个质数 \(p\),就将 \(f(p), f(p^2), \cdots, f(p^c)\) 全都求出来。
  • 其余满足 \(i \neq\mathrm{low}_i\)\(i\),使用递推式 \(f(i) = f(\mathrm{low}_i) \times f\left(\frac{i}{\mathrm{low}_i}\right)\) 计算函数值。

0x34 线性筛筛积性函数.cpp

int primeCount, prime[MaxV], low[MaxV];
int f[MaxV];

/* 线性筛筛积性函数 */
void sieve(const int &n) {
    f[1] = 1;
    for (int i = 2; i <= n; i ++) {
        if (!low[i]) {
            prime[++ primeCount] = i, low[i] = i;
            for (s64 v = i, c = 1; v <= n; v *= i, c ++) {
                // 计算 f[v]
            }
        }
        for (int j = 1; j <= primeCount; j ++) {
            if (prime[j] > n / i) break;
            low[i * prime[j]] = i % prime[j] ? prime[j] : low[i] * prime[j];
            if (i % prime[j] == 0) {
                break;
            }
        }
        if (low[i] < i) {
            f[i] = f[low[i]] * f[i / low[i]];
        }
    }
}

对于一些简单的积性函数(例如 \(\varphi, \mu\))就有更简单的筛法,不一定需要按照上述流程维护。

0x34 线性筛筛欧拉函数.cpp

int primeCount, prime[MaxV], fac[MaxV];
int phi[MaxV];

/* 线性筛筛欧拉函数 */
void sieve(const int &n) {
    phi[1] = 1;
    for (int i = 2; i <= n; i ++) {
        if (!fac[i]) {
            prime[++ primeCount] = i, fac[i] = i;
            phi[i] = i - 1;
        }
        for (int j = 1; j <= primeCount; j ++) {
            if (prime[j] > fac[i] || prime[j] > n / i) break;
            fac[i * prime[j]] = prime[j];
            phi[i * prime[j]] = phi[i] * (i % prime[j] ? prime[j] - 1 : prime[j]);
        }
    }
}

0x34 线性筛筛莫比乌斯函数.cpp

int primeCount, prime[MaxV], fac[MaxV];
int mu[MaxV];

/* 线性筛筛莫比乌斯函数 */
void sieve(const int &n) {
    mu[1] = 1;
    for (int i = 2; i <= n; i ++) {
        if (!fac[i]) {
            prime[++ primeCount] = i, fac[i] = i;
            mu[i] = -1;
        }
        for (int j = 1; j <= primeCount; j ++) {
            if (prime[j] > fac[i] || prime[j] > n / i) break;
            fac[i * prime[j]] = prime[j];
            mu[i * prime[j]] = mu[i] * (i % prime[j] ? -1 : 0);
        }
    }
}

迪利克雷卷积

迪利克雷卷积:对于两个数论函数 \(f, g\),定义 \(f, g\) 的迪利克雷卷积为

\[(f \ast g)(x) = \sum_{d \mid x} f(d)g\left( \frac{x}{d} \right) \]

迪利克雷卷积的简单性质

  • 交换律:\(f \ast g = g \ast f\)
  • 结合律:\((f \ast g) \ast h = f \ast (g \ast h)\)
  • 分配律:\(f \ast (g + h) = f \ast g + f \ast h\)
  • 单位元:\(f \ast \epsilon = \epsilon \ast f = f\),因此 \(\epsilon\) 又被称为迪利克雷卷积的单位元。

常见迪利克雷卷积式

式 1

\[\boldsymbol{\epsilon = \mu \ast 1 \iff \sum_{d \mid n} \mu(d) = [n = 1]} \]

简单推导:由积性函数的简单性质可知,关于 \(n\) 的函数 \(f(n) = \sum_{d \mid n} \mu(d)\) 为积性函数。

  • \(n = 1\) 时,等式成立。
  • \(n > 1\) 时,将 \(n\) 唯一分解成 \(n = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\),则有

\[\begin{aligned} f(n) & = \prod_{i = 1}^t f(p_i^{c_i}) \\ & = \prod_{i = 1}^t \left( \sum_{j = 0}^{c_i} \mu(p_i^j) \right) \\ & = \prod_{i = 1}^t \left( \mu(1) + \mu(p_i) \right) \\ & = 0 \end{aligned} \]

综上,\(\sum_{d \mid n} \mu(d) = [n = 1]\)

式 2

\[\boldsymbol{\mathrm{ID} = \varphi \ast 1 \iff \sum_{d \mid n} \varphi(d) = n} \]

简单推导:由积性函数的简单性质可知,关于 \(n\) 的函数 \(f(n) = \sum_{d \mid n} \varphi(d)\) 为积性函数。

  • \(n = 1\) 时,等式成立。
  • \(n > 1\) 时,将 \(n\) 唯一分解成 \(n = p_1^{c_1}p_2^{c_2}\cdots p_t^{c_t}\),则有

\[\begin{aligned} f(n) & = \prod_{i = 1}^t f(p_i^{c_i}) \\ & = \prod_{i = 1}^t \left( \sum_{j = 0}^{c_i} \varphi(p_i^j) \right) \\ & = \prod_{i = 1}^t \left( 1 + \sum_{j = 1}^{c_i}(p_i^{j} - p_i^{j - 1}) \right) \\ & = \prod_{i = 1}^t p_i^{c_i} \\ & = n \end{aligned} \]

综上,\(\sum_{d \mid n} \varphi(d) = n\)

式 3

\[\boldsymbol{\varphi = \mu \ast \mathrm{ID} \iff \varphi(n) = \sum_{d \mid n} d\mu\left( \frac{n}{d} \right)} \]

简单推导:

\[\begin{aligned} \varphi \ast 1 = \mathrm{ID} & \Longrightarrow \varphi \ast 1 \ast \mu = \mathrm{ID} \ast \mu \\ & \Longrightarrow \varphi \ast \epsilon = \mathrm{ID} \ast \mu \\ & \Longrightarrow \varphi = \mathrm{ID} \ast \mu \end{aligned} \]

杜教筛

杜教筛:用于 \(\mathcal{O}(n^{\frac{2}{3}})\) 时间内,求解一类数论函数 \(f(x)\) 的前缀和。

  • \(f(x)\) 为积性函数。
  • 存在辅助函数 \(g(x)\),满足函数 \(f \ast g\)\(g\) 的前缀和便于求出。

\(S(n) = \sum_{i = 1}^n f(i)\)。取杜教筛中的辅助函数 \(g(x)\),则有

\[\begin{aligned} \sum_{i = 1}^n (f \ast g)(i) & = \sum_{i = 1}^n \sum_{d \mid i} g(d)f\left( \frac{i}{d} \right) \\ & = \sum_{i = 1}^n g(i) \sum_{j = 1}^{\left\lfloor \frac{n}{i} \right\rfloor} f(j) \\ & = \sum_{i = 1}^n g(i) S\left( \left\lfloor \frac{n}{i} \right\rfloor \right) \end{aligned} \]

移项得

\[g(1)S(n) = \sum_{i = 1}^n (f \ast g)(i) - \sum_{i = 2}^n g(i)S\left( \left\lfloor \frac{n}{i} \right\rfloor \right) \]

此时得到了一个 \(S(n)\) 关于 \(S\left( \left\lfloor \frac{n}{i} \right\rfloor \right)\) 的递推式。若函数 \(f \ast g\)\(g\) 的前缀和便于求出,就可以使用数论分块处理该式。因此找到一个合适的辅助函数 \(g\) 是杜教筛的关键。

杜教筛的时间复杂度分析

由数论分块的性质,对于 \(m \in D_n\),有 \(D_m \subseteq D_n\)。于是我们需要使用记忆化保证杜教筛的复杂度,使用记忆化以后,只需要对所有的 \(k \in D_n\) 计算一次 \(S(k)\) 即可。

假设函数 \(f \ast g\)\(g\) 的前缀和均可 \(\mathcal{O}(1)\) 求出,设计算 \(S(n)\) 的时间复杂度为 \(T(n)\),则有

\[\begin{aligned} T(n) & = \sum\limits_{k \in D_n} T(k) \\ & = \mathcal{O}(\sqrt{n}) + \sum_{i = 1}^{\sqrt{n}}\mathcal{O}(\sqrt{i}) + \sum_{i = 2}^{\sqrt{n}} \mathcal{O}\left( \sqrt{\frac{n}{i}} \right) \\ & = \mathcal{O}\left( \int_0^{\sqrt{n}} \left( \sqrt{x} + \sqrt{\frac{n}{x}} \right) \mathrm{dx} \right) \\ & = \mathcal{O}(n^{\frac{3}{4}}) \end{aligned} \]

进一步优化,我们预处理出数论函数 \(f(x)\) 的前 \(m\) 项(其中 \(m \geq \sqrt{n}\)),并预处理出其前缀和。设预处理的时间复杂度为 \(T_0(m)\),则有

\[\begin{aligned} T(n) & = T_0(m) + \sum_{k \in D_n, k > m} T(k) \\ & = T_0(m) + \sum_{i = 1}^{\left\lfloor \frac{n}{m} \right\rfloor} \mathcal{O}\left(\sqrt{\frac{n}{i}}\right) \\ & = \mathcal{O}\left(T_0(m) + \int_0^{\frac{n}{m}} \sqrt{\frac{n}{x}} \mathrm{dx}\right) \\ & = \mathcal{O}\left(T_0(m) + \frac{n}{\sqrt{m}} \right) \end{aligned} \]

通常我们使用线性筛预处理 \(f(x)\) 的前 \(m\) 项,此时 \(T_0(m) = \mathcal{O}(m)\)。由均值不等式,取 \(m = \mathcal{O}(n^{\frac{2}{3}})\) 可得杜教筛的最优时间复杂度 \(\mathcal{O}(n^{\frac{2}{3}})\)

总结:预处理出 \(f(x)\) 的前 \(n^{\frac{2}{3}}\) 项及其前缀和,使用关于 \(S\left( \left\lfloor \frac{n}{i} \right\rfloor \right)\) 的递推式求解 \(S(n)\),并使用 std::map 记忆化。

0x34 杜教筛.cpp

/* 杜教筛 */
namespace xudyh {
    const int V = 1e6, MaxV = V + 10; // 预处理到的范围(需根据实际问题调整)

    int primeCount, prime[MaxV], low[MaxV];
    int f[MaxV];

    s64 fArr[MaxV];
    std::map<int, s64> fBuc;

    void sieve(const int &n = V) {
        f[1] = 1;
        for (int i = 2; i <= n; i ++) {
            if (!low[i]) {
                prime[++ primeCount] = i, low[i] = i;
                for (s64 v = i, c = 1; v <= n; v *= i, c ++) {
                    // 计算 f[v]
                }
            }
            for (int j = 1; j <= primeCount; j ++) {
                if (prime[j] > n / i) break;
                low[i * prime[j]] = i % prime[j] ? prime[j] : low[i] * prime[j];
                if (i % prime[j] == 0) {
                    break;
                }
            }
            if (low[i] < i) {
                f[i] = f[low[i]] * f[i / low[i]];
            }
        }

        for (int i = 1; i <= n; i ++) {
            fArr[i] = fArr[i - 1] + f[i];
        }
    }

    s64 fSum(int n) {
        if (n <= V) {
            return fArr[n];
        }
        if (fBuc.contains(n)) {
            return fBuc[n];
        }

        s64 ans = 0; // 计算函数 f*g 第 1~n 项的和
        for (int x = 2, nx; x <= n; x = nx + 1) {
            nx = n / (n / x);
            s64 sum = 0; // 计算函数 g 第 x~nx 项的和
            ans -= sum * fSum(n / x);
        }

        return fBuc[n] = ans;
    }
}

杜教筛求莫比乌斯函数与欧拉函数前缀和

给莫比乌斯函数选择的辅助函数为 \(1(n) = 1\),由于 \(\mu \ast 1 = \epsilon\),函数 \(\epsilon\)\(1\) 的前缀和均便于求出。代入杜教筛公式得

\[S(n) = 1 - \sum_{i = 2}^n S\left( \left\lfloor \frac{n}{i} \right\rfloor \right) \]

给欧拉函数选择的辅助函数为 \(1(n) = 1\),由于 \(\varphi \ast 1 = \mathrm{ID}\),函数 \(\mathrm{ID}\)\(1\) 的前缀和均便于求出。代入杜教筛公式得

\[S(n) = \frac{n(n + 1)}{2} - \sum_{i = 2}^n S\left( \left\lfloor \frac{n}{i} \right\rfloor \right) \]

求欧拉函数前缀和,更简便的方法是莫比乌斯反演,注意到 \(\sum_{i = 1}^n \varphi(i) = \sum_{1 \leq i \leq j \leq n} [\gcd(i, j) = 1]\)

然而 \(\sum_{i = 1}^n \sum_{j = 1}^n [\gcd(i, j) = 1] = \sum_{d = 1}^n \mu(d)\left\lfloor \frac{n}{d} \right\rfloor^2\),使用莫比乌斯函数前缀和配合数论分块处理该式即可。

0x34 杜教筛求莫比乌斯函数与欧拉函数前缀和.cpp

/* 杜教筛求莫比乌斯函数与欧拉函数前缀和 */
namespace xudyh {
    const int V = 1e6, MaxV = V + 10;

    int primeCount, prime[MaxV], fac[MaxV];
    int mu[MaxV];

    int muArr[MaxV];
    std::map<int, int> muBuc;

    void sieve(const int &n = V) {
        mu[1] = 1;
        for (int i = 2; i <= n; i ++) {
            if (!fac[i]) {
                prime[++ primeCount] = i, fac[i] = i;
                mu[i] = -1;
            }
            for (int j = 1; j <= primeCount; j ++) {
                if (prime[j] > fac[i] || prime[j] > n / i) break;
                fac[i * prime[j]] = prime[j];
                mu[i * prime[j]] = mu[i] * (i % prime[j] ? -1 : 0);
            }
        }

        for (int i = 1; i <= n; i ++) {
            muArr[i] = muArr[i - 1] + mu[i];
        }
    }

    int muSum(int n) {
        if (n <= V) {
            return muArr[n];
        }
        if (muBuc.contains(n)) {
            return muBuc[n];
        }

        int ans = 1;
        for (int x = 2, nx; x <= n; x = nx + 1) {
            nx = n / (n / x);
            ans -= (nx - x + 1) * muSum(n / x);
        }

        return muBuc[n] = ans;
    }

    s64 phiSum(int n) {
        s64 ans = 0;
        for (int x = 1, nx; x <= n; x = nx + 1) {
            nx = n / (n / x);
            ans += 1ll * (n / x) * (n / x) * (muSum(nx) - muSum(x - 1));
        }
        return (ans + 1) / 2;
    }
}

Min_25 筛

Min_25 筛:用于 \(\mathcal{O}(n^{1 - \epsilon})\) 时间内,求解一类数论函数 \(f(x)\) 的前缀和。

  • \(f(x)\) 为积性函数。
  • 对于 \(p \in \mathbb{P}\)\(f(p)\) 是关于 \(p\) 的低次多项式。
  • 对于 \(p \in \mathbb{P}\)\(f(p^c)\) 便于求出。

一些约定:

  • \(p_k\) 表示第 \(k\) 小的质数(特别地,\(p_0 = 1\))。
  • \(\mathrm{lpf}(i)\) 表示 \(i\) 的最小质因子。

\(F(n, k) = \sum_{i = 2}^n \left[p_k < \mathrm{lpf}(i)\right] f(i)\),记 \(F_{\mathrm{prime}}(n) = \sum_{2 \leq p \leq n, p \in \mathbb{P}} f(p)\)

现在的目标是求出 \(\sum_{i = 1}^n f(i) = F(n, 0) + f(1)\)

考虑求出 \(F(n, k)\),枚举 \(2 \sim n\) 每个数的最小质因子及其次数,以计算贡献

\[\begin{aligned} F(n, k) & = \sum_{i = 2}^n [p_k < \mathrm{lpf}(i)] f(i) \\ & = \sum_{\substack{k < i \\ p_i^2 \leq n}} \sum_{\substack{c \geq 1 \\ p_i^c \leq n}} f(p_i^c) \left( F\left( \left\lfloor \frac{n}{p_i^c} \right\rfloor, i + 1 \right) + [c > 1] \right) + \sum_{\substack{k < i \\ p_i \leq n}}f(p_i) \\ & = \sum_{\substack{k < i \\ p_i^2 \leq n}} \sum_{\substack{c \geq 1 \\ p_i^c \leq n}} f(p_i^c) \left( F\left( \left\lfloor \frac{n}{p_i^c} \right\rfloor, i + 1 \right) + [c > 1] \right) + F_{\mathrm{prime}}(n) - F_{\mathrm{prime}}(p_k) \end{aligned} \]

边界情况:当 \(p_k > n\) 时,\(F(n, k) = 0\)

考虑求出 \(F_{\mathrm{prime}}(n)\),根据 \(F(n, k)\) 的递推式,我们发现只有 \(x \in D_n\) 处的点值 \(F_{\mathrm{prime}}(x)\) 是需要的。

对于 \(p \in \mathbb{P}\)\(f(p)\) 是关于 \(p\) 的低次多项式。可以表示成 \(f(p) = \sum a_ip^i\),那么可以分开考虑每个 \(p^i\) 的贡献,其对 \(F_{\mathrm{prime}}(n)\) 的贡献为 \(a_i\left( \sum_{2 \leq p \leq n, p \in \mathbb{P}} p^i \right)\)

对于一个固定的次数 \(c\),记 \(G(n, k) = \sum_{i = 2}^n \left[p_k < \mathrm{lpf}(i) \lor i \in \mathbb{P}\right] i^c\)

现在的目标是,对于所有 \(x \in D_n\) 求出 \(G(x, \lambda)\),其中 \(p_{\lambda}\) 是最后一个不超过 \(\sqrt{n}\) 的质数。

考虑求出 \(G(n, k)\),考虑如何从 \(G(n, k - 1)\) 转移到 \(G(n, k)\),相当于是要去掉 \(2 \sim n\) 中满足 \(\mathrm{lpf}(i) = p_k\) 的合数 \(i\) 的贡献。先提取出一个 \(p_k\),剩下数的范围不超过 \(\left\lfloor \frac{n}{p_k} \right\rfloor\)。于是剩下数的贡献即为 \(G\left( \left\lfloor \frac{n}{p_k} \right\rfloor, k - 1 \right)\),但此时我们会把 \([1, p_{k - 1}]\) 范围内的质数贡献给扣除掉,所以还要补回这些质数的贡献 \(G(p_{k - 1}, k - 1)\)

\[G(n, k) = G(n, k - 1) - \left[ p_k^2 \leq n \right] p_k^c \left( G\left( \left\lfloor \frac{n}{p_k} \right\rfloor, k - 1 \right) - G(p_{k - 1}, k - 1) \right) \]

注意到式子中有一项 \(G(p_{k - 1}, k - 1)\),即质数处的点值。由于此时 \(p_{k - 1} < \lfloor \sqrt{n} \rfloor\),可以证明 \(p_{k - 1} \in D_n\)。于是质数处的点值也会被处理出来。

引理:对于任意 \(1 \leq k < \lfloor \sqrt{n} \rfloor\),均有 \(k \in D_n\)

边界情况:\(G(n, 0) = \sum_{i = 2}^n i^c\),一般可以根据公式快速算出。

0x34 Min_25 筛.cpp

// 以求解函数 f(p^c) = p^c(p^c - 1) 为例,注意 n >= 1e10 情况下的取模与 long long 问题
/* Min_25 筛 */
namespace Min_25 {
    const int V = 1e5, MaxV = V + 10;

    int primeCount, prime[MaxV], fac[MaxV];
    void sieve(const int &n = V) {
        for (int i = 2; i <= n; i ++) {
            if (!fac[i]) {
                prime[++ primeCount] = i, fac[i] = i;
            }
            for (int j = 1; j <= primeCount; j ++) {
                if (prime[j] > fac[i] || prime[j] > n / i) break;
                fac[i * prime[j]] = prime[j];
            }
        }
    }

    constexpr int inv6 = qpow(6, mod - 2, mod);

    int S1(int n) {
        return (1ll * n * (n + 1) / 2) % mod;
    }
    int S2(int n) {
        return 1ll * n * (n + 1) % mod * (2 * n + 1) % mod * inv6 % mod;
    }

    int fSum(int n) {
        int b = sqrt(n);
        int t = std::upper_bound(prime + 1, prime + 1 + primeCount, b) - prime - 1;

        std::vector<int> id1(b + 1), id2(b + 1);
        auto id = [&] (int x) {
            return x <= b ? id1[x] : id2[n / x];
        };

        std::vector<int> seq(1); // 集合 D_n,数值按照从大到小排序
        std::vector<int> g1(1), g2(1);

        for (int x = 1, nx; x <= n; x = nx + 1) {
            nx = n / (n / x);

            int v = n / x;
            if (v <= b) {
                id1[v] = seq.size();
            } else {
                id2[n / v] = seq.size();
            }

            seq.push_back(v);
            g1.push_back(norm(S1(v) - 1));
            g2.push_back(norm(S2(v) - 1));
        }

        for (int j = 1; j <= t; j ++) {
            int p = prime[j];
            for (int i = 1; i < seq.size(); i ++) {
                if (1ll * p * p > seq[i]) {
                    break;
                }
                int x1 = id(seq[i] / p), x2 = id(prime[j - 1]);
                dec(g1[i], 1ll * p * (g1[x1] - g1[x2] + mod) % mod);
                dec(g2[i], 1ll * p * p % mod * (g2[x1] - g2[x2] + mod) % mod);
            }
        }

        std::function<int(int, int)> solve = [&] (int n, int j) {
            if (prime[j] > n) {
                return 0;
            }
            int x1 = id(n), x2 = id(prime[j]);
            int ans = norm((g2[x1] - g1[x1]) - (g2[x2] - g1[x2]));
            for (int i = j + 1; i <= t; i ++) {
                int p = prime[i];
                if (1ll * p * p > n) {
                    break;
                }
                for (s64 v = p; v <= n; v *= p) {
                    add(ans, 1ll * v * (v - 1) % mod * (solve(n / v, i) + (v > p)) % mod);
                }
            }
            return ans;
        };
        return (solve(n, 0) + 1) % mod;
    }
}

Min_25 筛求前缀质数个数:相当于是求出 \(2 \sim n\) 中所有质数的 \(0\) 次方,仅使用 Min_25 筛前半部分的求质数贡献即可。

0x34 Min_25 筛求前缀质数个数.cpp

/* Min_25 筛求前缀质数个数 */
namespace Min_25 {
    const int V = 1e5, MaxV = V + 10;

    int primeCount, prime[MaxV], fac[MaxV];
    void sieve(const int &n = V) {
        for (int i = 2; i <= n; i ++) {
            if (!fac[i]) {
                prime[++ primeCount] = i, fac[i] = i;
            }
            for (int j = 1; j <= primeCount; j ++) {
                if (prime[j] > fac[i] || prime[j] > n / i) break;
                fac[i * prime[j]] = prime[j];
            }
        }
    }

    int fSum(int n) {
        int b = sqrt(n);
        int t = std::upper_bound(prime + 1, prime + 1 + primeCount, b) - prime - 1;

        std::vector<int> id1(b + 1), id2(b + 1);
        auto id = [&] (int x) {
            return x <= b ? id1[x] : id2[n / x];
        };

        std::vector<int> seq(1); // 集合 D_n,数值按照从大到小排序
        std::vector<int> g(1);

        for (int x = 1, nx; x <= n; x = nx + 1) {
            nx = n / (n / x);

            int v = n / x;
            if (v <= b) {
                id1[v] = seq.size();
            } else {
                id2[n / v] = seq.size();
            }

            seq.push_back(v);
            g.push_back(v - 1);
        }

        for (int j = 1; j <= t; j ++) {
            int p = prime[j];
            for (int i = 1; i < seq.size(); i ++) {
                if (1ll * p * p > seq[i]) {
                    break;
                }
                int x1 = id(seq[i] / p), x2 = id(prime[j - 1]);
                g[i] -= g[x1] - g[x2];
            }
        }
        return g[id(n)];
    }
}

同余

带余除法:对于任意 \(n, m \in \Z\)\(m \neq 0\)存在唯一的 \(q, r \in \Z\),使得 \(n = qm + r\)\(0 \leq r < |m|\))。

这里记 \(q = \left\lfloor \frac{n}{m} \right\rfloor\)\(r = n \bmod m\)

同余:若 \(a \bmod m = b \bmod m\),则称 \(a, b\)\(m\) 同余,记作 \(a \equiv b \pmod m\)

同余的简单性质

  • \(a \equiv b \pmod m, c \equiv d \pmod m\),则
    • \(a + c \equiv b + d \pmod m\)
    • \(ac \equiv bd \pmod m\)
  • \(ac \equiv bc \pmod m \Longrightarrow a \equiv b \pmod{\frac{m}{\gcd(c, m)}}\)

同余类:集合 \(\overline{a} = \{a + km \mid k \in \Z\}\)(其中 \(a\in [0, m - 1]\))中的所有数模 \(m\) 同余,余数均为 \(a\),该集合称为一个模 \(m\) 的同余类。

(完全)剩余系\(m\) 的完全剩余系包含 \(m\) 个元素 \(\overline{0}, \overline{1}, \cdots, \overline{m - 1}\)

(简化)剩余系\(m\) 的简化剩余系包含 \(\varphi(m)\) 个元素,\(1 \sim m\) 中与 \(m\) 互质的数代表的同余类构成该剩余系。

剩余系的简单性质

  • 对于 \(i = 0, 1, \cdots, m - 1\)\(ia \bmod m\) 两两不同当且仅当 \(\gcd(a, m) = 1\)
  • 对于与 \(m\) 互质的整数 \(i, j\)\(ij \bmod m\) 仍然与 \(m\) 互质。

费马小定理与欧拉定理

费马小定理:设 \(p\) 是质数,对于任意整数 \(a\)\(a\) 不能是 \(p\) 的倍数),满足 \(a^{p - 1} \equiv 1 \pmod p\)

另一个形式:设 \(p\) 是质数。对于任意整数 \(a\),满足 \(a^p \equiv a \pmod p\)

简单推导:由于 \(p\) 是质数且 \(p \nmid a\),对于 \(i = 0, 1, \cdots, p - 1\)\(ia \bmod p\) 两两不同。

因此 \(\{ ia \bmod p \}\) 也构成 \(0 \sim p - 1\) 的排列。所以

\[\prod_{i = 1}^{p - 1}(ia \bmod p) = \prod_{i = 1}^{p - 1} i \]

又由于

\[\prod_{i = 1}^{p - 1}ia \equiv a^{p - 1}\prod_{i = 1}^{p - 1} i \pmod p \]

所以

\[(a^{p - 1} - 1)\prod_{i = 1}^{p - 1}i \equiv 0 \pmod p \]

由于 \(1, 2, \cdots, p - 1\) 均不是 \(p\) 的倍数,所以 \(a^{p - 1} - 1\)\(p\) 的倍数。即 \(a^{p - 1}\equiv 1 \pmod p\)

By the way:费马小定理的逆命题并不成立。

即使对于所有与 \(p\) 互质的整数 \(a\) 均满足 \(a^{p - 1} \equiv 1 \pmod p\)\(p\) 也不一定是质数。

欧拉定理:设整数 \(m > 0\),对于任意满足 \(\gcd(a, m) = 1\) 的整数 \(a\),满足 \(a^{\varphi(m)} \equiv 1 \pmod m\)

简单推导:设 \(R\) 表示 \(1 \sim m\) 中与 \(m\) 互质的数构成的集合。已知 \(a\in R\),对于所有 \(R\) 中的元素 \(r\)\(ra \bmod m\) 仍然属于 \(R\) 且两两不同。所以

\[\prod_{r \in R} (ra \bmod m) = \prod_{r \in R} r \]

又由于

\[\prod_{r \in R} ra \equiv a^{\varphi(m)} \prod_{r \in R} r \pmod m \]

所以

\[(a^{\varphi(m)} - 1) \prod_{r \in R} r \equiv 0 \pmod m \]

由于 \(r \in R\) 均不是 \(m\) 的倍数,所以 \(a^{\varphi(m)} - 1\)\(m\) 的倍数。即 \(a^{\varphi(m)} \equiv 1 \pmod m\)

扩展欧拉定理:设整数 \(m > 0\),对于任意整数 \(a, b(b \geq 0)\),有

\[a^b \equiv \begin{cases} a^{b \bmod \varphi(m)} & \gcd(a, m) = 1 \\ a^b & \gcd(a, m) \neq 1, b < \varphi(m) \\ a^{b \bmod \varphi(m) + \varphi(m)} & \gcd(a, m) \neq 1, b \geq \varphi(m) \end{cases} \pmod m \]

直观理解:考虑 \(a^b \bmod m\) 的循环情况。当 \(\gcd(a, m) = 1\) 时,该循环为纯循环;当 \(\gcd(a, m) \neq 1\) 时,该循环为混循环(类似 \(\rho\) 形),需要对尾巴和环形分别讨论。

通常使用扩展欧拉定理,以达到降幂的效果。

模意义下逆元

模意义下逆元:对于非零整数 \(a, m\),如果存在整数 \(b\) 使得 \(ab \equiv 1 \pmod m\),则称 \(b\)\(a\) 在模 \(m\) 意义下的逆元。记作 \(a^{-1} \bmod p\)

模质数意义下逆元

当模数 \(p\) 为质数时。对于任意整数 \(a\)\(a\) 不能是 \(p\) 的倍数),有

\[a \times a^{p - 2} \equiv 1 \pmod p \]

此时,\(a^{p - 2} \bmod p\) 即为 \(a\) 在模 \(p\) 意义下的逆元。

单个逆元的求解

若要求解 \(a\) 在模 \(m\) 意义下的逆元,相当于是要求解线性同余方程 \(ax \equiv 1 \pmod m\),使用 exgcd 即可。

时间复杂度 \(\mathcal{O}(\log V)\)

0x34 单个逆元的求解.cpp

/* 单个逆元的求解 */
s64 inverse(s64 a, s64 m) {
    auto [d, x, y] = exgcd(a, m);
    // assert(d == 1);
    return (x % m + m) % m;
}

多个逆元的求解

若要求解多个整数 \(a_1, \cdots, a_n\) 在模 \(m\) 意义下的逆元,处理出前缀积与前缀积逆元,通过 \(a^{-1}_i = S_{i - 1}S^{-1}_i \bmod m\) 求出所有数的逆元即可。

时间复杂度 \(\mathcal{O}(n + \log V)\)

0x34 多个逆元的求解.cpp

/* 多个逆元的求解 */
auto inverse(std::vector<int> a, int mod) {
    int n = a.size(), x;
    std::vector<int> res(n);
    
    x = 1;
    for (int i = 0; i < n; i ++) {
        res[i] = x;
        x = 1ll * x * a[i] % mod;
    }
    x = qpow(x, mod - 2, mod); // 模数为质数时可用
    for (int i = n - 1; i >= 0; i --) {
        res[i] = 1ll * res[i] * x % mod;
        x = 1ll * x * a[i] % mod;
    }

    return res;
}

预处理线性逆元

若要求解 \(1 \sim n\) 所有整数模质数 \(p\) 意义下的逆元。对于 \(1 < i < p\),有

\[0 \equiv \left\lfloor \frac{p}{i} \right\rfloor i + (p \bmod i) \pmod p \]

将等式两边同乘以 \(i^{-1}(p \bmod i)^{-1}\),得

\[i^{-1} \equiv -\left\lfloor \frac{p}{i} \right\rfloor (p \bmod i)^{-1} \pmod p \]

根据上式递推即可。

时间复杂度 \(\mathcal{O}(n)\)

0x34 预处理线性逆元.cpp

std::vector<int> v;

/* 预处理线性逆元 */
void linearInverse(const int &n) {
    v.resize(n + 1);
    v[1] = 1;
    for (int i = 2; i <= n; i ++) {
        v[i] = 1ll * v[mod % i] * (mod - mod / i) % mod;
    }
}

线性同余方程

裴蜀定理:设 \(a, b\) 是不均为 \(0\) 的整数。

  • 存在整数 \(x, y\),使得 \(ax + by = \gcd(a, b)\)
  • 对于任意整数 \(x, y\),均有 \(\gcd(a, b) \mid ax + by\)

裴蜀定理可以扩展到更多维:设 \(a_1, \cdots, a_n\) 是不均为 \(0\) 的整数。

  • 存在整数 \(x_1, \cdots, x_n\),使得 \(\sum_{i = 1}^n a_ix_i = \gcd(a_1, \cdots, a_n)\)
  • 对于任意整数 \(x_1, \cdots, x_n\),均有 \(\gcd(a_1, \cdots, a_n) \mid \sum_{i = 1}^n a_ix_i\)

可以使用 gcd 的结合律进行归纳证明。

exgcd(扩展欧几里得算法):用于求出二元一次不定方程 \(ax + by = \gcd(a, b)\) 的一组可行解 \(x, y\)

在欧几里得算法的基础上归纳求解:

  • \(b = 0\) 时。此时有 \(x = 1, y = 0\),使得 \(a \cdot 1 + 0 \cdot 0 = \gcd(a, 0)\)
  • \(b > 0\) 时。由于 \(\gcd(a, b) = \gcd(b, a \bmod b)\),假设存在一对 \(x, y\) 使得

\[bx + (a \bmod b)y = \gcd(b, a \bmod b) \]

利用 \(a \bmod b = a - \left\lfloor \frac{a}{b} \right\rfloor b\),并移项得

\[ay + b\left(x - \left\lfloor \frac{a}{b} \right\rfloor y\right) = \gcd(a, b) \]

此时取 \(x' = y, y' = x - \left\lfloor \frac{a}{b} \right\rfloor y\),使得 \(ax' + by' = \gcd(a, b)\)

时间复杂度 \(\mathcal{O}(\log \max(a, b))\)

可以证明,扩展欧几里得算法求出的可行解 \(x, y\) 满足 \(|x| \leq b, |y| \leq a\)

0x34 exgcd(扩展欧几里得算法).cpp

/* 扩展欧几里得算法 */
std::array<s64, 3> exgcd(s64 a, s64 b) {
    if (b == 0) return {a, 1, 0};
    auto [d, x, y] = exgcd(b, a % b);
    return {d, y, x - (a / b) * y};
}

二元一次不定方程 \(ax + by = c\)

  • 有解性:当且仅当 \(\gcd(a, b) \mid c\)
  • 通解:使用 exgcd 找出方程 \(ax + by = \gcd(a, b)\) 的一组特解 \(x_0, y_0\),则有通解

\[x = \frac{c}{\gcd(a, b)}x_0 + k\frac{b}{\gcd(a, b)} \\ y = \frac{c}{\gcd(a, b)}y_0 - k\frac{a}{\gcd(a, b)} \]

线性同余方程 \(ax \equiv b \pmod m\)

该方程等价于 \(ax - b\)\(m\) 的倍数,不妨设为 \(-y\) 倍,则原方程转化为 \(ax + my = b\)

  • 有解性:当且仅当 \(\gcd(a, m) \mid b\)
  • 通解:使用 exgcd 找出方程 \(ax + my = \gcd(a, m)\) 的一组特解 \(x_0, y_0\),则有通解

\[x = \frac{b}{\gcd(a, m)}x_0 + k\frac{m}{\gcd(a, m)} \]

将线性同余方程中 \(x\) 的系数消成 \(1\)

对于线性同余方程 \(ax \equiv b \pmod m\),有解的充要条件是 \(\gcd(a, m) \mid b\)。当满足该条件时,方程可以转化为

\[\frac{a}{\gcd(a, m)}x \equiv \frac{b}{\gcd(a, m)} \pmod{\frac{m}{\gcd(a, m)}} \]

此时 \(\gcd\left( \frac{a}{\gcd(a, m)}, \frac{m}{\gcd(a, m)} \right) = 1\)\(x\) 在模 \(\frac{m}{\gcd(a, m)}\) 意义下有唯一解 \(x'\)

于是线性同余方程 \(ax \equiv b \pmod m\) 转化为了 \(x \equiv x' \pmod{m'}\) 的形式。该形式对线性同余方程组的求解大有用处。

线性同余方程组

模数两两互质的一元线性同余方程组(中国剩余定理,CRT)

\[\begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \cdots \\ x \equiv a_n \pmod{m_n} \end{cases} \]

  • 记所有模数的积为 \(M = \prod_{i = 1}^n m_i\)
  • 对于第 \(i\) 个方程:
    • \(M_i = \frac{M}{m_i}\)
    • \(V_i\) 表示 \(M_i\) 在模 \(m_i\) 意义下的逆元。
  • 特解:

\[x_0 = \sum_{i = 1}^n a_iM_iV_i \]

  • 通解:

\[x = x_0 + kM \]

0x34 CRT(中国剩余定理).cpp

// {a, m} 表示方程 x = a (mod m)
using Equ = std::pair<s64, s64>;

/* 中国剩余定理 */
Equ CRT(std::vector<Equ> seq) {
    auto inverse = [&] (s64 a, s64 m) -> s64 {
        auto [d, x, y] = exgcd(a, m);
        return (x % m + m) % m;
    };

    s64 M = 1;
    for (auto [a, m] : seq) {
        M *= m;
    }
    s64 x = 0;
    for (auto [a, m] : seq) {
        s64 Mi = M / m;
        s64 Vi = inverse(Mi, m);
        x = (x + a * Mi * Vi) % M; // 必要时,使用“快速乘”防止爆 long long
    }
    x = (x % M + M) % M;
    return {x, M};
}

不保证模数两两互质的一元线性同余方程组(扩展中国剩余定理,exCRT)

仅考虑两个方程的合并(多个方程的情况,可以依次合并)。设这两个方程为

\[\begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \end{cases} \]

\[x = pm_1 + a_1 = qm_2 + a_2 \Longrightarrow m_1p - m_2q = a_2 - a_1 \]

使用 exgcd 找出方程 \(m_1p - m_2q = a_2 - a_1\) 的一组特解 \(p_0, q_0\)

此时取 \(a' = p_0m_1 + a_1\)\(m' = \operatorname{lcm}(m_1, m_2)\),两个方程合并为

\[x \equiv a' \pmod{m'} \]

0x34 exCRT(扩展中国剩余定理).cpp

// {a, m} 表示方程 x = a (mod m)
using Equ = std::pair<s64, s64>;

/* 扩展中国剩余定理 */
Equ exCRT(std::vector<Equ> seq) {
    auto [a1, m1] = seq[0];
    for (int i = 1; i < seq.size(); i ++) {
        auto [a2, m2] = seq[i];
        auto [d, x, y] = exgcd(m1, m2);
        if ((a2 - a1) % d) {
            return {-1, -1};
        }
        x = x * ((a2 - a1) / d) % (m2 / d); // 必要时,使用“快速乘”防止爆 long long
        s64 m = m1 * (m2 / d);
        s64 a = (x * m1 + a1) % m; // 必要时,使用“快速乘”防止爆 long long
        a1 = a, m1 = m;
    }
    a1 = (a1 % m1 + m1) % m1;
    return {a1, m1};
}

阶与原根

:设整数 \(m > 0\),满足 \(a^x \equiv 1 \pmod m\) 的最小正整数 \(x\),称作 \(a\)\(m\) 的阶。记作 \(\mathrm{ord}_m(a)\)

阶的存在条件当且仅当 \(\gcd(a, m) = 1\)\(a\)\(m\) 的阶存在。

必要性:当 \(\gcd(a, m) \neq 1\) 时,\(a\) 在模 \(m\) 意义下的逆元不存在。故必定有 \(\gcd(a, m) = 1\)

充分性:当 \(\gcd(a, m) = 1\) 时,由欧拉定理 \(a^{\varphi(m)} \equiv 1 \pmod m\),故此时阶必定存在,并且 \(\mathrm{ord}_m(a) \mid \varphi(m)\)

阶的简单性质

  • \(a\perp m\),则 \(a, a^2, \cdots, a^{\mathrm{ord}_m(a)}\)\(m\) 两两不同。
  • \(a \perp m\),则 \(\mathrm{ord}_m(a) \mid \varphi(m)\)
  • \(a \perp m\),则

\[\mathrm{ord}_m(a^k) = \frac{\mathrm{ord}_m(a)}{\gcd(\mathrm{ord}_m(a), k)} \]

简单推导:\(\mathrm{ord}_m(a^k) = \frac{\mathrm{lcm}(\mathrm{ord}_m(a), k)}{k}\)

  • \(a\perp m, b\perp m\),则

\[\frac{\mathrm{lcm}(\mathrm{ord}_m(a), \mathrm{ord}_m(b))}{\gcd(\mathrm{ord}_m(a), \mathrm{ord}_m(b))} \mid \mathrm{ord}_m(ab) \mid \mathrm{lcm}(\mathrm{ord}_m(a), \mathrm{ord}_m(b)) \]

  • \(a \perp m, b \perp m\),则

\[\mathrm{ord}_m(ab) = \mathrm{ord}_m(a)\mathrm{ord}_m(b) \iff \gcd(\mathrm{ord}_m(a), \mathrm{ord}_m(b)) = 1 \]

  • \(a \perp m, b\perp m\),则总是存在满足 \(c\perp m\)\(c\),使得

\[\mathrm{ord}_m(c) = \mathrm{lcm}(\mathrm{ord}_m(a), \mathrm{ord}_m(b)) \]

简单构造:将 \(\mathrm{ord}_m(a), \mathrm{ord}_m(b)\) 唯一分解

\[\mathrm{ord}_m(a) = \prod p_i^{s_i}, \quad \mathrm{ord}_m(b) = \prod p_i^{t_i} \]

根据 \(s_i\)\(t_i\) 的大小关系,可以将所有质因子分成两类

\[A = \{ s_i \geq t_i \mid i \}, \quad B = \{ s_i < t_i \mid i \} \]

\[x_A = \prod_{i \in A} p_i^{s_i}, \quad x_B = \prod_{i \in B} p_i^{s_i}, \quad y_A = \prod_{i \in A} p_i^{t_i}, \quad y_B = \prod_{i \in B} p_i^{t_i} \]

此时 \(\mathrm{ord}_m(a) = x_Ax_B, \mathrm{ord}_m(b) = y_Ay_B\),可得

\[\mathrm{ord}_m(a^{x_B}) = x_A, \quad \mathrm{ord}_m(b^{y_A}) = y_B \]

又由于 \(x_A \perp y_B\),所以

\[\mathrm{ord}_m(a^{x_B}b^{y_A}) = x_Ay_B = \prod p_i^{\max(s_i, t_i)} = \mathrm{lcm}(\mathrm{ord}_m(a), \mathrm{ord}_m(b)) \]

\(c = a^{x_B}b^{y_A}\) 即可。

原根:设整数 \(m > 0\),若 \(\mathrm{ord}_m(g) = \varphi(m)\),则称 \(g\) 为模 \(m\) 的原根。

此时 \(g, g^2, \cdots, g^{\varphi(m)}\) 构成了 \(m\) 的简化剩余系

原根判定定理:设整数 \(m \geq 3\),对于整数 \(g \perp m\),若 \(g\) 是模 \(m\) 的原根,当且仅当对于 \(\varphi(m)\) 的每个质因子 \(p\),都有

\[g^{\frac{\varphi(m)}{p}} \not\equiv 1 \pmod m \]

原根存在定理\(m\) 存在原根,当且仅当 \(m = 1, 2, 4, p^c, 2p^c\)(其中 \(p\) 为奇质数且 \(c \geq 1\))。

阶的数量:若 \(m\) 存在原根,则对于 \(d \mid \varphi(m)\),阶等于 \(d\) 的元素恰好有 \(\varphi(d)\) 个。

简单推导:设 \(m\) 的原根为 \(g\),则 \(m\) 的简化剩余系中的元素可以写成 \(g^k \bmod m\) 的形式,其中 \(1 \leq k \leq \varphi(m)\)。则

\[\mathrm{ord}_m(g^k) = \frac{\varphi(m)}{\gcd(\varphi(m), k)} \]

\(\mathrm{ord}_m(g^k) = d\),则有 \(\gcd(\varphi(m), k) = \frac{\varphi(m)}{d}\)。整个式子除以 \(\frac{\varphi(m)}{d}\) 可得 \(\gcd(d, k') = 1\),此时 \(k'\) 的数量为 \(\varphi(d)\)

原根数量:若 \(m\) 存在原根,则 \(m\) 的原根个数为 \(\varphi(\varphi(m))\)

相当于是阶等于 \(\varphi(m)\) 的元素个数,由上述分析,恰好有 \(\varphi(\varphi(m))\) 个。

最小原根的上界:若 \(m\) 存在原根,则 \(m\) 的最小原根的上界为 \(m^{0.25}\) 级别。

原根求解:先求出 \(\varphi(m)\),再求出 \(\varphi(m)\) 的唯一分解形式。此时就可以根据 "原根判定定理" 配合快速幂判断一个数是否为 \(m\) 的原根。

从小到大暴力寻找原根,即可找到最小原根 \(g\)

当我们已知其中一个原根 \(g\) 时,可以根据 \(g\) 生成其他所有的原根。对于所有满足 \(k \perp \varphi(m)\)\(k\),这些 \(g^k \bmod m\) 不重不漏地覆盖了所有原根。

离散对数

离散对数:设整数 \(m > 0\) 存在原根 \(g\),对于 \(m\) 的简化剩余系中的元素 \(a\),必存在唯一的整数 \(0 \leq k < \varphi(m)\) 使得 \(g^k \equiv a \pmod m\)

此时称 \(k\) 为以 \(g\) 为底,模 \(m\) 的离散对数。记作 \(k = \mathrm{ind}_g(a)\)

离散对数的简单性质

  • \(\mathrm{ind}_g(ab) \equiv \mathrm{ind}_g(a) + \mathrm{ind}_g(b) \pmod{\varphi(m)}\)
    说明离散对数可以将 "模 \(m\) 意义下乘法" 转换成 "模 \(\varphi(m)\) 意义下加法"。
  • \(\mathrm{ind}_g(a^k) \equiv k\operatorname{ind}_g(a) \pmod{\varphi(m)}\)
  • \(\mathrm{ind}_{g_1}(a) \equiv \operatorname{ind}_{g_2}(a) \operatorname{ind}_{g_1}(g_2) \pmod{\varphi(m)}\)。其中 \(g_1, g_2\) 均为 \(m\) 的原根。

高次同余方程

固定底数且与模数互质的高次同余方程 \(a^x \equiv b \pmod{p}\)(大步小步算法,BSGS)

\(S = \left\lceil \sqrt{p} \right\rceil\),设 \(m = iS - j\)(其中 \(i \geq 1\)\(0 \leq j < S\)),则有

\[a^{iS - j} \equiv b \pmod p \\ a^{iS} \times a^{-j} \equiv b \pmod p \\ a^{iS} \equiv b \times a^j \pmod p \]

预处理 \(0 \leq j < S\)\(b\times a^j \bmod p\) 存入哈希表,枚举所有 \(1 \leq i \leq S\),在哈希表中查询 \((a^S)^i \bmod p\) 及其对应的 \(j\)。如果查询成功则返回 \(iS - j\)。如果一直查询失败则方程无解。

时间复杂度 \(\mathcal{O}(\sqrt{p})\)

0x34 BSGS(大步小步算法).cpp

/* 大步小步算法 */
int BSGS(int a, int b, int p) { // a^x = b (mod p)
    if (b == 1 || p == 1) return 0;
    if (a == 0) return b == 0 ? 1 : -1;

    int S = sqrt(p) + 1;
    std::unordered_map<int, int> buc;

    int w = 1;
    for (int j = 0; j < S; j ++, w = 1ll * w * a % p) {
        int v = 1ll * w * b % p;
        buc[v] = j;
    }
    int x = w;
    for (int i = 1; i <= S; i ++, x = 1ll * x * w % p) {
        if (buc.contains(x)) {
            return i * S - buc[x];
        }
    }

    return -1;
}

固定底数且不保证与模数互质的高次同余方程 \(a^x \equiv b \pmod{p}\)(扩展大步小步算法,exBSGS)

该方程等价于 \(a^x - b\)\(p\) 的倍数,不妨设为 \(-k\) 倍,则原方程转化为 \(a\cdot a^{x - 1} + kp = b\)

\(\gcd(a, p) \nmid b\) 则无解,否则

\[a^{x - 1} \frac{a}{\gcd(a, p)} + k\frac{p}{\gcd(a, p)} = \frac{b}{\gcd(a, p)} \]

每次将 \(\frac{a}{\gcd(a, p)}\) 提取出来,并将 \(b, p\) 除以 \(\gcd(a, p)\),就变成了一个规模更小的子问题。

记已经提取出的系数之积为 \(r = \prod \frac{a}{\gcd(a, p)}\)。在递降的过程中,若 \(r \equiv b' \pmod{p'}\),则直接返回答案。

否则一直递降直到 \(\gcd(a, p') = 1\),此时得到一个固定底数且与模数互质的高次同余方程 \(ra^x \equiv b' \pmod{p'}\),套用 BSGS。

0x34 exBSGS(扩展大步小步算法).cpp

/* 扩展大步小步算法 */
int exBSGS(int a, int b, int p) { // a^x = b (mod p)
    if (b == 1 || p == 1) return 0;
    if (a == 0) return b == 0 ? 1 : -1;

    int d, r = 1, step = 0;
    while (d = std::gcd(a, p), d > 1) {
        if (b % d) {
            return -1;
        }

        b /= d, p /= d, r = 1ll * r * (a / d) % p;
        step ++;

        if (r == b) {
            return step;
        }
    }

    if (p == 1) {
        return step;
    }

    int S = sqrt(p) + 1;
    std::unordered_map<int, int> buc;

    int w = 1;
    for (int j = 0; j < S; j ++, w = 1ll * w * a % p) {
        int v = 1ll * w * b % p;
        buc[v] = j;
    }
    int x = 1ll * r * w % p;
    for (int i = 1; i <= S; i ++, x = 1ll * x * w % p) {
        if (buc.contains(x)) {
            return step + i * S - buc[x];
        }
    }
    return -1;
}

固定次数的高次同余方程 \(x^a \equiv b \pmod p\):当 \(p\) 存在原根 \(g\) 时,记 \(x = g^c\)(这里 \(c\) 是未知数),问题转化为

\[(g^a)^c \equiv b \pmod p \]

\(v = g^a\),此时得到一个固定底数的高次同余方程 \(v^c \equiv b \pmod p\)

  • 特解:\(x \equiv g^{ac} \pmod p\),使用 (ex)BSGS 得到。
  • 通解:\(x \equiv g^{ac + k\frac{\varphi(p)}{\gcd(\varphi(p), a)}} \pmod{p}\),其中 \(\frac{\varphi(p)}{\gcd(\varphi(p), a)}\) 表示 \(\mathrm{ord}_m(g^a)\)

Misc

平方差

平方差表示:当且仅当 \(1, 2, 4\),以及所有形如 \(4k + 2\) 的正整数,不能被表示成两个不同正整数的平方差

平方差表示证明:对于正整数 \(n\),相当于判断是否存在两个不同的正整数 \(a, b(a > b)\) 使得 \(n = a^2 - b^2 = (a + b)(a - b)\)

\(x = a + b, y = a - b\),由于 \(x, y\) 之间的差为 \(2b\),所以 \(x, y\) 奇偶性相同。对于同奇偶的两个 \(x, y(x > y)\),也都可以解出 \(a, b\) 的值。

故对于正整数 \(n\),相当于是要找出 \(n\) 的两个不同的约数 \(x, y\),且 \(x, y\) 奇偶性相同。

  • \(n\) 为奇数时,取 \(x = n, y = 1\) 即可。
  • \(n\) 为偶数时:
    • \(n = 4k\) 时,取 \(x = 2k, y = 2\) 即可。
    • \(n = 4k + 2\) 时,分解出的两个约数,必为一奇一偶。
  • \(n = 1, 2, 4\) 为特殊情况,均不能被表示。

0x35 数学知识 离散数学

集合论基础

集合并\(A \cup B = \{ x \mid x \in A \ \operatorname{or} \ x \in B \}\)

集合交\(A \cap B = \{ x \mid x \in A \ \operatorname{and} \ x \in B \}\)

集合补\(\complement_U A = \{ x \mid x \notin A \ \operatorname{and} \ x \in U \}\)

集合相对补\(B \setminus A = \{ x \mid x \notin A \ \operatorname{and} \ x \in B \}\)

集合对称差\(A \operatorname{\triangle} B = \{ x \mid x \in A \ \operatorname{xor} \ x \in B \}\)。同时

\[A \operatorname{\triangle} B = (A \setminus B) \cup (B \setminus A) \]

集合笛卡尔积\(A \times B = \{(x, y) \mid x \in A, y \in B \}\)

集合反演律(德 · 摩根定律)

\[\complement_U (A \cap B) = \complement_U A \cup \complement_U B \\ \complement_U (A \cup B) = \complement_U A \cap \complement_U B \]

置换

置换:有限集合到自身的双射(即一一对应)称为置换。不可重集合 \(S = \{a_1, a_2, \cdots, a_n\}\) 上的置换可以表示为

\[f = \begin{pmatrix} a_1 & a_2 & \cdots & a_n \\ a_{p_1} & a_{p_2} & \cdots & a_{p_n} \end{pmatrix} \]

表示将所有 \(a_i\) 映射成 \(a_{p_i}\),即 \(f(a_i) = a_{p_i}\)。其中 \(p_1, p_2, \cdots, p_n\)\(1 \sim n\) 的一个排列。

置换的乘法(复合):给出两个置换

\[f = \begin{pmatrix} a_{p_1} & a_{p_2} & \cdots & a_{p_n} \\ a_{q_1} & a_{q_2} & \cdots & a_{q_n} \end{pmatrix} \qquad g = \begin{pmatrix} a_1 & a_2 & \cdots & a_n \\ a_{p_1} & a_{p_2} & \cdots & a_{p_n} \end{pmatrix} \]

\(f\)\(g\) 的乘法(复合)记作 \(f \circ g\)

\[f \circ g = \begin{pmatrix} a_1 & a_2 & \cdots & a_n \\ a_{q_1} & a_{q_2} & \cdots & a_{q_n} \end{pmatrix} \]

\((f \circ g)(x) = f(g(x))\),即先经过了 \(g\) 的映射再经过了 \(f\) 的映射(从右到左)。

置换的逆:给出置换

\[f = \begin{pmatrix} a_1 & a_2 & \cdots & a_n \\ a_{p_1} & a_{p_2} & \cdots & a_{p_n} \end{pmatrix} \]

\(f\) 的逆置换记作 \(f^{-1}\)

\[f^{-1} = \begin{pmatrix} a_{p_1} & a_{p_2} & \cdots & a_{p_n} \\ a_1 & a_2 & \cdots & a_n \end{pmatrix} \]

一个关于 \(n\) 的排列,与其每个元素排名的序列,互为逆排列。

对称群:集合 \(S\) 上的所有置换构成一个群:满足封闭性、结合律、有单位元(恒等置换)、存在逆元(逆置换)。

通常记集合 \(\{1, 2, \cdots, n\}\) 上的所有置换构成的群为 \(n\) 元对称群,记作 \(S_n\)

置换群:对称群 \(S_n\) 的任意一个子群,即为置换群。

轮换:一类特殊的置换,可表示为

\[(a_1, a_2, \cdots, a_m) = \begin{pmatrix} a_1 & a_2 & \cdots & a_{m - 1} & a_m \\ a_2 & a_3 & \cdots & a_m & a_1 \end{pmatrix} \]

置换的轮换表示:任意一个置换,都可以分解成若干个不相交的轮换的乘积。轮换可以看作是构成置换的基本单位。

轮换的几何意义:将元素视为图中的点,将映射关系视为图中的有向边,由于每个节点的入度与出度均为 \(1\),因此形成的图必定由若干个不相交的环构成。

1-轮换(不动点):对于集合 \(X\) 上的置换 \(\sigma\),常用 \(X^\sigma\) 表示 \(\sigma\) 的不动点集合。即 \(X^{\sigma} = \{x \in X \mid \sigma(x) = x \}\)

2-轮换(对换)

  • 任何置换都可以写作一系列对换的乘积。
  • 进一步,任何置换都可以写作一系列相邻对换的乘积(冒泡排序)。

置换的奇偶性

排列的奇偶性:排列的奇偶性,为排列逆序对数的奇偶性。

  • 交换排列的任意两个相邻元素 \(p_i, p_{i + 1}\),会使得整个排列的奇偶性取反。
  • 交换排列的任意两个不同元素 \(p_i, p_j\),可以使用 \(2|i - j| - 1\) 次邻项交换实现。故交换排列的任意两个不同的元素,也会使得整个排列的奇偶性取反

置换的奇偶性:置换的奇偶性,为将该置换分解成若干个对换,所需对换个数的奇偶性。

群/环/域 基础

群类型 性质 举例
封闭性、结合律、一一一、单位元、逆元 对称群 \(S_n\)(集合 \(\{1, 2, \cdots, n\}\) 上的所有置换)
半群 封闭性、结合律、一一一、一一一、一一 \((\N_+, +)\)
幺半群 封闭性、结合律、一一一、单位元、一一 \((\Z, \times)\)
交换群 封闭性、结合律、交换律、单位元、逆元 \((\Z, +)\)

:设 \(G\)非空集合,其上有 \(G \times G \to G\)封闭性)的二元运算 \(\cdot\),如果满足以下性质,则称 \((G, \cdot)\) 是一个群:

  1. 结合律:对于任意 \(a, b, c \in G\),成立 \(a \cdot (b \cdot c) = (a \cdot b) \cdot c\)
  2. 有单位元:存在 \(e \in G\),使得对于任意 \(a \in G\),都成立 \(a \cdot e = e \cdot a = a\)。这里称 \(e\)\(G\) 的单位元。
  3. 存在逆元:对于任意 \(a \in G\),都存在相应的 \(b \in G\) 使得 \(a \cdot b = b \cdot a = e\)。这里称 \(b\)\(a\) 的逆元。

群的基本性质

  • 结合律推论:对于任意有限长的列 \(g_1, g_2, \cdots, g_k\),乘积 \(g_1 \cdot g_2 \cdot \ldots \cdot g_k\) 的结果与加括号的方式无关。
  • 单位元唯一:单位元 \(e\) 总是唯一的。
  • 逆元唯一:对于任意 \(x \in G\),其逆元 \(x^{-1}\) 也是唯一的。
  • 消去律:对于任意 \(a, b, c \in G\),若 \(a \cdot c = b \cdot c\)\(c \cdot a = c \cdot b\),则 \(a = b\)

半群

半群:设 \(G\)非空集合,其上有 \(G \times G \to G\)封闭性)的二元运算 \(\cdot\),若其满足结合律,则称 \((G, \cdot)\) 是一个半群。

幺半群

幺半群:设半群 \((G, \cdot)\),若其有单位元,则称 \((G, \cdot)\) 是一个幺半群。

数据结构中,线段树等结构可以维护幺半群的信息。

交换群(Abel 群)

交换群:设群 \((G, \cdot)\),若其满足交换律,则称 \((G, \cdot)\) 是一个交换群。

环类型 性质 举例
加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、一一一、一一一、一一。
分配律
幺环 加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、一一一、单位元、一一。
分配律
多项式环 \(R[x]\)
除环 加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、一一一、单位元、逆元。
分配律
四元数环 \(\mathbb{H}\)
交换环 加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、交换律、一一一、一一。
分配律
\(2\Z\)
整环 加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、交换律、单位元、一一。
分配律、无零因子
\((\Z, +, \times)\)

:设 \(R\)非空集合,其上有两个 \(R\times R \to R\)封闭性)的二元运算 \(+, \cdot\),如果满足以下性质,则称 \((R, +, \cdot)\) 是一个环:

  1. \((R, +)\) 构成交换群:即 \(+\) 满足结合律,有单位元(记作 \(0\)),存在逆元(记作 \(-a\))。
  2. \((R, \cdot)\) 构成半群:即 \(\cdot\) 满足结合律。
  3. 分配律:对于任意 \(a, b, c \in R\),有 \(a \cdot (b + c) = a \cdot b + a \cdot c\)\((a + b) \cdot c = a \cdot c + b \cdot c\)

为方便表述,记 $+, \cdot $ 分别表示该环的加法和乘法。加法单位元称作零元,乘法单位元(如果存在)称作幺元。

幺环

幺环:设环 \((R, +, \cdot)\),若其存在乘法单位元(记作 \(1\)),则称 \((R, +, \cdot)\) 是一个幺环。

除环

除环:设幺环 \((R, +, \cdot)\),若其所有非 \(0\) 元素 \(a \in R\) 都存在乘法逆元 \(a^{-1}\),则称 \((R, +, \cdot)\) 是一个除环。

交换环

交换环:设环 \((R, +, \cdot)\),若其满足乘法交换律,则称 \((R, +, \cdot)\) 是一个交换环。

整环

零元乘以任何元素都得到零元(即 \(0 \cdot a = a \cdot 0 = 0\))。理解一般环的乘法结构时,要去除加法单位元的影响,考察 \(R \setminus \{0\}\)。基于这一想法,有如下定义:

  • 零因子:对于环 \((R, +, \cdot)\) 中的非零元素 \(a \in R\),若存在非零 \(b \in R\) 满足 \(a \cdot b = 0\)\(b \cdot a = 0\),则称 \(a\) 为一个零因子。
  • 可逆元:对于环 \((R, +, \cdot)\) 中的非零元素 \(a \in R\),若存在 \(b \in R\) 满足 \(a \cdot b = b \cdot a = 1\),则称 \(a\) 为一个可逆元。

零因子不可能是可逆元,可逆元不可能是零因子。即零因子与可逆元互斥。但是一个非零元素可以既不是零因子,也不是可逆元。

整环:设交换幺环 \((R, +, \cdot)\),且无零因子,则称 \((R, +, \cdot)\) 为整环。

整环的性质

  • 整环的消去律:对于任意 \(a, b, c\in R\)\(a \neq 0\),若 \(a \cdot b = a \cdot c\),则 \(b = c\)
  • 非零元素对乘法封闭,但元素未必可逆,故仅构成半群。

乘法群

乘法群(单位群):设幺环 \((R, +, \cdot)\),设 \(R^\times\)\(R\) 中全体可逆元的集合,则 \((R^\times, \cdot)\) 构成群,成为幺环 \(R\) 的乘法群。

域类型 性质 举例
加法:封闭性、结合律、交换律、单位元、逆元。
乘法:封闭性、结合律、交换律、单位元、逆元。
分配律
数域 \(\Q, \R, \C\)

域(交换除环):设 \(F\)非空集合,其上有两个 \(F \times F \to F\)封闭性)的二元运算 \(+, \cdot\),如果满足以下性质,则称 \((F, +, \cdot)\) 是一个域:

  1. \((F, +)\) 构成一个交换群。其单位元记作 \(0\),元素 \(a \in F\)\(+\) 下的逆元记作 \(-a\)
  2. \((F \setminus \{0\}, \cdot)\) 构成一个交换群。其单位元记作 \(1\),元素 \(a \in F\)\(\cdot\) 下的逆元记作 \(a^{-1}\)

域是对加、减、乘、除四则运算都封闭的代数结构。非零元素不仅封闭,且构成乘法群。

posted @ 2022-12-19 10:24  Calculatelove  阅读(808)  评论(0)    收藏  举报