🌲 完全二叉树子树计数 · 精讲

树 DP 入门 | 高度 h · 满二叉树 full · 完全二叉树 complete

n ≤ 10⁵ · 一次后序遍历搞定

0先翻译题目

题干给了我们一棵「有根二叉树」:1 号是根,每个点 i 有左儿子 li 和右儿子 ri(0 表示没有)。

题目到底要什么:每个点 i 都「管着」一棵以它为根的子树(它 + 所有后代)。
把这 n 棵子树挨个检查,数一数有几棵是完全二叉树

所以这题有两步:① 搞清楚什么叫「完全二叉树」;② 设计一个能同时判断 n 棵子树的高效方法。n 最大 10⁵,不能一棵一棵去扫(那就 O(n²) 了),必须一次遍历把 n 个答案全算出来——这就是「树 DP(动态规划)」的思想。

1什么是完全二叉树?

完全二叉树 = 一层一层从左往右填,除了最后一层,其它层必须填满;最后一层也必须从左到右连续,不能有「空位」。

注意区分三个容易混的词:

满二叉树 → 完全 ✓

完全但不满 ✓

最后一层有缺口 ✗

⚠️ 最容易错:完全二叉树不要求最后一层填满,只要求「从左往右连续」。最后一层只有 1 个节点(最左边)也可以是完全二叉树!

🔍 判定小测试:这些子树是完全二叉树吗?(点选项看对错)

根 + 两个叶子(3 个点)

✔️ 是!满二叉树 → 一定是完全二叉树。

只有左孩子的两个点(2 个点)

✔️ 是!第 1 层满,第 2 层只有最左边的节点 → 从左往右连续 ✓

三个点全往左挂(一条斜线)

✔️ 不是!第 2 层本该有 2 个节点,但只有左边的 1 个 → 第 2 层没满,而它又不是最后一层(第 3 层还有节点)→ ✗

只有右孩子(2 个点)

✔️ 不是!第 2 层最左边是空的,第一个节点就跑到右边去了 → 不连续 ✗

左孩子带着一个左孙子,右孩子是叶子(4 个点)

✔️ 是!第 3 层只有一个节点且在最左边 → 连续 ✓。这就是「完全但不满」的经典例子!

左右孩子各带一个左孙子(5 个点)

✔️ 不是!第 3 层从左到右是:有、、有 → 中间有缺口 ✗
🎯 一句话记法:把节点按「堆的编号」排(左=2i,右=2i+1),完全二叉树 = 编号恰好是 1、2、3……k 没有跳号

2三个判定量:h、满、完全

要判断「子树是不是完全二叉树」,只需要给每个节点存三个信息:

信息含义怎么算
高度 h子树有多高(叶子 = 1)max(左高, 右高) + 1
满 full是不是满二叉树(每层都填满)左满 && 右满 && 左右等高
完全 ok是不是完全二叉树 ← 我们要的答案见第 3 节的规则

关键:这三个量都只和「孩子」有关——先算孩子,就能算爸爸。这就是「自底向上」的树 DP。

🖱️ 判案练习:点击树上的节点,看它「管着」的子树是不是完全二叉树

这棵演示树有 10 个点。点一下任意节点,它的整棵子树会亮起来,同时给出判决和理由!

是完全二叉树 不是完全二叉树 选中的节点
👆 点一个节点,我来判案!

3判定规则:四种情况

节点 u 的子树是否完全,看它的孩子情况(hl = 左子树高度,hr = 右子树高度):

情况 ① 叶子(没有孩子)是完全二叉树 ✓

高度 1,满二叉树,当然也是完全二叉树。

情况 ② 只有左孩子 → 左孩子必须是叶子(高度 1)才是完全 ✓

只有左孩子时,第 2 层右边是空的。如果第 2 层不是最后一层(左孩子下面还有节点),第 2 层就没满 → ✗。所以左孩子必须是叶子。

情况 ③ 只有右孩子永远不是完全二叉树 ✗

第 2 层最左边就空了,第一个节点在右边 → 不连续,直接 ✗。

情况 ④ 两个孩子都有 → 分三种:

· hl == hr(等高):左子树必须是满二叉树 → 才完全 ✓
· hl == hr + 1(左比右高 1 层):右子树必须是满二叉树 → 才完全 ✓
· 其他情况(左边比右边矮等)→

🤔 为什么「等高时」只要左边满、右边不用满?
等高时,最后一层横跨左右两边。节点是从左往右排的:先排完左子树最后一层,才轮到右边。
如果左边最后一层有空位,空位后面却还有右子树的节点 → 不连续 → ✗。所以左边必须填满;而右边只要「自己的部分从左往右连续」就行(右子树自己完全即可),允许右边最后一层不满。
🤔 为什么「左高 1 层」时反而要右边满?
左比右高 1 层时,最后一层只存在左子树里,倒数第二层横跨两边。
倒数第二层必须填满:左边那半天然满(左子树完全且比右高),右边那半是右子树的最后一层——它必须是满的才行。所以这时要检查右边是否满

4动画:后序遍历 DP 全过程

程序用后序遍历(先左、后右、再自己)把树走一遍:孩子算完才轮到爸爸,正好符合「自底向上」。每处理一个节点就填好它的 h / 满 / 完全,如果是完全二叉树就计数 +1。绿色 = 完全,红色 = 不完全。

还没访问 在栈中(孩子还没处理完) 处理完:完全 ✓ 处理完:不完全 ✗ 当前处理
栈:
DP 表:(还没处理任何节点)

