GESP 六级 · 2026-03 真题 · 树形 DP

P15801 完全二叉树
不是只判断整棵树,而是判断每个结点的子树

这节课抓一个核心:每个结点只保留高度、是否满、是否完全三件事;左右子树信息已知后,当前结点就能 O(1) 合并。

一、先回答问题

结论

对每个结点 u,后序遍历先处理左右儿子,再合并当前结点。

h[u] = max(h[L], h[R]) + 1
full[u] = full[L] && full[R] && h[L] == h[R]
complete[u] = (complete[L] && full[R] && h[L] == h[R] + 1)
              || (full[L] && complete[R] && h[L] == h[R])
教学安排
  1. 先区分满二叉树和完全二叉树。
  2. 用两种合法形状解释转移式。
  3. 互动演示:点一个结点,看它如何由左右子树合并。
  4. 最后读代码,重点看空树初始化和后序顺序。

二、核心观察

状态 1

h[u]:以 u 为根的子树高度。

它告诉我们左右两边谁高、高多少。

状态 2

full[u]:这棵子树是不是满二叉树。

完全二叉树的判断里经常需要“一边必须满”。

状态 3

complete[u]:这棵子树是不是完全二叉树。

答案就是所有 complete[u] 为真的结点个数。

为什么这三个信息够用

完全二叉树只关心形状:最后一层必须从左到右连续出现。判断当前结点时,不需要重新扫整棵子树;只要知道左右子树是否满、是否完全、以及高度,就能确认有没有“中间空洞”。

信息压缩:把一整棵子树压缩成 高度 + 满 + 完全,不会丢失判断当前形状所需的信息。
常见错法:只看左右高度差不超过 1。这只能排除一部分坏树,不能保证最后一层靠左。

三、互动演示

点“下一步”按后序顺序合并。蓝色是已经处理好的子树,绿色是当前结点,虚线是空儿子。

当前合并
结点
-
高度 h
-
是否计入答案
-
样例 1 用来演示后序合并。点下一步,从叶子开始把每棵子树压缩成三个状态。
位置hfullcomplete

四、自己选答案

Q1 为什么要用后序遍历?
Q2 空树为什么设成 full=true 且 complete=true?
Q3 哪个条件表示“左右一样高时当前是完全二叉树”?

五、手推结果

样例 1
1
├── 2
│   └── 4
└── 3

四个结点的子树都是完全二叉树,答案是 4

样例 2
1
├── 2
└── 3
    └── 4

结点 3 的子树不是完全二叉树,其余三个结点是,答案是 3

六、C++14 参考代码

读代码先读这三处
空树初始化
full_tree[0]complete_tree[0] 必须设为 true。
后序顺序
dfs(lch[u]),再 dfs(rch[u]),最后算 u
答案来源
每个结点只处理一次,若 complete_tree[u] 为真就加一。
#include <bits/stdc++.h>
using namespace std;

const int N = 100000 + 5;

int lch[N], rch[N];
int h[N];
bool full_tree[N], complete_tree[N];
int ans = 0;

void dfs(int u) {
    if (u == 0) return;

    dfs(lch[u]);
    dfs(rch[u]);

    int L = lch[u], R = rch[u];
    int hl = h[L], hr = h[R];

    h[u] = max(hl, hr) + 1;

    full_tree[u] = full_tree[L] && full_tree[R] && (hl == hr);

    complete_tree[u] =
        (complete_tree[L] && full_tree[R] && hl == hr + 1) ||
        (full_tree[L] && complete_tree[R] && hl == hr);

    if (complete_tree[u]) ans++;
}

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

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

    h[0] = 0;
    full_tree[0] = true;
    complete_tree[0] = true;

    dfs(1);
    cout << ans << '\n';
    return 0;
}
复杂度:每个结点只合并一次,时间 O(n),空间 O(n)
常见错误:把满二叉树和完全二叉树混为一谈;忘记初始化 0 号空树;只判断高度差;递归深度在极端链上可能很深,比赛环境若栈较小可改成显式栈后序。