计数中的容斥

计数中的容斥

容斥原理

式子:

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

一般的应用是钦定 \(k\) 个不合法,对答案的贡献乘上 \((-1)^k\) 的容斥系数。

P5405 [CTS2019] 氪金手游

给定 \(n\) 个点,第 \(i\) 个点的权值 \(w_i\) 分别有 \(p_{i, 1 \sim 3}\) 的概率取 \(1 \sim 3\)

确定所有 \(w_i\) 之后开始游戏:不断选点知道所有点都被选过,点 \(i\) 被选中的概率为 \(\frac{w_i}{\sum w}\)

给定 \(n - 1\) 个限制 \((u, v)\) 表示第一次抽到 \(u\) 的时间早于第一次抽到 \(v\) 的时间,保证所有 \((u, v)\) 连无向边后构成一棵树。

求满足所有限制的概率。

\(n \le 1000\)

先考虑 \(u \to v\) 连成外向树的情况,则要求每个点都要比子树内的点早抽到,点 \(u\) 合法的概率为 \(\frac{w_u}{\sum_{v \in subtree(u)} w_v}\) 。设 \(f_{u, i}\) 表示考虑 \(u\) 的子树、\(\sum_{v \in subtree(u)} w_v = i\) 的合法概率,转移类似树形背包,时间复杂度 \(O(n^2)\)

接下来考虑反向边的情况,考虑容斥,钦定有 \(i\) 条反向边不满足,即将钦定的这 \(i\) 条反向边反转,图会变成外向树森林,直接容斥可以做到 \(O(2^n n^2)\)

考虑优化,发现每次都暴力拆成外向树森林比较傻,考虑直接在树上 DP,DP 过程中决策每条反向边。设 \(f_{u, i, j}\) 表示考虑 \(u\) 的子树、\(\sum_{v \in subtree(u)} w_v = i\) 、钦定 \(j\) 条反向边的合法概率,注意钦定反向边的子树 \(w\)\(0\) ,时间复杂度 \(O(n^3)\)

发现记录 \(j\) 只与最后的容斥系数有关,不妨在 DP 中直接乘上容斥系数,就不用记录 \(j\) 这一维了,时间复杂度 \(O(n^2)\)

另解:对于反向边的处理,考虑用 “不考虑这条边”的方案数 减去 “考虑这条边为外向边”的方案数,即 \(o \rightarrow o \leftarrow o\) 等价于 \(o \rightarrow o \ \ \ \ o\) 减去 \(o \rightarrow o \rightarrow o\) ,树上 DP 时遇到此类内向边就将权值用上面这种方法计算一下即可。

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

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

int p[N][5], inv[N * 3], siz[N], f[N][N * 3], g[N * 3];

int n;

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 p, int b) {
    int res = 1;
    
    for (; b; b >>= 1, p = 1ll * p * p % Mod)
        if (b & 1)
            res = 1ll * res * p % Mod;
    
    return res;
}

void dfs(int u, int fa) {
    f[u][0] = 1;
    
    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;
        
        if (v == fa)
            continue;
        
        dfs(v, u);
        int su = siz[u] * 3, sv = siz[v] * 3;
        fill(g, g + su + sv + 1, 0);
        
        if (w) {
            int sum = accumulate(f[v], f[v] + sv + 1, 0ll) % Mod;
            
            for (int i = 0; i <= su; ++i)
                g[i] = 1ll * sum * f[u][i] % Mod;
            
            for (int i = 0; i <= su; ++i)
                for (int j = 0; j <= sv; ++j)
                    g[i + j] = dec(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
        } else {
            for (int i = 0; i <= su; ++i)
                for (int j = 0; j <= sv; ++j)
                    g[i + j] = add(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
        }
        
        memcpy(f[u], g, sizeof(int) * (su + sv + 1)), siz[u] += siz[v];
    }
    
    int su = siz[u] * 3;
    fill(g, g + su + 5, 0);
    
    for (int i = 0; i <= su; ++i) {
        g[i + 1] = add(g[i + 1], 1ll * f[u][i] * p[u][1] % Mod * inv[i + 1] % Mod);
        g[i + 2] = add(g[i + 2], 2ll * f[u][i] * p[u][2] % Mod * inv[i + 2] % Mod);
        g[i + 3] = add(g[i + 3], 3ll * f[u][i] * p[u][3] % Mod * inv[i + 3] % Mod);
    }
    
    memcpy(f[u], g, sizeof(int) * (su + 5)), ++siz[u];
}

signed main() {
    scanf("%d", &n);
    inv[0] = inv[1] = 1;
    
    for (int i = 2; i <= n * 3; ++i)
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
    
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= 3; ++j)
            scanf("%d", p[i] + j);
        
        int inv = mi(p[i][1] + p[i][2] + p[i][3], Mod - 2);
        
        for (int j = 1; j <= 3; ++j)
            p[i][j] = 1ll * p[i][j] * inv % Mod;
    }
    
    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v, 0), G.insert(v, u, 1);
    }
    
    dfs(1, 0);
    int ans = 0;
    
    for (int i = 0; i <= siz[1] * 3; ++i)
        ans = add(ans, f[1][i]);
    
    printf("%d", ans);
    return 0;
}

QOJ10781. Permutation Pair

给定序列 \(a_{1 \sim n}\),求排列二元组 \((p_{1 \sim n}, q_{1 \sim n})\) 的数量,满足对于 \(n \times n\) 的矩阵 \(M_{i, j} = [\exists k, p_k = i, q_k = j]\) ,对于所有 \(M_{i, j} = 1\) 的位置 \((i, j)\) ,不存在覆盖 \((i, j)\) 的大小为 \((a_j + 1) \times (a_j + 1)\) 的子矩阵满足其为单位矩阵(主对角线为 \(1\) ,其余为 \(0\) )或单位矩阵的镜像(反对角线为 \(1\) ,其余为 \(0\) )。

\(n \le 5000\)

不难发现可以固定一个排列为 \(1, 2, \cdots, n\) ,最后将答案乘 \(n!\) 即可。问题转化为求排列 \(p_{1 \sim n}\) 的数量,满足对于所有 \(i\)\(p_i\) 所在的下标连续的公差为 \(1\)\(-1\) 的等差数列长度 \(\le a_{p_i}\)

