Galgame

GitHub跳转原题关系图返回列表

把题目的“更有趣”关系看成所有本质不同有序二叉树的全序,先按结点数分类,再递归计算同大小树中的字典序排名。

OJ: luogu

题目 ID: P7118

难度:提高+/省选-

标签:递归组合计数Catalan排名

日期: 2026-06-20 08:59

题意

把一款 Galgame 看成一棵有序二叉树:

  • 每个结点是一个场景
  • 左儿子表示选 A 后到达的场景
  • 右儿子表示选 B 后到达的场景
  • 0 表示空场景

题目定义了两棵树谁更有趣:

  1. 先比较可达场景总数
  2. 场景总数相同就比较左子树
  3. 左子树也相同再比较右子树

要求输出当前这棵树前面有多少个本质不同、并且更不有趣的 Galgame,答案对 998244353 取模。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

int n;
int lc[25], rc[25];

struct Node {
    int left;
    int right;
};

vector<Node> trees[15];
map<tuple<int, int, int>, int> id_of[15];

void init_all_trees(int max_size) {
    trees[0].push_back({-1, -1});

    for (int sz = 1; sz <= max_size; sz++) {
        for (int left_size = 0; left_size <= sz - 1; left_size++) {
            int right_size = sz - 1 - left_size;
            for (int i = 0; i < (int) trees[left_size].size(); i++) {
                for (int j = 0; j < (int) trees[right_size].size(); j++) {
                    tuple<int, int, int> state = make_tuple(left_size, i, j);
                    if (id_of[sz].count(state)) {
                        continue;
                    }
                    id_of[sz][state] = (int) trees[sz].size();
                    trees[sz].push_back({i, j});
                }
            }
        }
    }
}

int calc_size_from_input(int u) {
    if (u == 0) {
        return 0;
    }
    return calc_size_from_input(lc[u]) + calc_size_from_input(rc[u]) + 1;
}

int build_id_from_input(int u) {
    if (u == 0) {
        return 0;
    }

    int left_size = calc_size_from_input(lc[u]);
    int right_size = calc_size_from_input(rc[u]);
    int left_id = build_id_from_input(lc[u]);
    int right_id = build_id_from_input(rc[u]);
    int total_size = left_size + right_size + 1;

    return id_of[total_size][make_tuple(left_size, left_id, right_id)];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> lc[i] >> rc[i];
    }

    init_all_trees(n);

    int total_size = calc_size_from_input(1);
    int rank_in_same_size = build_id_from_input(1);

    i64 ans = 0;
    for (int i = 1; i < total_size; i++) {
        ans += (int) trees[i].size();
    }
    ans += rank_in_same_size;

    cout << ans << '\n';

    return 0;
}

这个暴力会把所有本质不同的小规模有序二叉树全部生成出来,再按题目规则给它们编号。

所以这题本质上就是求:

  • 当前这棵有序二叉树
  • 在所有本质不同有序二叉树组成的全序里
  • 排名是多少

关键拆分

答案可以拆成两部分:

  1. 结点数更小的所有树数量
  2. 与当前树结点数相同,但更不有趣的树数量

第一部分很好办,因为有序二叉树的本质不同结构数正是 Catalan 数。

设:

  • cat[i] 表示 i 个结点的本质不同有序二叉树数量

那么第一部分就是:

cat[1] + cat[2] + ... + cat[sz[root]-1]

同大小内部排名

设当前结点 u

  • 左子树大小是 ls
  • 右子树大小是 rs

定义:

  • rank[u]u 这棵树在所有大小为 sz[u] 的树中的排名,从 0 开始

则同大小下所有更不有趣的树分成三段:

部分 数量
左子树大小更小 sum cat[x] * cat[sz[u]-1-x]
左子树大小相同,但左子树排名更小 rank[left] * cat[rs]
左子树完全相同,右子树排名更小 rank[right]

所以:

rank[u] = sum_{x=0}^{ls-1} cat[x] * cat[sz[u]-1-x] + rank[left] * cat[rs] + rank[right]

递推公式与排名公式

对每个结点 u,需要维护两个量:

sz[u]=sz[lc[u]]+sz[rc[u]]+1 sz[u] = sz[lc[u]] + sz[rc[u]] + 1
rank[u]=x=0ls1cat[x]cat[sz[u]1x]+rank[lc[u]]cat[rs]+rank[rc[u]] rank[u] = \sum_{x=0}^{ls-1} cat[x] \cdot cat[sz[u]-1-x] + rank[lc[u]] \cdot cat[rs] + rank[rc[u]]

最终答案是:

