多项式大学习

多项式基础

多项式 FFT / NTT

以下只适合学过的人看

FFT 就简单背一下范德蒙德矩阵的结论、分治的方法以及逆变换的改动,就可以默写出代码了。

NTT 懒得推了。\(g\) 替换 \(w\) 就行。

【模板】多项式乘法(FFT)

半在线卷积

是的孩子们牛顿迭代已经被抛弃了

【模板】多项式乘法逆

分治 NTT

给定 \(F\)\(G\) 满足:

\[F(x)*G(x)≡1(\bmod x^n) \]

系数模 \(998244353\)

推理:

\(n = 0\) 时:

\[g_0 = f_0^{-1} \]

\(n > 0\) 时:

\[g_n = -\dfrac{1}{f_0}\sum_{i = 0} ^ {n - 1} g_i f_{n - i} \]

分治地做,先算左边的所有 \(g\),再算左边对右边的贡献,再算右边的 \(g\)

贡献是一个卷积的形式,可以 NTT。

时间复杂度:

\[T(n) = 2T(\dfrac n2) + O(n \log n) \]

由(类似)主定理知:

\[T(n) = O(n \log ^ 2n) \]

多叉分治优化

可以做 \(B\) 叉的分治以减少 DFT / IDFT 的次数以达到优化的目的。

多叉时依次算下去,每块累加点乘的值一并 IDFT 即可。

\[T(n) = BT(n/B) + O(nB+n \log \dfrac nB) \]

我也不知道为啥,但是反正 \(B = \log n\) 时有:

\[T(n) = O(\dfrac{n\log ^ 2n}{\log \log n}) \]

实际上为了保持长度是 \(2\) 的幂,一般取 \(B = 16\)

参考:半在线卷积 - whx1003 - 博客园

模板汇总

为了模板化,封装了多项式结构体。

为了处理区间长度简便,默认在主程序里面把多项式长度调到 \(n = 2^k\) 的形式。

多项式乘法

只放最简单的 NTT 了,竞赛常用属于是。

const int mod = 998244353;
const int gmod = 3;

struct Poly {
    int n;
    vector<long long> a;
    void redeg(int n_re) {
        a.resize(n = n_re, 0);
    }
    int deg() {
        return a.size();
    }
    void output(int output_deg) {
        for(int i = 0; i < output_deg; i++) printf("%d ", a[i]);
        puts("");
    }
};

long long calc_pow(long long x, int k) { // calculate x ^ k % mod
    long long res = 1;
    for(; k; k >>= 1, x = x * x % mod) if(k & 1) res = res * x % mod;
    return res;
}

void NTT(Poly& F, int op) {
    int n = F.deg();
    long long ginv = calc_pow(gmod, mod - 2);
    // butterfly transform
    vector<int> rev(n, 0);
    for(int i = 1; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) * (n >> 1));
    for(int i = 0; i < n; i++) if(rev[i] < i) swap(F.a[i], F.a[rev[i]]);

    // NTT
    for(int k = 1; k < n; k <<= 1) { // length
        long long wn = calc_pow(op == 1 ? gmod : ginv, (mod - 1) / (k << 1));
        for(int i = 0; i < n; i += (k << 1)) {
            long long w = 1, x, y;
            for(int j = i; j < i + k; j++, w = w * wn % mod) {
                x = F.a[j], y = w * F.a[j + k] % mod;
                F.a[j] = (x + y) % mod, F.a[j + k] = (x + mod - y) % mod;
            }
        }
    }
    
    // inv
    if(op == -1) {
        long long ninv = calc_pow(n, mod - 2);
        for(int i = 0; i < n; i++) F.a[i] = F.a[i] * ninv % mod;
    }
}

void Mul(Poly& F, Poly& G, Poly& H) {
    int deg = H.deg();
    Poly pF = F, pG = G;
    NTT(pF, 1), NTT(pG, 1);
    for(int i = 0; i < deg; i++) H.a[i] = pF.a[i] * pG.a[i] % mod;
    NTT(H, -1);
}

多项式乘法逆

给定 \(F\)\(G\) 满足:

\[F(x)*G(x)≡1(\bmod x^n) \]

考虑每一项有:

\[g_n = -\dfrac{1}{f_0}\sum_{i = 0} ^ {n - 1} g_i f_{n - i} \]

使用半在线卷积即可。

边界等注意事项之后不再赘述,读者可自行类比判断。

const int DAC_min = 128;
const int B = 16;