考虑将 \(1 \sim n\) 做合法分段,然后考虑段内的翻转与段间的顺序,并要求相邻段不能连起来。后者可以容斥,钦定 \(k\) 个间隔连起来,乘上 \((-1)^k\) 的容斥系数。

\(f_{i, j, 0/1}\) 表示将 \(1 \sim i\) 分为 \(j\) 段,当前段长度 \(\ge 2\) 时是否需要 \(\times 2\) 的贡献,记录第三维是因为若干段连在一起的段段内不能翻转,只能整体翻转。

考虑 \(i \to i + 1\) 的转移,记 \(l = i + 1\)\(r\) 表示最大位置 \(j\) 满足 \(j - i + 1 \le \min_{k = i + 1}^j a_k\) ,若 \(r = n\) ,则可以对答案产生贡献。

分讨转移:

  • 新建一段长度为 \(1\) 的段:\(f_{i, j, 0} + f_{i, j, 1} \to f_{i + 1, j + 1, 1}\)
  • 新建一段长度 \(\ge 2\) 的段:\((f_{i, j, 0} + 2 f_{i, j, 1}) \to f_{l + 1 \sim r, j + 1, 1}\)
  • 新建一段,但是钦定与当前段连在一起:\(-(f_{i, j, 0} + 2 f_{i, j, 1}) \to f_{l \sim r, j, 0}\)

前缀和优化即可做到 \(O(n^2)\)

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

int a[N], fac[N], mn[N][N], f[N][N][2], g[N][N][2];

int n;

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", &n), fac[0] = 1;

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), fac[i] = 1ll * fac[i - 1] * i % Mod;

    for (int i = 1; i <= n; ++i) {
        mn[i][i] = a[i];

        for (int j = i + 1; j <= n; ++j)
            mn[i][j] = min(mn[i][j - 1], a[j]);
    }

    int ans = 0;
    f[0][0][1] = 1;

    for (int i = 0; i < n; ++i) {
        int nxt = i + 1;

        while (nxt < n && nxt + 1 - i <= mn[i + 1][nxt + 1])
            ++nxt;

        if (i) {
            for (int j = 0; j <= i; ++j)
                for (int k = 0; k <= 1; ++k)
                    g[i][j][k] = add(g[i][j][k], g[i - 1][j][k]);
        }

        for (int j = 0; j <= i; ++j) {
            f[i][j][0] = add(f[i][j][0], g[i][j][0]), f[i][j][1] = add(f[i][j][1], g[i][j][1]);

            if (nxt == n) {
                ans = add(ans, (n - i >= 2 ? 2ll : 1ll) * f[i][j][1] * fac[j + 1] % Mod);
                ans = add(ans, 1ll * f[i][j][0] * fac[j + 1] % Mod);
            }

            int l = i + 1, r = nxt + 1;

            if (l > r)
                continue;

            f[i + 1][j + 1][1] = add(f[i + 1][j + 1][1], add(f[i][j][0], f[i][j][1]));

            g[l][j][0] = dec(g[l][j][0], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
            g[r][j][0] = add(g[r][j][0], add(f[i][j][0], 2ll * f[i][j][1] % Mod));

            if (l < r) {
                g[l + 1][j + 1][1] = add(g[l + 1][j + 1][1], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
                g[r][j + 1][1] = dec(g[r][j + 1][1], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
            }
        }
    }

    printf("%d", 1ll * ans * fac[n] % Mod);
    return 0;
}

CF1613F Tree Coloring

给出一棵树,定义一个排列 \(p_{1 \sim n}\) 合法当且仅当 \(\forall i \in [2, n], p_i \ne p_{fa_i} - 1\) ,求合法排列数。

\(n \le 2.5 \times 10^5\)

不难发现钦定一个点不合法是容易的,此时 \(p_i = p_{fa_i} - 1\)

考虑钦定 \(k\) 个点不合法,就只会剩下 \(n - k\) 个自由点,求方案数 \(f_k\) 后乘上系数 \((-1)^k (n - k)!\) 求和。

注意到一个父亲只能有一个儿子不合法,考虑在父亲处统计,方案数为 \(|son(u)|\) ,因此 \(f_k = [x^k] \prod_{u = 1}^n (|son(u)|x + 1)\) ,不难分治 + NTT 做到 \(O(n \log^2 n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, rt = 3, invrt = (Mod + 1) / 3;
const int N = 2.5e5 + 7;

int deg[N], fac[N];

int n;

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;
}

namespace Poly {
vector<int> rev;

inline int calc(int n) {
    int len = 1;

    while (len < n)
        len <<= 1;

    rev.resize(len);

    for (int i = 0; i < len; ++i)
        rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? len >> 1 : 0);

    return len;
}

inline void NTT(vector<int> &f, int op) {
    for (int i = 0; i < f.size(); ++i)
        if (i < rev[i])
            swap(f[i], f[rev[i]]);

    for (int k = 1; k < f.size(); k <<= 1) {
        int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));

        for (int i = 0; i < f.size(); i += k << 1) {
            int buf = 1;

            for (int j = 0; j < k; ++j) {
                int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
                f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
                buf = 1ll * buf * tG % Mod;
            }
        }
    }

    if (op == -1) {
        int invn = mi(f.size(), Mod - 2);

        for (int &it : f)
            it = 1ll * it * invn % Mod;
    }
}

inline vector<int> Mul(vector<int> f, vector<int> g) {
    int lim = f.size() + g.size() - 1, len = calc(lim);
    f.resize(len), g.resize(len);
    NTT(f, 1), NTT(g, 1);

    for (int i = 0; i < len; ++i)
        f[i] = 1ll * f[i] * g[i] % Mod;

    NTT(f, -1), f.resize(lim);
    return f;
}
} // namespace Poly

vector<int> solve(int l, int r) {
    if (l == r)
        return {1, deg[l]};
    
    int mid = (l + r) >> 1;
    return Poly::Mul(solve(l, mid), solve(mid + 1, r));
}

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

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        ++deg[u], ++deg[v];
    }

    fac[0] = fac[1] = 1;

    for (int i = 2; i <= n; ++i)
        --deg[i], fac[i] = 1ll * fac[i - 1] * i % Mod;

    vector<int> f = solve(1, n);
    int ans = 0;

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

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

