用 L_h[m] 记「内部结点数 m、高度不超过 h」的家谱数,一次卷积抬高一层高度上界,最后 L_K[m]-L_{K-1}[m] 就是高度恰为 K 的个数,O(K·N²)。
OJ: roj
题目 ID: 2031
难度:普及+/提高-
标签:动态规划计数组合计数递推树usacopython
创建: 2026-10-01 04:00
更新: 2026-10-07 13:50
形式化题目
统计有序满二叉树的形状数:每个结点的孩子数只能是
这里的高度按题面定义,是根到叶子的最长路径上的结点数:单结点树高度为
下图画出一组答案的两个家谱,帮助确认上面这两条约定。
输入
再补一条后面反复用到的换算:满二叉树中每个内部结点恰好有
正解
思路
朴素做法为什么不行。 最直接的想法是枚举所有满二叉树形状、按高度筛选。
m 个内部结点的满二叉树有 Catalan 数
只按规模计数还不够。 设
算出来正是 Catalan 数:5 3 的全部家谱数,9 4 的全部家谱数。
但题目要的是高度恰好为 9 4 的答案是
关键观察:把"高度恰好"换成"高度不超过"。 加高度维度的直觉写法是
"高度恰好为
先看边界:内部结点数为
高度不超过
边界 L_1 之外不需要 L_1 单独给定。
相减得到"恰好为
真实计数满足
下面是小规模的真实数值表,行是高度上界、列是内部结点数,可以直接核对上面的推导。
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 | 0 | |
| 1 | 1 | 0 | 0 | 0 | 0 | |
| 1 | 1 | 2 | 1 | 0 | 0 | |
| 1 | 1 | 2 | 5 | 6 | 6 | |
| 1 | 1 | 2 | 5 | 14 | 26 | |
| 0 | 0 | 0 | 0 | 8 | 20 | |
| 0 | 0 | 0 | 4 | 6 | 6 |
读表方式:单元格 5 3 对应 9 4 对应
两件事可以互相印证:同一列的数值随着高度上界升高而不减,到
实现上只需要滚动两个向量。 第 below 和 upto 分别指向 upto 卷积一次抬高一层(代码里这一次卷积就是函数 lift),
执行 upto[m] - below[m] 就是答案。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 04:00
# update_at: 2026-10-01 04:14
import sys
MOD = 9901 # 题面要求的模数;它不是质数,但本题只用加减乘,不需要逆元
def lift(F: list[int]) -> list[int]:
"""把「高度不超过 h」的计数向量变成「高度不超过 h+1」的计数向量(h >= 1)。
高度不超过 h+1 的树去掉根,就是左右两棵高度都不超过 h 的子树,
于是按内部结点数做一次卷积:G[m] = Σ_{a+b=m-1} F[a]·F[b]。
下标是内部结点数,下标 0 表示那个位置是叶子;左右两边都占一个位置,
所以 G[0] 恒为 1(由列表头的那个常量给出)。
"""
return [1] + [
sum(F[a] * F[i - 1 - a] for a in range(i)) % MOD for i in range(1, len(F))
]
def solve() -> None:
n, k = map(int, sys.stdin.buffer.read().split())
if n % 2 == 0: # 每个内部结点恰好贡献两个孩子,总结点数必为奇数
print(0)
return
m = (n - 1) // 2 # 内部结点数;叶子恰好比它多 1 片,两者相加才是 n
# L_h[m] = 内部结点数恰为 m、高度不超过 h 的家谱个数。
# 内部结点数为 0 的树就是那片叶子,高度是 1,所以 base 就是 L_1;
# below / upto 滚动保存 L_{k-1} 与 L_k,两者相减就只剩高度恰好为 k 的那些树。
base = [1] + [0] * m # L_1:只有那片叶子,m > 0 的树高度都超过 1
below, upto = base, base.copy()
for _ in range(k - 1): # 每轮把高度上界抬高一层:L_{i+1} = lift(L_i)
below, upto = upto, lift(upto)
print((upto[m] - below[m]) % MOD)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 09:25
* update_at: 2026-10-06 09:25
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXM = 105;
const ll MOD = 9901;
ll n, k; // 结点总数 N 与高度上界 K
ll m; // 内部结点数 m = (N-1)/2,叶子数比它多 1
ll below[MAXM]; // L_{h-1}[i]:内部结点数 i、高度不超过 h-1 的家谱数
ll upto[MAXM]; // L_h[i]:内部结点数 i、高度不超过 h 的家谱数
ll nxt[MAXM]; // 抬高一层得到的中间向量 L_{h+1}
// 把高度不超过 h 的计数向量 F 卷积一次,得到高度不超过 h+1 的向量 G。
// 高度不超过 h+1 的树去掉根,左右子树高度都不超过 h,于是
// G[i] = Σ_{a+b=i-1} F[a]*F[b],下标是内部结点数。
void lift(ll F[], ll G[]) {
G[0] = 1; // 内部结点数 0 的那棵树(叶子)高度为 1,任何上界都包含它
for (ll i = 1; i <= m; i++) {
ll sum = 0;
for (ll a = 0; a <= i - 1; a++) {
sum = (sum + F[a] * F[i - 1 - a]) % MOD;
}
G[i] = sum;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
// 每个内部结点恰好有两个孩子,结点数必为奇数:n = m + (m+1) = 2m+1
if (n % 2 == 0) {
cout << 0 << "\n";
return 0;
}
m = (n - 1) / 2; // 内部结点数
// L_1:只有那片叶子,内部结点数大于 0 的树高度都超过 1
for (ll i = 0; i <= m; i++) below[i] = 0;
below[0] = 1;
for (ll i = 0; i <= m; i++) upto[i] = below[i];
// 每轮把高度上界抬高一层,共 K-1 轮;
// 结束后 below = L_{K-1},upto = L_K。
for (ll h = 1; h <= k - 1; h++) {
lift(upto, nxt);
for (ll i = 0; i <= m; i++) {
below[i] = upto[i];
upto[i] = nxt[i];
}
}
// 高度不超过 K 的减去高度不超过 K-1 的,剩下的正好高度为 K
ll ans = ((upto[m] - below[m]) % MOD + MOD) % MOD;
cout << ans << "\n";
return 0;
}复杂度
- 时间复杂度:每层做一次长度
的卷积,共 次乘法,抬高 层, 总计 ,其中 。 代入最大数据 ,约 次乘法,本机实测不到 0.04 秒。 - 空间复杂度:只保存相邻两层的计数向量,
个计数器。
总结
- 换算:满二叉树满足
( 是内部结点数),所以 为偶数时答案为 。 - 建模:不要枚举形状,而是在"规模 + 高度上界"这两个维度上计数。
- 关键一步:用"高度不超过
"而不是"高度恰好为 "作状态, 去掉根的左右子树就共享同一个上界 ,约束退化成一次纯卷积 ,而不是"枚举左右子树高度搭配"。 递推从 (只有那片叶子)起用。 - 收尾:“高度恰好为
” ,两个上界层相减即可, 模意义下同样成立,因为真实计数单调不减。 - 易错点:高度按结点数计(单结点树高度为
),且左右孩子有区别; 上界层数从 开始( 是只有叶子的那层),高度还能超过内部结点数 (链有 )。这些都能在样例 5 3 → 2、9 4 → 6上验证。