组合数学杂记

组合数学杂记

二项式系数

基本结论

对称恒等式:

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

加法公式:

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

对其对应多项式函数求导可以得到:

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

吸收恒等式:

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

三项恒等式:

\[\binom{a}{b} \binom{b}{c} = \binom{a}{c} \binom{a - c}{b - c} \]

与 Fibonacci 数列联系:

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

与下降幂联系:

\[\frac{a^{\underline{i}}}{b^{\underline{i}}} = \frac{\binom{a}{i}}{\binom{b}{i}} = \frac{\binom{b - i}{b - a}}{\binom{b}{a}} \\ \binom{n}{k} \times k^{\underline{m}} = \binom{n - m}{k - m} \times n^{\underline{m}} \]

组合数求和

上指标求和

朱世杰恒等式:

\[\sum_{i = k}^n \dbinom{i}{k} = \dbinom{n + 1}{k + 1} \]

证明:在 \(n + 1\) 个球里拿 \(k + 1\) 个方案数为 \(\binom{n + 1}{k + 1}\) ,最后一个拿的是第 \(i + 1\) 个,对应方案数为 \(\binom{i}{k}\)

下指标求和

高橋君

\(m\) 次询问,每次给出 \(n, k\) ,求 \(\sum_{i = 0}^k \binom{n}{i}\)

\(n, m, k \le 10^5\)

考虑莫队,记 \(f(n, k) = \sum_{i = 0}^k \binom{n}{i}\) ,分类讨论得到:

\[f(n, k + 1) = f(n, k) + \binom{n}{k + 1} \\ f(n, k - 1) = f(n, k) - \binom{n}{k} \\ f(n + 1, k) = 2 f(n, k) - \binom{n}{k} \\ f(n - 1, k) = \frac{f(n, k) + \binom{n - 1}{k}}{2} \]

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

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7;
 
struct Query {
    int n, k, bid, id;
 
    inline bool operator < (const Query &rhs) const {
        return bid == rhs.bid ? (bid & 1 ? k > rhs.k : k < rhs.k) : n < rhs.n;
    }
} qry[N];
 
int fac[N], inv[N], invfac[N], ans[N];
 
int m;
 
inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}
 
inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}
 
inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}
 
inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
 
signed main() {
    prework();
    scanf("%d", &m);
 
    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &qry[i].n, &qry[i].k), qry[i].bid = qry[i].n / 300, qry[i].id = i;
 
    sort(qry + 1, qry + 1 + m);
 
    for (int i = 1, n = 1, k = 0, result = 1; i <= m; ++i) {
        for (; n < qry[i].n; ++n) // f(n, k) -> f(n + 1, k)
            result = dec(2ll * result % Mod, C(n, k));
 
        for (; k > qry[i].k; --k) // f(n, k) -> f(n, k - 1)
            result = dec(result, C(n, k));
 
        for (; n > qry[i].n; --n) // f(n, k) -> f(n - 1, k)
            result = 1ll * inv[2] * add(result, C(n - 1, k)) % Mod;
 
        for (; k < qry[i].k; ++k) // f(n, k) -> f(n, k + 1)
            result = add(result, C(n, k + 1));
 
        ans[qry[i].id] = result;
    }
 
    for (int i = 1; i <= m; ++i)
        printf("%d\n", ans[i]);
 
    return 0;
}

平行求和法

\[\sum_{i = 0}^k \binom{n + i}{i} = \sum_{i = 0}^k \binom{n + i}{n} = \binom{n + k + 1}{n + 1} \]

二项式定理

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

\(a = b = 1\) 得到:

\[\sum_{i = 0}^n \binom{n}{i} = 2^n \]

\(a = 1, b = -1\) 得到:

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

范德蒙德卷积

\[\sum_{i = 0}^m \binom{n}{i} \binom{m}{k - i} = \binom{n + m}{k} \]

一般化的式子:

\[\sum_{i = -r}^s \binom{n}{r + i} \binom{m}{s - i} = \binom{n + m}{r + s} \]

推论一:

\[\sum_{i = 0}^{n} \binom{n}{i}^2 = \binom{2n}{n} \]

推论二:

\[\sum_{i = 1}^n \dbinom{n}{i} \dbinom{n}{i - 1} = \dbinom{2n}{n - 1} \]

推论三:

\[\sum_{i = 0}^n \binom{n}{i} \binom{m}{i} = \binom{n + m}{n} \]

容斥原理

普通形式

\[|\bigcup_{i \in S} A_i| = \sum_{T \subseteq S, T \ne \emptyset} (-1)^{|T| + 1} |\bigcap_{j \in T} A_j| \]

子集反演

\[f(S) = \sum_{T \subseteq S} g(T) \iff g(S) = \sum_{T \subseteq S} (-1)^{|S| - |T|} f(T) \\ f(S) = \sum_{S \subseteq T} g(T) \iff g(S) = \sum_{S \subseteq T} (-1)^{|T| - |S|} f(T) \]

Min-Max 容斥

最值形式:

\[\max(S) = \sum_{T \subseteq S} (-1)^{|T| + 1} \min(T) \\ \min(S) = \sum_{T \subseteq S} (-1)^{|T| + 1} \max(T) \]