还可以优化,记 \(s_i = |son(i)|\)\(c_i\) 表示 \(s = i\) 的数量,则 \(\sum_{i = 1}^n s_i = \sum_{i = 1}^n i c_i = n - 1\)

考虑降序对于每个 \(i\) ,先 \(O(c_i)\) 二项式展开求出 \((ix + 1)^{c_i}\) ,然后将其卷到最终式上。

时间复杂度 \(O(\sum_{i = 1}^n (\sum_{j = i}^{n} c_j) \log n) = O(\log n \sum_{i = 1}^n i c_i) = O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, rt = 3, invrt = (Mod + 1) / 3;
const int N = 2.5e5 + 7;

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

int n;

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;
}

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;
    }
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

namespace Poly {
vector<int> rev;

inline int calc(int n) {
    int len = 1;

    while (len < n)
        len <<= 1;

    rev.resize(len);

    for (int i = 0; i < len; ++i)
        rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? len >> 1 : 0);

    return len;
}

inline void NTT(vector<int> &f, int op) {
    for (int i = 0; i < f.size(); ++i)
        if (i < rev[i])
            swap(f[i], f[rev[i]]);

    for (int k = 1; k < f.size(); k <<= 1) {
        int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));

        for (int i = 0; i < f.size(); i += k << 1) {
            int buf = 1;

            for (int j = 0; j < k; ++j) {
                int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
                f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
                buf = 1ll * buf * tG % Mod;
            }
        }
    }

    if (op == -1) {
        int invn = mi(f.size(), Mod - 2);

        for (int &it : f)
            it = 1ll * it * invn % Mod;
    }
}

inline vector<int> Mul(vector<int> f, vector<int> g) {
    int lim = f.size() + g.size() - 1, len = calc(lim);
    f.resize(len), g.resize(len);
    NTT(f, 1), NTT(g, 1);

    for (int i = 0; i < len; ++i)
        f[i] = 1ll * f[i] * g[i] % Mod;

    NTT(f, -1), f.resize(lim);
    return f;
}
} // namespace Poly

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

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        ++deg[u], ++deg[v];
    }

    ++cnt[deg[1]];

    for (int i = 2; i <= n; ++i)
        ++cnt[--deg[i]];

    prework(n);
    vector<int> f = {1};

    for (int i = n; i; --i) {
        if (!cnt[i])
            continue;

        vector<int> g(cnt[i] + 1);

        for (int j = 0, pw = 1; j <= cnt[i]; ++j, pw = 1ll * pw * i % Mod)
            g[j] = 1ll * C(cnt[i], j) * pw % Mod;

        f = Poly::Mul(f, g);
    }

    int ans = 0;

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

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

P2567 [SCOI2010] 幸运数字

定义:

  • 幸运数字:各数位均为 \(6\)\(8\) 的数。
  • 近似幸运数字:存在一个因数为幸运数字的数。

\([L, R]\) 内近似幸运数字的数量。

\(L \le R \le 10^{10}\)

首先预处理出 \([1, 10^{10}]\) 内的幸运数字集合 \(S\) ,只有 \(2046\) 个。

然后考虑统计近似幸运数字,答案即为 \(\sum_{T \subseteq S} (\lfloor \frac{R}{\mathrm{lcm}(T)} \rfloor - \lceil \frac{L}{\mathrm{lcm}(T)} \rceil + 1)\) ,直接算会 TLE,考虑剪枝:

  • \(\mathrm{lcm}(T) > R\) 时无需计算。
  • dfs 枚举子集时按幸运数字降序排序,从而使得 \(\mathrm{lcm}(T)\) 增长更快。
  • 对于两个幸运数字 \(a, b\) ,若 \(a \mid b\) ,则 \(b\) 的倍数一定是 \(a\) 的倍数,因此可以忽略这个 \(b\) ,幸运数字数量降为 \(943\)
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

vector<ll> vec;

ll L, R, ans;

void dfs1(ll k) {
    if (k > R)
        return;

    vec.emplace_back(k);
    dfs1(k * 10 + 6), dfs1(k * 10 + 8);
}

void dfs2(int x, int sgn, ll lcm) {
    ans += sgn * (R / lcm - (L + lcm - 1) / lcm + 1);

    for (int i = x + 1; i < vec.size(); ++i) {
        ll g = __gcd(lcm, vec[i]);

        if ((__int128)lcm / g * vec[i] <= R)
            dfs2(i, -sgn, lcm / g * vec[i]);
    }
}