后序遍历的顺序是 4, 5, 2, 8, 6, 9, 10, 7, 3, 1——每个节点算的时候,它的孩子一定已经算完了。数一数绿色:2、4、5、6、7、8、9、10 共 8 棵

5C++ 代码

n 最大 10⁵,树可能长成一条「斜链」——递归会爆栈,所以主代码用迭代后序遍历(显式栈)。三个数组 h / full / ok 就是上一节动画里填的表。

#include <iostream>
#include <stack>
using namespace std;

const int N = 100005;
int lc[N], rc[N];      // 左儿子、右儿子(0 = 没有)
int h[N];              // 子树高度(叶子 = 1)
bool full[N];          // 是不是满二叉树
bool ok[N];            // 是不是完全二叉树 ← 答案

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

    // 迭代后序遍历:先左、后右、再自己(用显式栈,不递归)
    int order[N];                       // 存后序顺序
    int cntOrder = 0;
    stack<pair<int,int>> st;           // <节点, 0=先去孩子 1=孩子处理完>
    st.push(make_pair(1, 0));
    while (!st.empty()) {
        int u = st.top().first, s = st.top().second;
        st.pop();
        if (s == 0) {
            st.push(make_pair(u, 1));
            if (rc[u]) st.push(make_pair(rc[u], 0));   // 先压右
            if (lc[u]) st.push(make_pair(lc[u], 0));   // 再压左 → 左先出
        } else {
            order[cntOrder++] = u;      // 孩子都处理完了
        }
    }

    // 自底向上 DP:按后序顺序算
    int ans = 0;
    for (int k = 0; k < cntOrder; k++) {
        int u = order[k];
        int l = lc[u], r = rc[u];
        if (l == 0 && r == 0) {                    // 情况① 叶子
            h[u] = 1; full[u] = true; ok[u] = true;
        }
        else if (l && r) {                          // 情况④ 两个孩子
            int hl = h[l], hr = h[r];
            h[u] = (hl > hr ? hl : hr) + 1;
            full[u] = full[l] && full[r] && (hl == hr);
            ok[u] = false;
            if (ok[l] && ok[r]) {
                if (hl == hr) ok[u] = full[l];       // 等高 → 左边要满
                else if (hl == hr + 1) ok[u] = full[r]; // 左高1 → 右边要满
            }
        }
        else if (l) {                                // 情况② 只有左孩子
            h[u] = h[l] + 1; full[u] = false;
            ok[u] = (h[l] == 1);                     // 左孩子必须是叶子
        }
        else {                                       // 情况③ 只有右孩子
            h[u] = h[r] + 1; full[u] = false; ok[u] = false;
        }
        if (ok[u]) ans++;
    }

    cout << ans << endl;
    return 0;
}

时间复杂度 O(n):每个节点只处理一次。空间 O(n)

📖 递归版(代码短,但斜链大数可能爆栈,慎用)
void dfs(int u) {
    if (u == 0) return;
    dfs(lc[u]); dfs(rc[u]);          // 先孩子
    int l = lc[u], r = rc[u];
    if (l == 0 && r == 0) { h[u] = 1; full[u] = true; ok[u] = true; }
    else if (l && r) {
        h[u] = max(h[l], h[r]) + 1;
        full[u] = full[l] && full[r] && h[l] == h[r];
        ok[u] = ok[l] && ok[r] &&
                ((h[l] == h[r] ? full[l] : (h[l] == h[r] + 1 ? full[r] : false)));
    }
    else if (l) { h[u] = h[l] + 1; full[u] = false; ok[u] = (h[l] == 1); }
    else        { h[u] = h[r] + 1; full[u] = false; ok[u] = false; }
}

6小测验

1️⃣ 完全二叉树的最后一层,节点必须是?

✔️ 连续不缺口!「填满」是满二叉树的要求,完全二叉树只要连续。

2️⃣ 一个节点只有左孩子,什么情况下它的子树才是完全二叉树?

✔️ 左孩子必须是叶子(高度 1)!否则第 2 层没满、而它又不是最后一层 → ✗

3️⃣ 两个孩子等高(hl == hr)时,要成为完全二叉树,必须满足?

✔️ 只需要左边满!最后一层先排左边,左边有空位就会挡住右边的节点 → 不连续。右边自己完全就行。

4️⃣ 左子树比右子树高 1 层(hl == hr+1)时,必须满足?

✔️ 这时最后一层在左边,倒数第二层横跨两边,右子树的最后一层正好是它 → 右边必须填满。

5️⃣ 满二叉树一定是完全二叉树,反过来也对吗?

✔️ 满 ⇒ 完全 ✓,但完全 ⇏ 满(比如「根 + 一个左叶子」是完全但不满)。

6️⃣ n = 10⁵ 时,这题应该怎么做?

✔️ 一次后序遍历!每个节点只算一次,孩子先算完爸爸就能直接算。O(n²) 在 10⁵ 下会超时。

7总结 & 考点

知识点一句话
完全二叉树除最后一层全满 + 最后一层从左到右连续(可不满)
满 vs 完全满 ⇒ 完全;完全 ⇏ 满
后序遍历先左、后右、再自己 —— 孩子先算完,爸爸才能算
树 DP 三件套高度 h + 满 full + 完全 ok,自底向上填
两个孩子的微妙规则等高 → 左边要满;左高 1 层 → 右边要满
防爆栈n 大时用迭代栈代替递归
🎯 这是「树 DP」的入门题:答案不靠搜,靠「自底向上递推」。学会了它,七级的「树上背包」「树上最大独立集」都是同一套路——先想清楚每个节点要维护哪几个量,再想它们怎么由孩子推出