把题目的“更有趣”关系看成所有本质不同有序二叉树的全序,先按结点数分类,再递归计算同大小树中的字典序排名。
OJ: luogu
题目 ID: P7118
难度:提高+/省选-
标签:树递归组合计数Catalan排名
日期: 2026-06-20 08:59
题意
把一款 Galgame 看成一棵有序二叉树:
- 每个结点是一个场景
- 左儿子表示选 A 后到达的场景
- 右儿子表示选 B 后到达的场景
0表示空场景
题目定义了两棵树谁更有趣:
- 先比较可达场景总数
- 场景总数相同就比较左子树
- 左子树也相同再比较右子树
要求输出当前这棵树前面有多少个本质不同、并且更不有趣的 Galgame,答案对 998244353 取模。
思路
先看一个可以直接验证想法的朴素解:
#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;
}这个暴力会把所有本质不同的小规模有序二叉树全部生成出来,再按题目规则给它们编号。
所以这题本质上就是求:
- 当前这棵有序二叉树
- 在所有本质不同有序二叉树组成的全序里
- 排名是多少
关键拆分
答案可以拆成两部分:
- 结点数更小的所有树数量
- 与当前树结点数相同,但更不有趣的树数量
第一部分很好办,因为有序二叉树的本质不同结构数正是 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,需要维护两个量:
最终答案是:
第一项如果直接枚举,在极端结构下会很慢。
这里继续利用 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 的前缀和。
代码
#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 数
- 两次 DFS
- 排名计算部分整体
- 额外使用若干长度为
n的数组 - 总空间复杂度
总结
这题最关键的是把原题的“有趣度比较”翻译成一棵有序二叉树的全序排名问题。
一旦看出这一点,后面就只是在做:
- Catalan 数计数
- 同大小树的递归字典序排名
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。