signed main() {
    scanf("%lld%lld", &L, &R);
    dfs1(6), dfs1(8);
    sort(vec.begin(), vec.end(), greater<ll>());
    vector<ll> tmp;

    for (int i = 0; i < vec.size(); ++i) {
        bool flag = true;

        for (int j = i + 1; j < vec.size(); ++j)
            if (!(vec[i] % vec[j])) {
                flag = false;
                break;
            }

        if (flag)
            tmp.emplace_back(vec[i]);
    }

    vec = tmp;

    for (int i = 0; i < vec.size(); ++i)
        dfs2(i, 1, vec[i]);

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

CF1530F Bingo

有一个 \(n \times n\) 的矩阵,\((i, j)\) 位置有 \(p_{i, j}\) 的概率为 \(1\)\(1 - p_{i, j}\) 的概率为 \(0\)

求至少满足以下条件其一的概率:

  • 有一行全 \(1\)
  • 有一列全 \(1\)
  • 有一个对角线全 \(1\)

\(n \le 21\)

直接容斥可以做到 \(2^{2n + 2}\) ,无法通过。

考虑只枚举列和对角线的情况,预处理 \(p_{i, s}\) 表示第 \(i\) 行钦定情况为 \(s\) 的概率乘积,则不难 \(O(n)\) 求出 \(2^n\) 种行的情况的概率之和,时间复杂度 \(O(2^{n + 2} n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 31607, Inv = 3973;
const int N = 21;

int p[N][1 << 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 int sgn(int n) {
    return n & 1 ? Mod - 1 : 1;
}

inline int solve(int s) {
    int res = 1;

    for (int i = 0; i < n; ++i) {
        int t = s & ((1 << n) - 1);

        if (s >> n & 1)
            t |= 1 << i;

        if (s >> (n + 1) & 1)
            t |= 1 << (n - 1 - i);

        res = 1ll * res * dec(p[i][t], p[i][(1 << n) - 1]) % Mod;
    }

    return res;
}

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

    for (int i = 0; i < n; ++i) {
        p[i][0] = 1;

        for (int j = 0; j < n; ++j)
            scanf("%d", p[i] + (1 << j)), p[i][1 << j] = 1ll * p[i][1 << j] * Inv % Mod;

        for (int j = 1; j < (1 << n); ++j)
            p[i][j] = 1ll * p[i][j & -j] * p[i][(j - 1) & j] % Mod;
    }

    int ans = 1;

    for (int i = 0; i < (1 << (n + 2)); ++i)
        ans = add(ans, 1ll * sgn(__builtin_popcount(i) + 1) * solve(i) % Mod);

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

[AGC068D] Sum of Hash of Lexmin

给出一棵树,其中 \(fa_i < i\) 。定义排列 \(p_{1 \sim n}\) 合法当且仅当经过任意次以下操作不能得到一个字典序小于 \(p\) 的排列:

  • 选择一个位置 \(1 \le i < n\) ,若 \(p_i\)\(p_{i + 1}\) 在树上呈祖孙关系,交换 \(p_i\)\(p_{i + 1}\)

给定常数 \(B\) ,定义一个排列 \(p_{1 \sim n}\) 的权值为 \(\sum_{i = 1}^n B^{i - 1} \times p_i\) ,求所有合法排列的权值和 \(\bmod 998244353\)

\(n \le 100\)

考虑如何比较方便地判定一个排列是否合法:

  • 首先若存在 \(p_i \in \mathrm{subtree}(p_{i + 1})\) ,则说明 \(p_i > p_{i + 1}\) ,交换二者显然得到字典序更小的排列。
  • 其次考虑一般情况,最后一定是交换了一对 \(p_i \in \mathrm{subtree}(p_{i + 1})\) ,但是二者初始不一定相邻。而注意到初始二者之间的数一定都在 \(p_{i + 1}\) 的子树中,只要将初始的 \(p_i, p_{i + 1}\) 交换即可。

因此得到排列合法当且仅当不存在 \(p_i \in \mathrm{subtree}(p_{i + 1})\) 。但是这样仍然不好做,考虑容斥,钦定若干对相邻的位置是祖先-后代关系,这样就连出了若干条链,比较方便 DP 计数。

但是此时还有哈希值的限制,经典的套路有两类:

  • 对每一位单独考虑,这样系数就是固定的。
  • 对每个数考虑,这样变量就是固定的。

由于链之间的位置关系,每一位是什么并不好处理,因此考虑后者,枚举点 \(p\) 计算其贡献。设 \(f_{u, i, j}\) 表示 \(u\) 子树内 \(p\) 左边钦定了 \(i\) 条链,右边钦定了 \(j\) 条链,并且固定左右内部顺序的贡献和。

首先有子树合并的转移,这个是 trivial 的,直接树上背包即可,注意左右边内部的链顺序任意要乘上组合数。

\(u = p\) ,则:

  • \(p\) 接在左边的最后一条链的末尾,系数为 \(-p\)
  • 在中间新开一条链,系数为 \(p\)

\(u \ne p\) ,则:

  • 在左边选一条链,把 \(u\) 接在这条链的末尾,系数为 \(-iB\)
  • 在左边插入一条只包含 \(u\) 的新链,系数为 \((i + 1) B\)
  • 在右边选一条链,把 \(u\) 接在这条链的末尾,系数为 \(-j\)
  • 在右边插入一条只包含 \(u\) 的新链,系数为 \(j + 1\)
  • \(u\) 接在 \(p\) 所在链的末尾,系数为 \(-1\) ,需要满足 \(p \in \mathrm{subtree}(u)\)

直接做可以做到 \(O(n^5)\) ,加一维表示 \(p\) 是否确定即可优化到 \(O(n^4)\)

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

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

int fa[N], siz[N], f[N][N][N][2], g[N][N][2], C[N][N];

int n, base;

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;
}

void dfs(int u) {
    siz[u] = 1, f[u][0][0][0] = 1;

    for (int v : G.e[u]) {
        dfs(v);
        memset(g, 0, sizeof(g));

        for (int i = 0; i <= siz[u]; ++i)
            for (int j = 0; i + j <= siz[u]; ++j)
                for (int k = 0; k <= 1; ++k)
                    if (f[u][i][j][k])
                        for (int x = 0; x <= siz[v]; ++x)
                            for (int y = 0; x + y <= siz[v]; ++y)
                                for (int z = 0; k + z <= 1; ++z)
                                    if (f[v][x][y][z])
                                        g[i + x][j + y][k + z] = add(g[i + x][j + y][k + z], 1ll * f[u][i][j][k] * 
                                            f[v][x][y][z] % Mod * C[i + x][i] % Mod * C[j + y][j] % Mod);

        memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
    }

    memset(g, 0, sizeof(g));

    for (int i = 0; i <= siz[u]; ++i)
        for (int j = 0; i + j <= siz[u]; ++j)
            if (f[u][i][j][0]) {
                if (i)
                    g[i - 1][j][1] = dec(g[i - 1][j][1], 1ll * u * f[u][i][j][0] % Mod);

                g[i][j][1] = add(g[i][j][1], 1ll * u * f[u][i][j][0] % Mod);
            }

    for (int i = 0; i <= siz[u]; ++i)
        for (int j = 0; i + j <= siz[u]; ++j)
            for (int k = 0; k <= 1; ++k)
                if (f[u][i][j][k]) {
                    g[i][j][k] = dec(g[i][j][k], 1ll * i * base % Mod * f[u][i][j][k] % Mod);
                    g[i + 1][j][k] = add(g[i + 1][j][k], 1ll * (i + 1) * base % Mod * f[u][i][j][k] % Mod);
                    g[i][j][k] = dec(g[i][j][k], 1ll * j * f[u][i][j][k] % Mod);
                    g[i][j + 1][k] = add(g[i][j + 1][k], 1ll * (j + 1) * f[u][i][j][k] % Mod);
                }

    for (int i = 0; i <= siz[u]; ++i)
        for (int j = 0; i + j <= siz[u]; ++j)
            g[i][j][1] = dec(g[i][j][1], f[u][i][j][1]);

    memcpy(f[u], g, sizeof(g));
}

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

    for (int i = 2; i <= n; ++i)
        scanf("%d", fa + i), G.insert(fa[i], i);

    C[0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        C[i][0] = 1;

        for (int j = 1; j <= i; ++j)
            C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
    }

    dfs(1);
    int ans = 0;

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

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

P11197 [COTS 2021] 赛狗游戏 Tiket

给出三个排列 \(a, b, c\) ,求有多少对 \((x, y)\) 满足 \(a_x < a_y \and b_x < b_y \and c_x < c_y\)

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

可以直接用 \(O(n \log^2 n)\) 的 cdq 分治卡常冲过去,但并不优美,考虑利用 \(a, b, c\) 都是排列的性质。

记满足某一维偏序的集合为 \(S\) ,则答案为:

\[|S_a \cap S_b \cap S_c| = \frac{1}{2}(|S_a \cap S_b| + |S_a \cap S_c| + |S_b \cap S_c| - |S_a \cup S_b \cup S_c|) \]

注意到任意 \((x, y)\)\((y, x)\) 恰有一者满足二维偏序,故 \(|S_a \cup S_b \cup S_c| = \frac{n(n - 1)}{2}\) ,于是只要 \(O(n \log n)\) 二维偏序求出前三者即可。

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

int t[N], a[N], b[N], c[N];

int n;

namespace BIT {
int c[N];

inline void clear() {
    memset(c + 1, 0, sizeof(int) * 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 ll solve(int *a, int *b) {
    vector<pair<int, int> > vec;

    for (int i = 1; i <= n; ++i)
        vec.emplace_back(a[i], b[i]);

    sort(vec.begin(), vec.end()), memset(BIT::c + 1, 0, sizeof(int) * n);
    ll ans = 0;

    for (auto it : vec)
        ans += BIT::query(it.second), BIT::update(it.second, 1);

    return ans;
}

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

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

    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        a[x] = i;
    }

    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        b[x] = i;
    }

    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        c[x] = i;
    }

    printf("%lld", (solve(a, b) + solve(a, c) + solve(b, c) - 1ll * n * (n - 1) / 2) / 2);
    return 0;
}

P11363 [NOIP2024] 树的遍历

给定一棵 \(n\) 个点的树,考虑定义一种以边为基础的遍历方式。

定义两条边相邻当且仅当它们有公共端点。初始时,所有边都未被标记。进行如下过程:

  • 选择一条边 \(b\) 作为起始边,将它打上标记。
  • 假设当前访问边为 \(e\) ,寻找任意一条与 \(e\) 相邻且未被标记的边 \(f\) ,将 \(f\) 作为新的访问边打上此标记,然后再次进入第二步。
  • 如果与 \(e\) 相邻的边都被标记,如果 \(e = b\) 则遍历结束,否则将 \(e\) 设为遍历 \(e\) 之前的上一条边,再次进入第二步。

显然这样能够遍历所有边。考虑构建一张新图,新图中的点与原图中的边一一对应,且每次进行第二步时,在新图中将 \(e\)\(f\) 对应的点连边。显然新图也是一棵树。

规定原树上的 \(k\) 条树边为关键边,求以这些边为起始边时,可能得到的本质不同的新树个数。

\(n \le 10^5\)

先考虑 \(k = 1\) 怎么做,不难发现到达一个点之后所有的出边顺序都是任意的,答案即为 \(\prod (\deg(i) - 1)!\)

再考虑 \(k = 2\) 的情况,若一棵新树会被二者同时统计到,则对于连接二者的链上的边,每对相邻的两条边在公共点的出边顺序中一定是一首一尾,因此链上的点的贡献为 \((\deg(i) - 2)!\) ,其余点的贡献不变,仍为 \((\deg(i) - 1)!\)

接下来考虑考虑容斥,统计同时被 \(i\) 条关键边统计到的新树的数量。

若不存在一条链覆盖这些关键边则显然方案数为 \(0\) ,因为此时虚树上存在三度点,无法钦定一首一尾。

因此只要考虑一条链上的边拿出来容斥统计方案,并且点的贡献只与首尾两条边的位置有关。考虑如此钦定方式:确定首尾两条边,然后中间任意钦定。若中间有 \(x\) 条边,则总的容斥系数为 \(\sum_{i = 0}^x \binom{x}{i} \times (-1)^{i + 2} = [x = 0]\) ,因此只需要对相邻的两条边容斥。

考虑方案计算,不难发现答案即为 \(\prod (\deg(i) - 1)!\) 乘上中间点的 \(\frac{\deg(i) - 2}{\deg(i) - 1}\) 之积,这样比较方便树上 DP。

\(f_u\) 表示考虑 \(u\) 子树与 \(u\) 父亲的边时要减去的积的贡献和,\(g_u\) 表示 \(u\) 子树内与 \(u\) 相邻的边与 \(u\) 产生的要减去的贡献和,转移不难做到线性,答案即为 \((m - f_1) \times \prod (\deg(i) - 1)!\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct Edge {
    int u, v;
} e[N];

int fac[N], inv[N], invfac[N];
int fa[N], val[N], f[N], g[N];
bool tag[N];

int n, 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;
    }
}

void dfs1(int u, int f) {
    fa[u] = f;

    for (int v : G.e[u])
        if (v != f)
            dfs1(v, u);
}

void dfs2(int u) {
    f[u] = g[u] = 0;
    int sum = 0;

    for (int v : G.e[u]) {
        if (v == fa[u])
            continue;

        dfs2(v);
        f[u] = add(f[u], add(f[v], 1ll * sum * g[v] % Mod * val[u] % Mod));
        sum = add(sum, g[v]);
    }

    if (tag[u])
        f[u] = add(f[u], 1ll * sum * val[u] % Mod), g[u] = 1;
    else
        g[u] = 1ll * sum * val[u] % Mod;
}

signed main() {
    prework();
    int testid, T;
    scanf("%d%d", &testid, &T);

    while (T--) {
    	scanf("%d%d", &n, &m);
        G.clear(n);

        for (int i = 1; i < n; ++i) {
    		scanf("%d%d", &e[i].u, &e[i].v);
            G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u);
        }

        dfs1(1, 0);
        memset(tag + 1, false, sizeof(bool) * n);

        for (int i = 1; i <= m; ++i) {
        	int id;
        	scanf("%d", &id);
            tag[e[id].v == fa[e[id].u] ? e[id].u : e[id].v] = true;
        }

        int all = 1;

        for (int i = 1; i <= n; ++i) {
            all = 1ll * all * fac[G.e[i].size() - 1] % Mod;

            if (G.e[i].size() >= 2)
                val[i] = 1ll * fac[G.e[i].size() - 2] * invfac[G.e[i].size() - 1] % Mod;
        }

        dfs2(1);
        printf("%d\n", 1ll * dec(m, f[1]) * all % Mod);
    }

    return 0;
}