\(k\) 大/小值形式:

\[\max_k (S) = \sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1} \min(T) \\ \min_k(S) = \sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1} \max(T) \]

值得注意的是,Min-Max 容斥在期望下也是成立的。一般情况下,求期望的最大/最小值并不好求,但是若最大/最小值有一个好求,那么就可以用 Min-Max 容斥求出另一个。

扩展到 gcd-lcm 上,可以得到:

  • \(\mathrm{lcm}_{x \in S}(x) = \prod_{T \subseteq S} \gcd_{x \in T}(x)^{(-1)^{|T| - 1}}\)
  • \(\gcd_{x \in S}(x) = \prod_{T \subseteq S} \mathrm{lcm}_{x \in T}(x)^{(-1)^{|T| - 1}}\)

P5643 [PKUWC2018] 随机游走

给出一棵树,每次等概率随机走到一个相邻点。\(q\) 次询问,每次给出一个集合 \(S\) ,求从 \(x\) (一开始就固定)出发随机游走直到点集 \(S\) 中所有点都至少经过一次的话,期望游走几步。特别地,点 \(x\) 视为一开始就被经过了一次。

\(n \le 18\)\(q \le 5000\)

求经过 \(S\) 里所有元素的期望时间,即到达 \(S\) 中最后一个点的期望步数( \(\max\) ),那么可以转化为枚举 \(S\) 的子集 \(T\) ,求到达 \(T\) 中第一个元素的期望时间( \(\min\) )。

\(f_{u, S}\) 表示 \(u\) 第一次走到 \(S\) 中的点的期望步数,\(d_u\) 表示 \(u\) 的度数,则:

\[f_{u, S} = \frac{f_{fa_u, S} + \sum_{v \in son(u)} f_{v, S}}{d_u} + 1 \quad (x \not \in S) \\ f_{u, S} = 0 \quad (x \in S) \]

考虑将每个点的值都写作 \(f_{u, S} = k_u \times f_{fa_u, S} + b_u\) 的形式,记:

\[K_u = \sum_{v \in son(u)} k_v, B_u = \sum_{v \in son(u)} b_v \]

则得到:

\[f_{u, S} = \dfrac{1}{d_u - K_u} \times f_{fa_u, S} + \dfrac{d_u + B_u}{d_u - K_u} \]

即:

\[k_u = \dfrac{1}{d_u - K_u}, b_u = \dfrac{d_u + B_u}{d_u - K_u} \]

答案即为 \(\sum_{T \subseteq S} (-1)^{|T| + 1} f_{r, T}\) ,不难用高维前缀和预处理后 \(O(1)\) 查询。时间复杂度 \(O(n 2^n + q)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int f[1 << N], g[1 << N];
int deg[N];

int n, q, r;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline int sgn(int n) {
    return n & 1 ? Mod - 1 : 1;
}

struct Node {
    int k, b;

    inline Node(const int _b = 0) : k(0), b(_b) {}

    inline Node(const int _k, const int _b) : k(_k), b(_b) {}

    inline Node operator + (const Node &rhs) const {
        return Node(add(k, rhs.k), add(b, rhs.b));
    }
} nd[N];

void dfs(int u, int f, int state) {
    Node res = 0;

    for (int v : G.e[u])
        if (v != f)
            dfs(v, u, state), res = res + nd[v];

    if (~state >> u & 1) {
        nd[u].k = mi(dec(deg[u], res.k), Mod - 2);
        nd[u].b = 1ll * add(deg[u], res.b) * nd[u].k % Mod;
    } else
        nd[u] = 0;
}

signed main() {
    n = read(), q = read(), r = read() - 1;

    for (int i = 1; i < n; ++i) {
        int u = read() - 1, v = read() - 1;
        G.insert(u, v), G.insert(v, u);
        ++deg[u], ++deg[v];
    }

    for (int i = 1; i < (1 << n); ++i)
        dfs(r, -1, i), f[i] = 1ll * nd[r].b * sgn(__builtin_popcount(i) + 1) % Mod;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < (1 << n); ++j)
            if (j >> i & 1)
                f[j] = add(f[j], f[j ^ (1 << i)]);

    while (q--) {
        int k = read(), state = 0;

        while (k--)
            state |= 1 << (read() - 1);

        printf("%d\n", f[state]);
    }

    return 0;
}

P3175 [HAOI2015] 按位或

有一个 \(n\) 位二进制数 \(x\) ,初始为 \(0\) ,每次有 \(p_i\) 的概率令 \(x \gets x \operatorname{or} i\) ,求 \(x = 2^n - 1\) 的期望次数。

\(n \le 20\)

将其转化为枚举子集 \(T\)\(T\) 中元素出现一个 \(1\) 的期望,由离散随机变量的几何分布得到该值为 \(\frac{1}{p_T}\) 。求 \(p_T\) 做一遍高位前缀和就好了。

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-12;
const int N = 21;

double p[1 << N];

int n;

