GESP 六级备考 · 洛谷 P1210
给定一棵有根二叉树(根为 1),每个节点有左右儿子。问:所有 n 棵子树中,有多少棵是完全二叉树?
输入:第一行 n,接下来 n 行每行两个整数 li, ri(0 表示无此儿子)。
输出:一个整数——完全二叉子树的数量。
对每个节点 u,判断以 u 为根的子树是否是完全二叉树。朴素做法是对每棵子树单独检查,但那样是 O(n²)。我们要找一种 O(n) 的方法——一次 DFS 搞定所有节点。
完全二叉树是二叉树中一种"排得整整齐齐"的结构,它的定义是:
1. 除最后一层外,每一层都被完全填满(节点数达到最大值)。
2. 最后一层的节点从左到右排列,中间不能有空位。
简单说:就像一个队列,从上到下、从左到右依次填满,最后一层可以没填完,但不能"跳着"填。
点击每张卡片,看看对不对。注意最后一层节点的排列方式。
"层满左对齐" —— 除最后一层全满,最后一层从左到右不留空。这就是完全二叉树。
满二叉树(Perfect Binary Tree):每个非叶子节点都有两个子节点,所有叶子在同一层。它是完全二叉树的特例。
完全二叉树(Complete Binary Tree):允许最后一层不满,但必须从左到右排满。范围更宽。
所以:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
怎么用一次 DFS 判断所有子树?秘诀是:对每个节点维护三个属性,后序遍历时自底向上算出来。
子树的高度(最深叶子到 u 的距离)。叶子高度 = 0。
子树是否是满二叉树(所有层都填满)。
子树是否是完全二叉树。这是我们最终要的答案。
根据节点 u 的子节点情况,分四种情形:
| 情形 | height | isPerfect | isComplete |
|---|---|---|---|
| 叶子(无子) | 0 | true | true |
| 只有左子 l | h[l]+1 | false | h[l]==0(左子是叶子) |
| 只有右子 r | h[r]+1 | false | false |
| 有左右子 l, r | max(h[l],h[r])+1 | perf[l] && perf[r] && h[l]==h[r] | 见下方 |
设 hL = h[l],hR = h[r]:
1. hL == hR(左右等高):左子树必须满,右子树可以"缺"最后一层。
isComplete = isPerfect[l] && isComplete[r]
2. hL == hR + 1(左比右高一层):左子树可以"缺"最后一层,右子树必须满。
isComplete = isComplete[l] && isPerfect[r]
3. 其他(高度差过大):isComplete = false
想象从上到下、从左到右给节点编号。当左右子树等高时,最后一层横跨两棵子树。如果左子树有"缺口"(不满),而右子树还有节点,就违反了"从左到右排满"——左边的位置空着,右边却有节点。所以等高时左子树必须满(isPerfect),右子树可以缺(isComplete)。
而左比右高一层时,最后一层只在左子树里,右子树是倒数第二层——必须满。左子树可以缺(isComplete),右子树必须满(isPerfect)。
用样例 2 的树(有一棵子树不是完全二叉树)。点击任意节点,右侧面板会显示它的高度、isPerfect、isComplete。
点击树上的节点
选择一个样例,看后序 DFS 如何一步步算出每个节点的三个属性。
等待动画开始...
核心就是一个后序 DFS,对每个节点算三个属性,最后数 isComplete 为 true 的节点。
// 完全二叉子树计数 - O(n) 后序 DFS #include <iostream> using namespace std; const int N = 100005; int n, lc[N], rc[N]; int h[N]; // 子树高度 bool perf[N], comp[N]; // isPerfect, isComplete void dfs(int u) { int l = lc[u], r = rc[u]; if (l == 0 && r == 0) { // 叶子节点 h[u] = 0; perf[u] = true; comp[u] = true; } else if (l != 0 && r == 0) { // 只有左子 dfs(l); h[u] = h[l] + 1; perf[u] = false; comp[u] = (h[l] == 0); // 左子必须是叶子 } else if (l == 0 && r != 0) { // 只有右子 → 不可能完全 dfs(r); h[u] = h[r] + 1; perf[u] = false; comp[u] = false; } else { // 有左右子 dfs(l); dfs(r); h[u] = max(h[l], h[r]) + 1; perf[u] = perf[l] && perf[r] && (h[l] == h[r]); if (h[l] == h[r]) { comp[u] = perf[l] && comp[r]; } else if (h[l] == h[r] + 1) { comp[u] = comp[l] && perf[r]; } else { comp[u] = false; } } } int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> lc[i] >> rc[i]; dfs(1); int cnt = 0; for (int i = 1; i <= n; i++) if (comp[i]) cnt++; cout << cnt << endl; return 0; }
时间复杂度:O(n) —— 每个节点只访问一次。
空间复杂度:O(n) —— 数组 + 递归栈。对于 n = 10⁵,最坏情况(退化为链)递归深度可达 10⁵,可能在某些评测机上爆栈。如果遇到这个问题,可以改成迭代式后序遍历。
这是一棵更大的树(9 个节点)。点击任意节点,查看它的子树属性。点击"显示所有结果"可以一键看到哪些子树是完全二叉树。
点击树上的节点
9
2 3
4 5
0 6
7 8
0 0
9 0
0 0
0 0
0 0
你可以把这段输入复制到上面的代码里跑一跑,验证你的判断。
做做下面的题,检验你对完全二叉树和递归判定的理解。