P10681 [COTS 2024] 奇偶矩阵 Tablica

求满足如下条件的大小为 \(n \times m\) 的 01 矩阵数量:

  • 每行 \(1\) 的数量 \(\in \{ 1, 2 \}\)
  • 每列 \(1\) 的数量 \(\in \{ 1, 2 \}\)

\(n, m \le 5000\)

考虑枚举 \(1\) 的数量为 \(1, 2\) 的行有 \(a, b\) 个,\(1\) 的数量为 \(1, 2\) 的列有 \(c, d\) 个,由于 \(a + b = n\)\(c + d = m\)\(a + 2b = c + 2d\) ,因此枚举的量为 \(O(\min(n, m))\) 级别。

考虑对于固定的 \(c, d\) ,将这些 \(1\) 分配到行上满足 \(a, b\) 的限制,问题转化为:有 \(c + d\) 种颜色的球,其中有 \(c\) 种颜色只有一个,\(d\) 种颜色有两个,然后将其划分为 \(n\) 个非空集合,每个集合的大小 \(\le 2\) ,且内部球的颜色不同。

考虑将所有球排成一排,取点 \(a\) 个球各自组成集合,剩下按顺序相邻两个球放在一个集合中,但是此时会出现:

  • 将同色球放在同一集合中的错误情况:容斥即可。
  • 集合内部会带顺序(排成一排的统计天然带顺序),大小为 \(2\) 的集合会被统计两次:将方案除以 \(2^b\) 即可。