signed main() {
    scanf("%d", &n);
    int all = (1 << n) - 1;

    for (int i = 0; i <= all; ++i)
        scanf("%lf", p + i);

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= all; ++j)
            if (j >> i & 1)
                p[j] += p[j ^ (1 << i)];

    double ans = 0;

    for (int i = 1; i <= all; ++i) {
        if (1 - p[all ^ i] < eps)
            return puts("INF"), 0;

        ans += (__builtin_parity(i) ? 1 : -1) / (1 - p[all ^ i]);
    }

    printf("%.6lf", ans);
    return 0;
}

P4707 重返现世

每秒按固定概率出现物品,求收集到 \(k\) 种物品的期望时间。

\(n \le 1000\)\(n - k \le 10\)

先令 \(k \gets n - k + 1\) ,将问题转化为 \(\max_k\) ,此时 \(k \le 11\)

考虑 Min-Max 容斥:\(E(\max_k(S)) = \sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1} E(\min(T))\) 。设 \(f_{i, j, k}\) 表示前 \(i\) 个位置,目前选出的物品 \(\sum p = j\)\(\sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1}\) 的值。若不选 \(i\) ,则:\(f_{i, j, k} \gets f_{i - 1, j, k}\) 。否则加入 \(i\)\(\sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1}\) 会变为 \(\sum_{T \subseteq S} (-1)^{|T| - k + 1} \binom{|T|}{k - 1}\) (即 \(|T| \to |T| + 1\) ),考虑用加法公式展开:

\[\begin{align} &\sum_{T \subseteq S} (-1)^{|T| - k + 1} \binom{|T|}{k - 1} \\ =& \sum_{T \subseteq S} (-1)^{|T| - k + 1} \left( \binom{|T| - 1}{k - 1} + \binom{|T| - 1}{k - 2} \right) \\ =& \sum_{T \subseteq S} (-1)^{|T| - (k - 1)} \binom{|T| - 1}{k - 2} - \sum_{T \subseteq S} (-1)^{|T| - k} \binom{|T| - 1}{k - 1} \end{align} \]

于是可以得到:

\[f_{i, j, k} = f_{i - 1, j, k} + f_{i - 1, j - p_i, k - 1} - f_{i - 1, j - p_i, k} \]

初始时 \(f_{i, 0, 0} = 1\) ,时间复杂度 \(O(nmk)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e3 + 7, M = 1e4 + 7, K = 1e1 + 7;

int p[N], inv[M], f[M][K], g[M][K];

int n, m, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int m) {
    inv[0] = inv[1] = 1;

    for (int i = 2; i <= m; ++i)
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
}