pre_cat[sz[1]1]+rank[1] pre\_cat[sz[1]-1] + rank[1]

第一项如果直接枚举,在极端结构下会很慢。

这里继续利用 Catalan 总和:

cat[sz[u]] = sum_{x=0}^{sz[u]-1} cat[x] * cat[sz[u]-1-x]

如果左子树比较大,就改成“总数减补集”,于是每个结点只需要枚举左右子树里较小的一边。

最后答案就是:

pre_cat[sz[root]-1] + rank[root]

其中 pre_cat[i]cat 的前缀和。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

const int MAXN = 1000000 + 5;
const i64 MOD = 998244353LL;

int n;
int lc[MAXN], rc[MAXN];
int sz[MAXN];          // sz[u] 表示以 u 为根能到达的场景总数
i64 cat[MAXN];         // cat[i] 表示 i 个结点的本质不同二叉树数量(Catalan 数)
i64 pre_cat[MAXN];     // pre_cat[i] = cat[1] + ... + cat[i]
i64 rank_in_size[MAXN];// rank_in_size[u] 表示在“结点数相同”的所有本质不同 Galgame 中的排名(从 0 开始)
i64 inv[MAXN];

inline int read_int() {
    int x = 0;
    int f = 1;
    int ch = getchar();

    while (ch != '-' && (ch < '0' || ch > '9')) {
        ch = getchar();
    }
    if (ch == '-') {
        f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}

int main() {
    n = read_int();
    for (int i = 1; i <= n; i++) {
        lc[i] = read_int();
        rc[i] = read_int();
    }

    // 预处理 Catalan 数。
    inv[1] = 1;
    for (int i = 2; i <= n + 1; i++) {
        inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
    }

    cat[0] = 1;
    for (int i = 1; i <= n; i++) {
        cat[i] = cat[i - 1] * (4LL * i - 2) % MOD;
        cat[i] = cat[i] * inv[i + 1] % MOD;
        pre_cat[i] = (pre_cat[i - 1] + cat[i]) % MOD;
    }

    // 非递归后序遍历,避免深链爆栈。
    vector<int> order;
    order.reserve(n);
    vector<int> st;
    st.reserve(n);
    st.push_back(1);

    while (!st.empty()) {
        int u = st.back();
        st.pop_back();
        order.push_back(u);

        if (lc[u] != 0) {
            st.push_back(lc[u]);
        }
        if (rc[u] != 0) {
            st.push_back(rc[u]);
        }
    }

    for (int i = (int) order.size() - 1; i >= 0; i--) {
        int u = order[i];
        sz[u] = sz[lc[u]] + sz[rc[u]] + 1;
    }

    for (int i = (int) order.size() - 1; i >= 0; i--) {
        int u = order[i];
        int left_size = sz[lc[u]];
        int right_size = sz[rc[u]];
        int total_size = sz[u];

        i64 ans = 0;

        // 第一段:
        // 左子树大小更小的所有情况。
        // 利用 Catalan 总和,只枚举更小的一边。
        if (left_size <= right_size) {
            for (int x = 0; x < left_size; x++) {
                ans += cat[x] * cat[total_size - 1 - x] % MOD;
                if (ans >= MOD) {
                    ans -= MOD;
                }
            }
        }
        else {
            ans = cat[total_size];
            for (int y = 0; y <= right_size; y++) {
                ans -= cat[total_size - 1 - y] * cat[y] % MOD;
                if (ans < 0) {
                    ans += MOD;
                }
            }
        }

        // 第二段:左子树大小相同,但左子树本身更不有趣。
        ans += rank_in_size[lc[u]] * cat[right_size] % MOD;
        ans %= MOD;

        // 第三段:左子树完全相同,再比较右子树。
        ans += rank_in_size[rc[u]];
        ans %= MOD;

        rank_in_size[u] = ans;
    }

    // 有趣度 = 所有结点数更少的本质不同 Galgame 数量
    //        + 在同样结点数下更不有趣的数量。
    i64 ans = pre_cat[sz[1] - 1] + rank_in_size[1];
    ans %= MOD;

    printf("%lld\n", ans);

    return 0;
}

复杂度

  • 预处理 Catalan 数 O(n)O(n)
  • 两次 DFS O(n)O(n)
  • 排名计算部分整体 O(nlogn)O(n log n)
  • 额外使用若干长度为 n 的数组
  • 总空间复杂度 O(n)O(n)

总结

这题最关键的是把原题的“有趣度比较”翻译成一棵有序二叉树的全序排名问题。

一旦看出这一点,后面就只是在做:

  • Catalan 数计数
  • 同大小树的递归字典序排名

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析