答案即为:

\[\sum_{a, b, c, d} \binom{n}{a} \binom{m}{b} \sum_{i = 0}^{\min(b, d)} (-1)^i \binom{b}{i} \binom{d}{i} i! \times \frac{(a + 2(b - i))!}{2^{d - i}} \times 2^{-b} \]

时间复杂度 \(O(\min(n, m)^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int N = 1e4 + 7;

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

int n, 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(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;
    }

    ipw[0] = 1;

    for (int i = 1; i <= n; ++i)
        ipw[i] = 1ll * ipw[i - 1] * inv2 % Mod;
}

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

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

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

    for (int b = 0; b <= n; ++b) {
        int a = n - b, d = (a + b * 2 - m), c = m - d, res = 0;

        if (d < 0 || c < 0)
            continue;

        for (int i = 0; i <= min(b, d); ++i)
            res = add(res, 1ll * sgn(i) * C(b, i) % Mod * C(d, i) % Mod * fac[i] % Mod * 
                fac[a + (b - i) * 2] % Mod * ipw[b + d - i] % Mod);

        ans = add(ans, 1ll * C(n, a) * C(m, c) % Mod * res % Mod);
    }

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

二项式反演

有四个形式:

\[\begin{aligned} 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) = \sum_{i = n}^m \dbinom{i}{n} g(i) &\iff g(n) = \sum_{i = n}^m (-1)^{i - n} \dbinom{i}{n} f(i) \end{aligned} \]

通常可以用于恰好 \(k\) 个和至少(钦定)\(k\) 个的转化。

CF285E Positions in Permutations

求长度为 \(n\) 、恰好有 \(m\) 个位置满足 \(|p_i - i| = 1\) 的排列 \(p_{1 \sim n}\) 的数量 \(\bmod (10^9 + 7)\)

\(n \le 1000\)

\(F(m)\) 为恰好 \(m\) 个位置满足条件的答案,\(G(m)\) 为钦定 \(m\) 个位置满足条件的答案,则:

\[G(m) = \sum_{i = m}^n \binom{i}{m} F(i) \]

二项式反演得到:

\[F(m) = \sum_{i = m}^n (-1)^{i - m} \binom{i}{m} G(i) \]

\(f_{i, j, 0/1, 0/1}\) 表示考虑前 \(i\) 个位置,钦定 \(j\) 个位置满足条件, \(i\)\(i + 1\) 是否被选的方案数。转移分类讨论 \(p_i\) 的取值:

  • \(p_i = i - 1\)\(f_{i, j, k, 0} \gets f_{i - 1, j - 1, 0, k}\)
  • \(p_i = i + 1\)\(f_{i, j, k, 1} \gets f_{i - 1, j - 1, 0, k} + f_{i - 1, j - 1, 1, k}\)
  • 其他情况:\(f_{i, j, k, 0} \gets f_{i - 1, j, 0, k} + f_{i - 1, j, 1, k}\)

特殊处理一下 \(i = 1, n\) 的情况即可,其中 \(G(i) = (n - i)! (f_{n, i, 0, 0} + f_{n, i, 1, 0})\) ,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e3 + 7;

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

int n, 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 int sgn(int n) {
    return n & 1 ? Mod - 1 : 1;
}

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;
    }
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d%d", &n, &m);
    prework(n);
    f[1][1][0][1] = f[1][0][0][0] = 1;

    for (int i = 2; i <= n; ++i) {
        f[i][0][0][0] = 1;

        for (int j = 1; j <= i; ++j)
            for (int k = 0; k <= 1; ++k) {
                f[i][j][k][0] = add(f[i - 1][j - 1][0][k], add(f[i - 1][j][0][k], f[i - 1][j][1][k]));

                if (i < n)
                    f[i][j][k][1] = add(f[i - 1][j - 1][0][k], f[i - 1][j - 1][1][k]);
            }
    }

    int ans = 0;

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

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