signed main() {
    scanf("%d%d%d", &n, &k, &m);
    k = n - k + 1, prework(m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", p + i);

    f[0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        memcpy(g, f, sizeof(f));

        for (int j = p[i]; j <= m; ++j)
            for (int l = 1; l <= k; ++l)
                f[j][l] = add(f[j][l], dec(g[j - p[i]][l - 1], g[j - p[i]][l]));
    }

    int ans = 0;

    for (int i = 1; i <= m; ++i)
        ans = add(ans, 1ll * f[i][k] * m % Mod * inv[i] % Mod);

    printf("%d", ans);
    return 0;
}

[AGC038E] Gachapon

有一个随机数生成器,生成 \([0, n - 1]\) 之间的整数,生成 \(i\) 的概率为 \(\frac{a_i}{\sum_{j = 0}^{n - 1} a_j}\)

这个随机数生成器不断生成随机数,当 \(\forall i \in [0, n - 1]\)\(i\) 至少出现 \(b_i\) 次时停止生成。

求期望生成随机数的次数。

\(\sum a_i, \sum b_i, n \le 400\)

考虑 Min-Max 容斥:

\[E(\max(S)) = \sum_{T \subseteq S} (-1)^{|T| - 1} E(\min(T)) \]

考虑求 \(E(\min(T))\) ,即 \(T\) 中存在一个元素达到 \(b\) 的期望次数。首先可以得到期望 \(p = \frac{\sum_{i = 0}^{n - 1} a_i}{\sum_{x \in T} a_x}\) 次操作才会生成 \(T\) 中的数,相当于一次操作的代价从 \(1\) 变为 \(p\) ,因此只要将结果乘上 \(p\) 即可,这样就不用考虑 \(T\) 之外的数的影响了。

对于生成中的每个状态 \(c\) ,其中 \(c_i\) 表示当前 \(x_i \in T\) 生成了 \(c_i < b_{x_i}\) 次,考虑把贡献拆到生成过程中的每个状态上,则只要统计中间状态的生成概率即可得到:

\[E(\min(T)) = \frac{\sum a_i}{\sum_{x \in T} a_x} \sum_{c_i < b_{x_i}} \frac{(\sum c_i)!}{\prod c_i!} \times \prod (\frac{a_i}{\sum_{x \in T} a_x})^{c_i} \]

带入 Min-Max 容斥的式子得到:

\[\begin{aligned} E(\max(S)) &= \sum_{T \subseteq S} (-1)^{|T| - 1} E(\min(T)) \\ &= \sum_{T \subseteq S} (-1)^{|T| - 1} \frac{\sum a_i}{\sum_{x \in T} a_x} \sum_{c_i < b_{x_i}} \frac{(\sum c_i)!}{\prod c_i!} \times \prod (\frac{a_i}{\sum_{x \in T} a_x})^{c_i} \\ &= \sum_{T \subseteq S} (-1)^{|T| - 1} \frac{\sum a_i}{\sum_{x \in T} a_x} \sum_{c_i < b_{x_i}} (\frac{1}{\sum_{x \in T} a_x})^{\sum c_i} \frac{(\sum c_i)!}{\prod c_i!} \times \prod a_i^{c_i} \\ \end{aligned} \]

\(f_{i, j, k}\) 表示考虑了 \(1 \sim i\)\(T\) 中的情况,其中 \(\sum_{x \in T} a_x = j\)\(\sum c_i = k\)\((-1)^{|T| - 1} \prod \frac{a_i^{c_i}}{c_i !}\) 的和,转移就枚举当前元素是否在 \(T\) 中即可。

\(A = \sum a, B = \sum b\) ,时间复杂度 \(O(AB^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 4e2 + 7;

int a[N], b[N], fac[N], inv[N], invfac[N], f[N][N];

int n, A, B;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", a + i, b + i), A += a[i], B += b[i];

    prework(), f[0][0] = Mod - 1;

    for (int i = 1; i <= n; ++i)
        for (int j = A; j >= a[i]; --j)
            for (int k = B; ~k; --k)
                for (int c = 0, pw = 1; c <= min(k, b[i] - 1); ++c, pw = 1ll * pw * a[i] % Mod)
                    f[j][k] = dec(f[j][k], 1ll * f[j - a[i]][k - c] * pw % Mod * invfac[c] % Mod);

    int ans = 0;

    for (int i = 1; i <= A; ++i)
        for (int j = 0, pw = inv[i]; j <= B; ++j, pw = 1ll * pw * inv[i] % Mod)
            ans = add(ans, 1ll * f[i][j] * A % Mod * pw % Mod * fac[j] % Mod);

    printf("%d", ans);
    return 0;
}

二项式反演

形式一

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

形式二

\[f(n) = \sum_{i = n}^m (-1)^i \binom{i}{n} g(i) \iff g(n) = \sum_{i = n}^m (-1)^i \binom{i}{n} f(i) \]

形式三

\[f(n) = \sum_{i = 0}^n \dbinom{n}{i} g(i) \iff g(n) = \sum_{i = 0}^n (-1)^{n - i} \dbinom{n}{i} f(i) \]

常见套路是记 \(f(n)\) 表示所有元素均满足条件的方案数,\(g(i)\) 表示钦定 \(i\) 个元素不满足条件、其他元素任意的方案数,则 \(f(n) = \sum_{i = 0}^n (-1)^i \binom{n}{i} g(i)\)

形式四

\[f(n) = \sum_{i = n}^m \dbinom{i}{n} g(i) \iff g(n) = \sum_{i = n}^m (-1)^{i - n} \dbinom{i}{n} f(i) \]

格路计数问题

以下讨论 \((0, 0) \to (n, m)\) 的方案数,移动方式为 \((x, y)\) 可以移动到 \((x + 1, y)\)\((x, y + 1)\) 的情况。

若移动方式为 \((x, y) \to (x + 1, y \pm 1)\) ,则相当于旋转坐标系,对应到原问题就相当于求 \((0, 0) \to (\frac{n + m}{2}, \frac{n - m}{2})\) 的方案数,需要判断一下奇偶性。

无限制

\(f(n, m)\) 表示无限制时,\((0, 0) \to (n, m)\) 的方案数,则:

\[f(n, m) = \binom{n + m}{n} \]

意义: \(n + m\) 中选 \(n\) 步向上、其余向右的方案数。

一条直线

\(g_1(n, m, k)\) 表示 \((0, 0) \to (n, m)\) 且与直线 \(y = x + k\) 相交的方案数,则:

\[g_1(n, m, k) = (m + k, n - k) \]

意义:将第一次碰到的位置后面的路径按 \(y = x + k\) 对称,新路径与原路径形成双射。

\(g_0(n, m, k)\) 表示 \((0, 0) \to (n, m)\) 且与直线 \(y = x + k\) 不相交的方案数,则:

\[g_0(n, m, k) = f(n, m) - g_1(n, m, k) \]

两条直线(反射容斥)

\(h(n, m, l, r)\) 表示 \((0, 0) \to (n, m)\) 且与直线 \(y = x + l\) 、直线 \(y = x + r\) 均不相交的方案数,其中 \(l < 0 < r\)

考虑将路径上碰到的直线类型写成字符串,该串仅由 LR 构成,若连续多次碰到同一条直线则只记录第一次,即消掉连续段。

不难发现字符串一定形如 LRLRL...RLRLR... 。考虑容斥,对所有这样的字符串计数,需要保证空串被统计一次,其余串不被统计。

考虑容斥,记 \(F(S)\) 表示 \(S\) 为子序列(消去连续段)的对应字符串的方案数量,答案即为:

\[F(\emptyset) - F(L) - F(R) + F(LR) + F(RL) - F(LRL) - F(RLR) + \cdots \]

接下来考虑求 \(F(S)\) ,按 \(S\) 依次翻转终点,则 \(S\) 相邻两个字符之间的路径不做限制,直接算组合数即可。

由于每次翻折操作都会扩大终点值域,时间复杂度 \(O(\frac{n + m}{r - l})\)

有些题目可以用于与 \(O((n + m)(r - l))\) 的暴力 DP 做平衡复杂度,实现时注意判断求组合数时 \(n, m\) 均需非负。

inline int calc(int n, int m, int l, int r) {
    auto flip = [](int &x, int &y, int k) {
        swap(x, y), x += k, y -= k;
    };
    
    int ans = C(n + m, m), x = n, y = m;
    
    while (x >= 0 && y >= 0) {
        flip(x, y, l), ans = dec(ans, C(x + y, y));
        flip(x, y, r), ans = add(ans, C(x + y, y));
    }
    
    x = n, y = m;
    
    while (x >= 0 && y >= 0) {
        flip(x, y, r), ans = dec(ans, C(x + y, y));
        flip(x, y, l), ans = add(ans, C(x + y, y));
    }
    
    return ans;
}

常见数列

Catalan 数

以下问题均属于 Catalan 数列:

  • \(n\) 个节点构成的无标号、区分左右儿子的二叉树数量为 \(Cat_n\)
  • \(n\) 个节点构成的无标号、区分儿子的有根树数量为 \(Cat_{n - 1}\)
  • \(n\) 个左括号与 \(n\) 个右括号组成的合法序列有 \(Cat_n\) 种。
  • \(n\) 个元素按照大小进栈,合法的出栈序列有 \(Cat_n\) 种。
  • \(n\) 边凸多边形的三角剖分方案数为 \(Cat_{n - 2}\)
  • \((0,0)\) 出发,每次沿正方向走,到达 \((n,n)\) 且不接触直线 \(y=x\) 的路径数量为 \(Cat_n\)
  • 以圆上的 \(2n\) 个点为端点,连互不相交的 \(n\) 条弦的方案数为 \(Cat_n\)
  • \(1 \sim 2n\) 中的数不重不漏地填入 \(2 \times n\) 的矩阵,每个数字大于其左边和上面数字的方案数 \(Cat_n\)

Catalan 数列的前几项为:

\(Cat_0\) \(Cat_1\) \(Cat_2\) \(Cat_3\) \(Cat_4\) \(Cat_5\) \(\cdots\)
\(1\) \(1\) \(2\) \(5\) \(14\) \(42\) \(\cdots\)

常见求法:

\[\begin{align} Cat_n &= \frac{\binom{2n}{n}}{n + 1} & (n > 2) \\ Cat_n &= \binom{2n}{n} - \binom{2n}{n - 1} \\ Cat_n &= \frac{(4n - 2)Cat_{n - 1}}{n + 1} \\ Cat_n &= \sum_{i = 1}^n Cat_{i - 1} \times Cat_{n - i} & (n > 2) \end{align} \]

Bell 数

\(B_n\) 表示将 \(1 \sim n\) 划分为若干集合的方案数。

Bell 数列的前几项为:

\(B_0\) \(B_1\) \(B_2\) \(B_3\) \(B_4\) \(B_5\) \(\cdots\)
\(1\) \(1\) \(2\) \(5\) \(15\) \(52\) \(\cdots\)

常见求法:

\[\begin{align} B_n &= \sum_{k = 0}^n {n \brace k} \\ B_{n + 1} &= \sum_{k = 0}^n \dbinom{n}{k} B_k \end{align} \]

Fibonacci 数列

Fibonacci 数列的前几项为:

\(F_0\) \(F_1\) \(F_2\) \(F_3\) \(F_4\) \(F_5\) \(\cdots\)
\(0\) \(1\) \(1\) \(2\) \(3\) \(5\) \(\cdots\)

常见求法:

\[F_i = \begin{cases} 1 & (i \le 2) \\ F_{i - 1} + F_{i - 2} & (i \ge 3) \end{cases} \\ F_i = \frac{1}{\sqrt{5}} \left( (\frac{1 + \sqrt{5}}{2})^n - (\frac{1 - \sqrt{5}}{2})^n \right) \\ \begin{cases} F_{2n} = F_n (2 F_{n + 1} - F_n) \\ F_{2n + 1} = F_{n + 1}^2 + F_n^2 \end{cases} \]

常用结论:

  • \(F_{n - 1} F_{n + 1} - F_n^2 = (-1)^n\)

  • \(F_{n + m} = F_{n} F_{m + 1} + F_{n - 1} F_m\)

    • 特殊情况:\(F_{2n} = F_n(F_{n + 1} + F_{n - 1})\)
  • \(n \mid m \iff F_n \mid F_m\)

  • \(\gcd(F_n, F_m) = F_{\gcd(n, m)}\)

\(m\) 意义下 Fibonacci 数列的最小正周期被称为皮萨诺周期,相关结论:

  • 皮萨诺周期 \(\le 6m\)\(m = 2 \times 5^k\) 时取等)。
  • Fibonacci 数列 \(\bmod 2\) 的最小正周期为 \(3\)
  • Fibonacci 数列 \(\bmod 5\) 的最小正周期为 \(20\)
  • Fibonacci 数列 \(\bmod p\)\(p\) 是素数且 \(p \equiv \pm 1 \pmod{5}\) )的最小正周期是 \(p - 1\) 的因数。
  • Fibonacci 数列 \(\bmod p\)\(p\) 是素数且 \(p \equiv \pm 2 \pmod{5}\) )的最小正周期是 \(2p + 2\) 的因数。