void Inv_DAC(Poly& F, Poly& G, int l, int r) { // [l, r)
    if(r - l <= DAC_min) {
        for(int i = l; i < r; i++) {
            if(i == 0) G.a[i] = calc_pow(F.a[i], mod - 2);
            else G.a[i] = 1ll * (mod - G.a[0]) * G.a[i] % mod; // mod - G.a[0] = - 1 / F.a[0]

            for(int j = 1; j < r - i; j++) G.a[i + j] = (G.a[i + j] + G.a[i] * F.a[j]) % mod;
        }
        return ;
    }

    int len = (r - l) / B;
    Poly pF[B], pG[B];
    for(int i = 0; i < B; i++) pF[i].redeg(len << 1), pG[i].redeg(len << 1);

    for(int i = 1; i < B; i++) { // calculate F (points)
        for(int j = 0; j < len << 1; j++) pF[i].a[j] = F.a[(i - 1) * len + j];
        NTT(pF[i], 1);
    }

    for(int i = 0; i < B; i++) {  // calculate G (points)
        NTT(pG[i], -1);
        for(int j = 0; j < len; j++) G.a[l + i * len + j] = (G.a[l + i * len + j] + pG[i].a[len + j]) % mod;
        Inv_DAC(F, G, l + i * len, l + (i + 1) * len);
        pG[i].redeg(len << 1);
        for(int j = 0; j < len; j++) pG[i].a[j] = G.a[l + i * len + j], pG[i].a[j + len] = 0;
        NTT(pG[i], 1);

        for(int j = i + 1; j < B; j++) { // calculate from i to j
            for(int k = 0; k < len << 1; k++) pG[j].a[k] = (pG[j].a[k] + pG[i].a[k] * pF[j - i].a[k]) % mod;
        }
    }

    for(int i = 0; i < B; i++) pF[i].redeg(0), pG[i].redeg(0);
}
void Inv(Poly& F, Poly& G, int deg) {
    Inv_DAC(F, G, 0, deg);
}

多项式对数

【模板】多项式对数函数(多项式 ln)

给定 \(F\)\(G\) 满足:

\[G(x) ≡ \ln F(x)(\bmod x^n) \]

考虑求个导。

\[G'(x) = \dfrac{F'(x)}{F(x)} \]

只需要求导、求逆、乘法和积分就做完了。

void Ln(Poly& F, Poly& G, int deg) {
    Poly dF, Fn, dG;
    dF.redeg(deg << 1), Fn.redeg(deg << 1), dG.redeg(deg << 1);
    for(int i = 1; i < deg; i++) dF.a[i - 1] = F.a[i] * i % mod; dF.a[deg - 1] = 0;
    for(int i = deg; i < deg << 1; i++) dF.a[i] = 0, Fn.a[i] = 0;
    Inv(F, Fn, deg);
    Mul(dF, Fn, dG);
    for(int i = deg - 2; i >= 0; i--) G.a[i + 1] = dG.a[i] * calc_pow(i + 1, mod - 2) % mod; G.a[0] = 0;
    dF.redeg(0), Fn.redeg(0), dG.redeg(0);
}

多项式指数

【模板】多项式指数函数(多项式 exp)

给定 \(F\)\(G\) 满足:

\[G(x) ≡ \exp F(x)(\bmod x^n) \]

考虑求个导。