P4491 [HAOI2018] 染色

有一个长度为 \(n\) 的序列,可以给每个位置染上 \([1, m]\) 的颜色。若恰好出现 \(S\) 次的颜色有 \(k\) 种,则会获得 \(w_k\) 的价值。求所有染色方案的价值和。

\(n \le 10^7\)\(m \le 10^5\)\(S \le 150\)

不难发现当 \(k > \min(m, \lfloor \frac{n}{s} \rfloor)\)\(w_k\) 不会被统计,下令 \(n = \min(m, \lfloor \frac{n}{s} \rfloor)\)

考虑容斥,钦定 \(k\) 种颜色染 \(S\) 次,则:

\[f(k) = \binom{m}{k} \times \frac{n!}{(S!)^k (n - kS)!} \times (n - kS)^{m - k} \]

不难发现这个会算重,设恰好出现 \(S\) 次的颜色有 \(k\) 种的方案数为 \(g(k)\) ,则不难得到:

\[f(k) = \sum_{i = k}^n \binom{i}{k} g_i \]

二项式反演得到:

\[g(k) = \sum_{i = k}^n (-1)^{i - k} \binom{i}{k} f_i \]

拆开组合数后 NTT 做差卷积即可。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1004535809;
const int N = 1e7 + 7, M = 1e5 + 7;

int fac[N], inv[N], invfac[N];
int a[M], f[M], g[M], h[M];

int n, m, s;

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;
}

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;
}

namespace Poly {
#define cpy(f, g, n) memcpy(f, g, sizeof(int) * (n))
#define clr(f, n) memset(f, 0, sizeof(int) * (n))
const int rt = 3, invrt = 334845270;
const int S = 2e6 + 7;

int rev[S];

inline void calrev(int n) {
    for (int i = 1; i < n; ++i)
        rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? n >> 1 : 0);
}

inline int calc(int n) {
    int len = 1;

    while (len <= n)
        len <<= 1;

    return calrev(len), len;
}

inline void NTT(int *f, int n, int op) {
    for (int i = 0; i < n; ++i)
        if (i < rev[i])
            swap(f[i], f[rev[i]]);

    for (int k = 1; k < n; k <<= 1) {
        int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));

        for (int i = 0; i < n; i += k << 1) {
            int buf = 1;

            for (int j = 0; j < k; ++j) {
                int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
                f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
                buf = 1ll * buf * tG % Mod;
            }
        }
    }

    if (op == -1) {
        int invn = mi(n, Mod - 2);

        for (int i = 0; i < n; ++i)
            f[i] = 1ll * f[i] * invn % Mod;
    }
}

inline void Times(int *f, int *g, int n) {
    NTT(f, n, 1), NTT(g, n, 1);

    for (int i = 0; i < n; ++i)
        f[i] = 1ll * f[i] * g[i] % Mod;

    NTT(f, n, -1);
}

inline void Mul(int *f, int n, int *g, int m, int *res) {
    static int a[S], b[S];
    int len = calc(n + m - 1);
    cpy(a, f, n), clr(a + n, len - n);
    cpy(b, g, m), clr(b + m, len - m);
    Times(a, b, len), cpy(res, a, n + m - 1);
}
#undef cpy
#undef clr
} // namespace Poly

signed main() {
    prework();
    n = read(), m = read(), s = read();

    for (int i = 0; i <= m; ++i)
        a[i] = read();

    int lim = min(m, n / s);

    for (int i = 0; i <= lim; ++i) {
        f[i] = 1ll * C(m, i) * fac[n] % Mod * mi(invfac[s], i) % Mod * 
            invfac[n - s * i] % Mod * mi(m - i, n - s * i) % Mod * fac[i] % Mod;
        h[i] = 1ll * sgn(i) * invfac[i] % Mod;
    }

    reverse(f, f + lim + 1);
    Poly::Mul(f, lim + 1, h, lim + 1, g);
    reverse(g, g + lim + 1);
    int ans = 0;

    for (int i = 0; i <= lim; ++i)
        ans = add(ans, 1ll * g[i] * invfac[i] % Mod * a[i] % Mod);

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

P10004 [集训队互测 2023] Permutation Counting 2

给定 \(n\) ,对于每组 \(x, y \in [0, n - 1]\) ,求满足 \(\sum_{i = 1}^{n - 1} [p_i < p_{i + 1}] = x\)\(\sum_{i = 1}^{n - 1} [p^{-1}_i < p^{-1}_{i + 1}] = y\)\(1 \sim n\) 的排列 \(p\) 的数量。

\(n \le 500\)

\(f_{i, j}\) 表示在原排列和逆排列分别钦定 \(i, j\) 上升的连续对的方案数,则:

\[f_{i, j} = \sum_{a \ge i} \sum_{b \ge j} \binom{a}{i} \binom{b}{j} ans_{a, b} \]

考虑二元二项式反演,形式与一元类似:

\[ans_{i, j} = \sum_{a \ge i} \sum_{b \ge j} \binom{a}{i} \binom{b}{j} (-1)^{a - i + b - j} f_{a, b} \]

求出 \(f_{i, j}\) 后前缀和优化即可做到 \(O(n^3)\)

在原排列和逆排列分别钦定 \(i, j\) 上升的连续对,等价于分别钦定 \(n - i, n - j\) 个连续的上升段。

观察 \(p^{-1}\) 上一个连续的上升段,将其对应的下标区间 \([l, r]\) 依次插入 \(p\) 中的 \(i\) 个连续的上升段,只关心每个段插了多少元素,则每一种分配方式恰好对应一组 \(p\)

问题转化为计数有多少大小为 \(i \times j\) 的矩阵满足每行每列和非零,且总元素和为 \(n\)

考虑容斥,钦定 \(a\) 个行 \(b\) 个列为 \(0\) ,不难插板法直接计算。

时间复杂度 \(O(n^3)\) ,可能需要一些卡常。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7, M = 3e5 + 7;

int fac[M], inv[M], invfac[M], c[N][N], f[N][N], g[N][N], s[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;
}

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

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;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d%d", &n, &Mod), prework(n * (n + 1));

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

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            g[i][j] = C(n + i * j - 1, i * j - 1);

    for (int i = 1; i <= n; ++i) {
        memset(s + 1, 0, sizeof(int) * n);

        for (int j = 1; j <= n; ++j)
            for (int k = 1; k <= i; ++k)
                s[j] = add(s[j], 1ll * sgn(i - k) * c[i][k] % Mod * g[k][j] % Mod);

        for (int j = 1; j <= n; ++j)
            for (int k = 0; k <= j; ++k)
                f[i][j] = add(f[i][j], 1ll * sgn(j - k) * c[j][k] % Mod * s[k] % Mod);
    }

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

    memset(f, 0, sizeof(f));

    for (int i = 0; i < n; ++i) {
        memset(s, 0, sizeof(int) * n);

        for (int j = 0; j < n; ++j)
            for (int k = i; k < n; ++k)
                s[j] = add(s[j], 1ll * sgn(k - i) * c[k][i] % Mod * g[k][j] % Mod);

        for (int j = 0; j < n; ++j)
            for (int k = j; k < n; ++k)
                f[i][j] = add(f[i][j], 1ll * sgn(k - j) * c[k][j] % Mod * s[k] % Mod);
    }

    for (int i = 0; i < n; ++i, puts(""))
        for (int j = 0; j < n; ++j)
            printf("%d ", f[i][j]);

    return 0;
}