划分数

\(p_n\) 表示将 \(n\) 写作不增正整数和的方案数。

划分数的前几项为:

\(p_0\) \(p_1\) \(p_2\) \(p_3\) \(p_4\) \(p_5\) \(\cdots\)
\(1\) \(1\) \(2\) \(3\) \(5\) \(7\) \(\cdots\)

\(n\) 分成 \(k\) 个不增正整数和的方案数,称作 \(k\) 部划分数,记作 \(p(n, k)\)

常见求法:

\[p(n, k) = p(n - 1, k - 1) + p(n - k, k) \\ p_n = \sum_{k = 1}^n p(n, k) \]

P6189 [NOI Online #1 入门组] 跑步

\(n\) 的划分数 \(\bmod p\)

\(n \le 10^5\)

考虑两种 DP:

  • 完全背包:设 \(f_{i, j}\) 表示 \(i\) 拆分为若干 \(\le j\) 的数的方案数,则 \(f_{i, j} = f_{i, j - 1} + f_{i - j, j}\)
  • 柱状图 DP:设 \(g_{i, j}\) 表示将 \(i\) 拆分为 \(j\) 个数的方案数,则 \(g_{i, j} = g_{i - 1,j - 1} + g_{i - j, j}\)

考虑根号分治,对于 \(\le \sqrt{n}\) 的数用完全背包转移,对于 \(> \sqrt{n}\) 的数用柱状图 DP 转移,最后合并两个 DP 即可,时空复杂度 \(O(n \sqrt{n})\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, B = 327;

int f[N], g[N][B], h[N];

int n, Mod;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d", &n, &Mod);
    int block = sqrt(n);
    f[0] = 1;

    for (int i = 1; i <= block; ++i)
        for (int j = i; j <= n; ++j)
            f[j] = add(f[j], f[j - i]);

    g[0][0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= min(i, n / block); ++j) {
            g[i][j] = g[i - j][j];

            if (i > block)
                g[i][j] = add(g[i][j], g[i - block - 1][j - 1]);
        }

    for (int i = 0; i <= n; ++i)
        for (int j = 0; j <= n / block; ++j)
            h[i] = add(h[i], g[i][j]);

    int ans = 0;

    for (int i = 0; i <= n; ++i)
        ans = add(ans, 1ll * f[i] * h[n - i] % Mod);

    printf("%d", ans);
    return 0;
}

Lucas 定理

普通形式

P3807 【模板】卢卡斯定理/Lucas 定理

对于质数 \(p\) ,有:

\[\binom{n}{m} \bmod p = \binom{\lfloor \frac{n}{p} \rfloor}{\lfloor \frac{m}{p} \rfloor} \times \binom{n \bmod p}{m \bmod p} \bmod p \]

组合数的奇偶性:\(\binom{i}{j} \bmod 2 = [j \subseteq i]\)

exLucas

P4720 【模板】扩展卢卡斯定理/exLucas

可以处理模数非质数的情况。

根据唯一分解定理,将模数 \(p\) 分解为 \(p = \prod_{i=1}^r q_i^{k_i}\) ,构造同余方程组:

\[\begin{cases} x \equiv \binom{n}{m} \pmod{q_1^{k_1}} \\ x \equiv \binom{n}{m} \pmod{q_2^{k_2}} \\ \vdots \\ x \equiv \binom{n}{m} \pmod{q_r^{k_r}} \end{cases} \]

CRT 合并即可,问题转化为求 \(\binom{n}{m} \bmod q^k\) 的值,其中 \(q\) 为质数。

先将 \(\binom{n}{m}\) 拆为 \(\frac{n!}{m! (n - m)!}\) ,但是分母部分不一定在模 \(q^k\) 意义下存在逆元,考虑把 \(q\) 的次幂提取出来:

\[\frac{\frac{n!}{q^x}}{\frac{m!}{q^y} \frac{(n-m)!}{q^z}}q^{x-y-z} \pmod{q^k} \]

这样分母与 \(p\) 互质,可以用 exgcd 求逆元。

先考虑如何求 \(x, y, z\) ,令 \(h(n)\)\(n!\)\(q\) 的指数,则:

\[h(n) = \lfloor \frac{n}{q} \rfloor + h(\lfloor \frac{n}{q} \rfloor) \]

再考虑如何求 \(\frac{n!}{q^x} \bmod q^k\) ,可以得到:

\[\begin{aligned} \frac{n!}{q^x} &\equiv \left( \prod_{i = 1}^{\lfloor \frac{n}{q} \rfloor} iq \right) \times \left( \prod_{i = 1}^n [i \bot q] i \right) \\ &\equiv q^{\lfloor \frac{n}{q} \rfloor} \lfloor \frac{n}{q} \rfloor! \times \left(\prod_{i = 1}^{q^k} [i \bot q] i \right)^{\lfloor \frac{n}{q^k} \rfloor} \times \left(\prod_{i = 1}^{n \bmod q^k} [i \bot q] i \right) \\ \end{aligned} \]

其中 \(\lfloor \frac{n}{q} \rfloor!\) 里可能还有 \(q\) 的次幂,递归处理即可。

杨氏矩阵

定义:

  • 杨图:由有限个相邻的方格排列而成,其中各横行左边对齐,长度自上而下不升。

    • 不难发现杨图每行的长度与总方格数的整数分拆 \(\lambda\) 一一对应,因此可以用一组整数分拆 \(\lambda\) 表示杨图。
  • 杨表:用某个字母表的符号填充杨图得到,字母表通常需要是全序集合,为方便一般填入正整数。

  • 标准杨表:满足每列数字严格递增、每行数字严格递增的杨表。

  • 半标准杨表:满足每列数字严格递增、每行数字不降的杨表。

RSK 插入算法

\(S\) 是一个标准杨表,定义 \(S \gets x\) 表示将 \(x\) 插入杨表,流程如下:从第一行开始遍历行,在当前行找到第一个 \(>x\) 的数 \(y\)

  • 若存在,则交换 \(x, y\) 并移步至下一行重复操作。
  • 若不存在,则将 \(x\) 放在该行末尾,退出流程。

容易证明在上述算法过后,\(S\) 仍然是标准杨表。

由此可以得到标准杨表的一些性质:

  • 第一行长度为排列的 LIS 长度(内容不一定)。
  • 第一列长度为排列的 LDS 长度(内容不一定)。
  • 若将排列倒序插入杨表 \(S'\) ,则新杨表 \(S'\) 由原杨表 \(S\) 交换行列得到。
  • 更换全序集合的比较方式 \(\prec\)\(\succ\) ,则新杨表 \(S'\) 的形状为原杨表 \(S\) 交换行列得到(内容不一定)。

不难发现杨表的每一行的长度集合构成了 LIS 的划分,每一列的长度构成了 LDS 的划分(注意每一行/列并不一定是LIS/LDS)。

勾长公式

勾长:对于杨表 \(\lambda\) 中的某方格 \((x, y)\) ,定义:

  • 臂长:其右面的单元格个数,记为 \(a_\lambda(x, y)\)
  • 腿长:其下面的单元格个数,记为 \(l_\lambda(x, y)\)
  • 勾长:\(\mathrm{hook}(x, y) = a_\lambda(x, y) + l_\lambda(x, y) + 1\)

下图标明了每个方格的勾长:

对于一个 \(n\) 个方格的杨图 \(\lambda\) ,记将 \(1 \sim n\) 填入该杨表的方案数为 \(\dim_{\lambda}\) ,则勾长公式:

\[\dim_{\lambda} = \frac{n!}{\prod_{(x, y) \in \lambda} \mathrm{hook}(x, y)} \]

以上方的杨图为例,填表的方案数即为 \(\dim_{\lambda} = \frac{10!}{7 \times 5 \times 4 \times 3 \times 1 \times 5 \times 3 \times 2 \times 1 \times 1} = 288\)

运用勾长公式可以得到

  • \(n\) 个方格的杨表方案数为:\(f(n) = \begin{cases} 1 & n = 0, 1 \\ f(n - 1) + (n - 1) \times f(n - 2) & n > 1 \end{cases}\)

Robinson-Schensted correspondence 定理

内容:任何两个相同形状的杨表(填数的顺序可能不同)可以与排列建立一一对应,即:

\[\sum_{\lambda \in P_n} (\dim_\lambda)^2 = n! \]

其中 \(P_n\)\(n\) 的分拆数。

构造排列到双杨表的映射:维护插入杨表 \(P\) 和记录杨表 \(Q\) ,依次将 \(p_{1 \sim n}\) 插入 \(P\) ,插入 \(p_i\) 时在 \(Q\) 的同样位置填入 \(i\)

构造双杨表到排列的映射:根据填数从大到小枚举 \(Q\) 的单元格,从后往前确定排列 \(p\) 。枚举到一个单元格,在 \(P\) 中找到对应的单元格。若在第一行则直接删除,否则在上一行找到比它小的最大数,将它放到那里并继续删除被替换的数。

应用

LIS 模板题

给定序列 \(a_{1 \sim n}\) 和常数 \(m\) ,对每个前缀求:至多操作 \(m\) 次,每次操作删去一个不降子序列,删去的数之和的最大值。

\(n \le 10^5\)\(m \le 5\)

考虑将一个数 \(x\) 替换为 \(x\)\(x\) ,这样问题转化为求每次选出的 LIS 长度和的最大值。

考虑杨表,其满足自上而下长度不升,因此答案即为杨表前 \(m\) 行的长度和。

发现杨表内每行连续段只有 \(O(m)\) 个,用 set 维护 pair 表示数字及出现次数即可。

但是可能会一次弹出很多个数,注意到每个 pair 只会被弹出 \(m\) 次,而插入一个数时至多分裂 \(m\)pair ,因此复杂度正确。

时间复杂度 \(O(nm \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int M = 5;

struct Node {
    int x, k;

    inline bool operator < (const Node &rhs) const {
        return x < rhs.x;
    }
};

multiset<Node> st[M];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);
    ll ans = 0;

    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        vector<Node> now = {(Node){x, x}};

        for (int j = 0; j < m; ++j) {
            vector<Node> nxt;

            for (auto x : now) {
                auto it = st[j].upper_bound(x);

                if (it == st[j].end())
                    st[j].emplace(x), ans += x.k;
                else {
                    int sum = 0;

                    while (it != st[j].end() && sum < x.k)
                        nxt.emplace_back(*it), sum += it->k, it = st[j].erase(it);

                    if (sum > x.k)
                        st[j].emplace((Node){nxt.back().x, sum - x.k}), nxt.back().k -= sum - x.k, sum = x.k;
                    
                    st[j].emplace(x), ans += x.k - sum;
                }
            }

            now = nxt;
        }

        printf("%lld ", ans);
    }

    return 0;
}

P4484 [BJWC2018] 最长上升子序列

求长度为 \(n\) 的排列的 LIS 的期望 \(\bmod 998244353\)

\(n \le 28\)

由杨表的构造过程可知,序列的 LIS 即为杨表第一行的长度。

考虑枚举所有整数拆分,然后用 Robinson-Schensted correspondence 定理算方案数即可。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e1 + 7;

vector<int> vec;

int fac[N], inv[N], invfac[N];

int n, ans;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

void dfs(int l, int r) {
    if (!r) {
        int res = fac[n];
        vector<int> cnt(vec.back() + 1);

        for (int it : vec)
            for (int i = 1; i <= it; ++i)
                res = 1ll * res * inv[(++cnt[i]) + it - i] % Mod;

        ans = add(ans, 1ll * res * res % Mod * vec.back() % Mod);
    } else {
        for (int i = l; i <= r; ++i)
            vec.emplace_back(i), dfs(i, r - i), vec.pop_back();
    }
}

signed main() {
    scanf("%d", &n);
    prework(n), dfs(1, n);
    printf("%d", 1ll * ans * invfac[n] % Mod);
    return 0;
}

P3774 [CTSC2017] 最长上升子序列

给定序列 \(a_{1 \sim n}\)\(q\) 次询问前缀 \(a_{1 \sim m}\) 中最长的满足 LIS 长度 \(\le k\) 的子序列长度。

\(n \le 5 \times 10^4\)\(q \le 2 \times 10^5\)

注意到杨表的前 \(\sqrt{n}\) 行和前 \(\sqrt{n}\) 列可以覆盖整个杨表,考虑维护 \(\prec\) 关系的原杨表 \(S\)\(\succ\) 关系的转置杨表 \(S'\) ,分别维护前 \(\sqrt{n}\) 行。

根据 Dilworth 定理,答案即为杨表前 \(k\) 列的方格个数之和。

将询问离线后扫 \(m\) 这一维,每次加入元素就在两个杨表里同时插入,只枚举前 \(\sqrt{n}\) 行即可。加到树状数组中的时候处理一下不要加重,对于 \(S\) 只要维护 \(\sqrt{n} + 1\) 及之后的位置,对于 \(S'\) 只要维护 \(\sqrt{n}\) 及之前的位置即可。

时间复杂度 \(O(n \sqrt{n} \log n + q \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7, Q = 2e5 + 7;

vector<pair<int, int> > qry[N];
vector<vector<int> > a, b;

int val[N], ans[Q];

int n, q, B;

namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x < N; x += x & -x)
        c[x] += k;
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

inline void insertA(int x) {
    for (int i = 0; i < a.size(); ++i) {
        auto it = lower_bound(a[i].begin(), a[i].end(), x);

        if (it == a[i].end()) {
            a[i].emplace_back(x);

            if (a[i].size() > B)
                BIT::update(a[i].size(), 1);

            return;
        }

        swap(*it, x);
    }

    if (a.size() < B)
        a.emplace_back((vector<int>){x});
}

inline void insertB(int x) {
    for (int i = 0; i < b.size(); ++i) {
        auto it = upper_bound(b[i].begin(), b[i].end(), x, greater<int>());

        if (it == b[i].end()) {
            b[i].emplace_back(x), BIT::update(i + 1, 1);
            return;
        }

        swap(*it, x);
    }

    if (b.size() < B)
        b.emplace_back((vector<int>){x}), BIT::update(b.size(), 1);
}

signed main() {
    scanf("%d%d", &n, &q);

    for (int i = 1; i <= n; ++i)
        scanf("%d", val + i);

    for (int i = 1; i <= q; ++i) {
        int m, k;
        scanf("%d%d", &m, &k);
        qry[m].emplace_back(k, i);
    }

    B = sqrt(n);

    for (int i = 1; i <= n; ++i) {
        insertA(val[i]), insertB(val[i]);

        for (auto it : qry[i])
            ans[it.second] = BIT::query(it.first);
    }

    for (int i = 1; i <= q; ++i)
        printf("%d\n", ans[i]);

    return 0;
}
posted @ 2025-06-16 16:07  wshcl  阅读(8)  评论(0)    收藏  举报