\[G'(x) = \exp F(x)\cdot F'(x) = G(x)F'(x) \]

对每一项:(这里看第 \(n - 1\) 项)

\[ng_n = \sum_{i = 0} ^ {n - 1} g_{i}(n - i)f_{n - i} \]

这是一个半在线卷积。

void Exp_DAC(Poly& F, Poly& G, int l, int r) {
    if(r - l <= DAC_min) {
        for(int i = l; i < r; i++) {
            if(i == 0) G.a[i] = 1;
            else G.a[i] = G.a[i] * inv[i] % mod;

            for(int j = 1; j < r - i; j++) G.a[i + j] = (G.a[i + j] + G.a[i] * F.a[j] % mod * j) % mod;
        }
        return ;
    }

    int len = (r - l) / B;
    Poly pF[B], pG[B];
    for(int i = 0; i < B; i++) pF[i].redeg(len << 1), pG[i].redeg(len << 1);
    for(int i = 1; i < B; i++) { // calculate pF
        for(int j = 0; j < len << 1; j++) pF[i].a[j] = F.a[(i - 1) * len + j] * ((i - 1) * len + j) % mod;
        NTT(pF[i], 1);
    }

    for(int i = 0; i < B; i++) { // calculate pG
        NTT(pG[i], -1);
        for(int j = 0; j < len; j++) G.a[l + i * len + j] = (G.a[l + i * len + j] + pG[i].a[len + j]) % mod;
        Exp_DAC(F, G, l + i * len, l + (i + 1) * len);
        for(int j = 0; j < len << 1; j++) pG[i].a[j] = j >= len ? 0 : G.a[l + i * len + j];
        NTT(pG[i], 1);

        for(int j = i + 1; j < B; j++) {
            for(int k = 0; k < len << 1; k++) pG[j].a[k] = (pG[j].a[k] + pG[i].a[k] * pF[j - i].a[k]) % mod;
        }
    }
}
void Exp(Poly& F, Poly& G, int deg) {
    Exp_DAC(F, G, 0, deg);
}

多项式快速幂

给定 \(F\)\(G\) 满足:

\[G(x) ≡ (F(x))^k(\bmod x^n) \]

考虑换底(先取对数再取指数),有:

\[G(x) = \exp (k \ln F(x)) \]

只需对数、数乘和指数。

void Power(Poly& F, Poly& G, int deg, int k) {
    Poly H;
    H.redeg(deg);
    Ln(F, H, deg);
    for(int i = 0; i < deg; i++) H.a[i] = H.a[i] * k % mod;
    Exp(H, G, deg);
}

例题

残缺的字符串

先考虑没有通配符。KMP 秒了

但是要为通配符铺路,我们换个视角。

不妨定义两个等长串的距离:记 \(S\) 的坐标为 \((C(S_0),C(S_1),\cdots,C(S_{n - 1}))\),其中 \(n\) 为串长,\(C(a) = 1,C(b) = 2\),依此类推。\(S\)\(T\) 之间的距离就是坐标距离的平方(便于计算)。

那么两个等长串 \(S = (f_0,f_1, \cdots,f_{n - 1})\)\(T = (g_0,g_1, \cdots,g_{n - 1})\) 相等的充要条件是:

\[dist(S, T) = \sum_{i = 0}^{n - 1}(f_i - g_i) ^ 2 = 0 \]

带入 \(A,B\) 并展开这个式子:(\(B_i = B[i,\cdots,i + |A| - 1]\)

\[dist(A, B_i) = \sum_{j = 0} ^ {n - 1} f_j^2 + g_{i + j}^2 - 2f_jg_{i+j} \]

前两项容易求。后面的是一个差卷积,取 \(h_i = f_{n - 1 - i} (i < n),h_i = 0(i \ge n)\) 变换得

\[\sum_{j = 0} ^ {n - 1}f_jg_{i + j} = \sum_{j = 0} ^ {n - 1}h_{n - 1 - j}g_{i + j} = [x^{n - 1 + i}](h*g) \]

其中补全若干项是合理的,因为 \(h\) 的高位是 0。

那么就可以 \(O(n \log n)\) 计算所有的距离了。

有通配符时:

定义 \(C(*) = 0\)

改距离为:

\[dist(S, T) = \sum_{i = 0}^{n - 1}(f_i - g_i) ^ 2f_ig_i \]

即可。

展开后对每一项做卷积就行。

P3711 仓鼠的数学题

实在看不懂,直接记结论算了。

重新定义 \(S_k(x) = \sum_{i = 0} ^ {x - 1} i^k\)。有结论:

\[S_k(x) = \dfrac{1}{k + 1}\sum_{i = 0} ^ {k + 1}\dbinom{k + 1}{i}B_{k + 1 - i}x^{i} \]

其中伯努利数 \(B_i\) 的指数生成函数为 \(\dfrac{x}{e^x - 1}\)。那么容易通过求逆求出 \(B_i\)

证明是把这个 \(x\) 换成 \(n\),再考虑 \(S_k(n)\) 的指数生成函数。

那么就有:

\[\begin{aligned} \sum_{k = 0} ^ n a_kS_k(x) &= \sum_{k = 0} ^ n \dfrac{a_k}{k + 1}\sum_{i = 0} ^ {k + 1}\dbinom{k + 1}{i}B_{k + 1 - i}x^{i}\\ &= \sum_{k = 0} ^ n \dfrac{a_k}{k!}\sum_{i = 0} ^ {k + 1} \dfrac{B_{k + 1 - i}}{(k + 1 - i)!}\dfrac{x_i}{i!} \\ &= \sum_{i = 0} ^ {n + 1} \sum_{k = i - 1} ^ {n} \dfrac{a_k}{k!}\dfrac{B_{k + 1 - i}}{(k + 1 - i)!}\dfrac{x_i}{i!} \end{aligned} \]

\(\sum_{k = i - 1} ^ {n} \dfrac{a_k}{k!}\dfrac{B_{k + 1 - i}}{(k + 1 - i)!}\) 拿差卷积算即可。

最后要 \(x \rightarrow x + 1\),令上面求出的多项式为 \(F(x)\),转换后为 \(G(x)\)

\[\begin{aligned} G(x) &= F(x + 1) \\ &= \sum_{i = 0} ^ {n + 1}f_i(x + 1)^i \\ &= \sum_{i = 0} ^ {n + 1}f_i\sum_{j = 0} ^ i \dbinom i j x_j \\ &= \sum_{j = 0} ^ {n + 1}\sum_{i = j} ^ {n + 1}i!f_i \dfrac{1}{(i - j)!} \dfrac{x_j}{j!} \end{aligned} \]

那么又是一个差卷积。

P5488 差分与前缀和

这题相比之下就很简单了。

前缀和一次就是乘以 \(\dfrac{1}{1 - x}\),具体证明直接展开容易看出。

差分又是前缀和的逆运算,故答案为 \(f(x)\dfrac{1}{(1-x)^k}\)\(f(x)(1-x)^k\)

只需要多项式 \(\ln, \exp\) 以及数乘。

P4841 [集训队作业2013] 城市规划

这题用容斥就反而麻烦了,注意不要想歪就行了。

\(n\) 节点简单连通图数量为 \(f_n\)\(n\) 节点简单图数量为 \(g_n\)

明显 \(g_n = 2^{\binom{n}{2}}\)

考虑求 \(f\)\(g\) 的关系。枚举 \(g\)\(n\) 号节点所在连通块的大小,有:

\[g_n = \sum_{i = 1}^n \dbinom{n - 1}{i - 1}f_{i}g_{n-i} \]

拆开式子并定义 \(f_0 = 1\),有:

\[n\dfrac{g_n}{n!} = \sum_{i = 0}^n\dfrac{if_i}{i!}\dfrac{g_{n - i}}{(n - i)!} \]

也即:

\[G'(x) = F'(x)G(x) \]

解得

\[F(x) = \ln G(x) \]

由 EGF \(\exp\) 的意义也可导出。

P5915 冬至

两种方法都想了,没推到最后...

令人感到巧合的是此题也是同时能用矩阵递推和波斯坦茉莉做。

P7844「dWoi R2」FFT / 狒狒贴

求 NTT 做 \(k\) 次后的结果。有结论:

NTT 做两次相当于每个位置乘以 \(n\),然后翻转 \(f_1,\cdots,f_n\)。也有用这个性质来写 NTT 板子的,好处是不用求原根的逆元。

证明考虑直接算 NTT 矩阵的乘积就行。

那么NTT 做四次相当于每个位置乘以 \(n^2\),这些一起搞定,再做 \(k \bmod 4\) 次 NTT 即可。

P4389 付公主的背包

其生成函数为:

\[F(x) = \prod_{i = 1} ^ n\dfrac{1}{1 -x ^ {v_i}} \]

关键是怎么快速求出来。

容易想到求逆可以最后一起做,然后分治来乘。但这样时间其实不对。

考虑求个对数,加起来后最后 \(\exp\)

\[\ln \dfrac{1}{1 - x^{v_i}} = \sum_{j = 1} \dfrac{x^{jv_i}}{j} \]

可以统计 \(v\) 相同的一并贡献,只需在 \(v\) 的倍数次数上贡献,那么这是 \(O(m \log m)\) 的。

最后来一个 \(\exp\) 即可。也是 \(O(n \log n)\)(牛顿迭代)或 \(O(\dfrac{n \log ^ 2 n}{\log \log n})\)(半在线卷积)。

P5748 集合划分计数

好像叫贝尔数 \(bell(n)\)

考虑 \(n\) 个数组成一个非空集合的方案数的 EGF 显然为 \(e^x - 1\)

由 EGF 的 \(\exp\) 的意义得答案为 \([x^n]\exp (\exp(x) - 1)\)

P7092 计数题

P7435 简单的排列计数

P5641 【CSGRound2】开拓者的卓识

这个 \(sum_{k, l, r}\) 的贡献肯定是每个 \(i \in [l, r]\)\(a_i\) 乘以某个位置相关系数。考虑 \(a_i\) 的系数:

相当于求区间序列 \([l_0, r_0], [l_1, r_1], \cdots, [l_k, r_k]\) 满足 \([l_{i + 1}, r_{i + 1}] \subseteq [l_i, r_i], [l_0, r_0] = [l, r], [l_k, r_k] = [i, i]\) 的方案数。这是球盒问题的变种,于是答案为:

\[sum_{k, 1 ,r} = \sum_{i = 1}^r \dbinom{k + i - 2}{i - 1}\dbinom{k + r - i - 1}{r - i} \]

拆开式子直接卷积就行。

P9164 「INOH」Round 1 - 狂气

理论上甚至可以多叉分治。

轮换式 加强版

排列计数

HAOI2018 染色

P6667 [清华集训 2016] 如何优雅地求和

posted @ 2025-05-17 21:53  Vizing  阅读(19)  评论(0)    收藏  举报