子集反演

本质就是容斥,式子为:

\[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) \]

P8329 [ZJOI2022] 树

给定 \(N\) 和模数 \(M\) ,对于所有 \(n = 2, 3, \cdots, N\) ,求满足以下条件的树的二元组 \((T_1, T_2)\) 的方案数:

  • \(T_1\) 中每个点的父亲编号均小于自己。
  • \(T_2\) 中每个点的父亲编号均大于自己。
  • \(T_1\) 的叶子集合与 \(T_2\) 的非叶子集合相同,\(T_1\) 的非叶子集合与 \(T_2\) 的叶子集合相同。

\(n \le 500\)

设:

  • \(F(S)\) 表示 \(T_1\) 的非叶子集合恰好为 \(S\)\(T_1\) 的方案数。
  • \(G(S)\) 表示 \(T_2\) 的非叶子集合恰好为 \(S\)\(T_2\) 的方案数。

答案即为:

\[\sum_{S \cap T = \empty, S \cup T = \{ 1, 2, \cdots, n \}} F(S) G(T) \]

考虑子集反演,设:

  • \(f(S)\) 表示 \(T_1\) 的非叶子集合为 \(S\) 的子集时 \(T_1\) 的方案数,则 \(f(S) = \sum_{S' \subseteq S} F(S) \iff F(S) = \sum_{S' \subseteq S} (-1)^{|S| - |S'|} f(S)\)
  • \(g(S)\) 表示 \(T_2\) 的非叶子集合为 \(S\) 的子集时 \(T_2\) 的方案数,则 \(g(S) = \sum_{S' \subseteq S} G(S) \iff G(S) = \sum_{S' \subseteq S} (-1)^{|S| - |S'|} g(S)\)

答案即为:

\[\begin{aligned} & \sum_{S \cap T = \empty, S \cup T = \{ 1, 2, \cdots, n \}} F(S) G(T) \\ =& \sum_{S \cap T = \empty, S \cup T = \{ 1, 2, \cdots, n \}} \left( \sum_{S' \subseteq S} (-1)^{|S| - |S'|} f(S') \right) \left( \sum_{T' \subseteq T} (-1)^{|T| - |T'|} g(T') \right) \\ =& \sum_{S \cap T = \empty, S \cup T = \{ 1, 2, \cdots, n \}} \sum_{S' \subseteq S, T' \subseteq T} (-1)^{|S| - |S'| + |T| - |T'|} f(S') g(T') \\ =& \sum_{S \cap T = \empty} f(S) g(T) (-1)^{n - |S| - |T|} 2^{n - |S| - |T|} \\ =& \sum_{S \cap T = \empty} (-2)^{n - |S| - |T|} f(S) g(T) \\ \end{aligned} \]

考虑已知 \(S\) 时如何计算 \(f(S)\) ,对于 \(i \in (1, n]\)\(i\) 的父亲只可能是 \([1, i)\) 中钦定的非叶子节点,\(g(T)\) 同理。

\(f_{i, j, k}\) 表示确定了 \(1 \sim i\) 的点集在 \(S\) 中还是在 \(T\) 中,其中 \(S\)\(1 \sim i\) 放了 \(j\) 个、\(T\)\(i + 1 \sim n\) 放了 \(k\) 个,且确定了 \((1, i]\)\(T_1\) 中的父亲、\([1, i)\)\(T_2\) 中的父亲的方案数。转移时分类讨论 \(i\) 的归属与 \(i\)\(T_1, T_2\) 中的父亲(只能接在钦定的非叶子上):

  • \(i \in S\)\(f_{i - 1, j, k} \times j \times k \to f_{i, j + 1, k}\)
  • \(i \in T\)\(f_{i - 1, j, k} \times j \times k \to f_{i, j, k - 1}\)
  • \(i \notin S \cup T\)\(f_{i - 1, j, k} \times (-2 \times j \times k) \to f_{i, j, k}\)

时间复杂度 \(O(n^3)\)

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

int f[N][N][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);
    fill(f[1][1] + 1, f[1][1] + n, 1);

    for (int i = 2; i <= n; ++i) {
        for (int j = 1; j < i; ++j)
            for (int k = 1; k <= n - (i - 1); ++k) {
                if (!f[i - 1][j][k])
                    continue;

                f[i][j][k] = add(f[i][j][k], 1ll * (Mod - 2) * j % Mod * k % Mod * f[i - 1][j][k] % Mod);
                f[i][j + 1][k] = add(f[i][j + 1][k], 1ll * j * k % Mod * f[i - 1][j][k] % Mod);
                f[i][j][k - 1] = add(f[i][j][k - 1], 1ll * j * k % Mod * f[i - 1][j][k] % Mod);
            }

        int ans = 0;

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

        printf("%d\n", ans);
    }

    return 0;
}

posted @ 2025-07-25 21:26  wshcl  阅读(42)  评论(0)    收藏  举报