题解:洛谷 P15801 完全二叉树

【题目来源】

洛谷:P15801 [GESP202603 六级] 完全二叉树 - 洛谷

【题目描述】

给定一棵包含 \(n\) 个结点的有根二叉树,结点依次以 \(1,2,\dots,n\) 编号,根结点编号为 \(1\)

对于结点 \(i\),其左儿子的编号记为 \(l_i\),右儿子编号记为 \(r_i\)。特别地,如果左儿子不存在则 \(l_i=0\),如果右儿子不存在则 \(r_i=0\)

树中每个结点都对应一棵以其为根的子树。请你求出给定有根树的所有 \(n\) 棵子树中,有多少棵子树是完全二叉树。

【输入】

第一行,一个正整数 \(n\),表示有根二叉树结点数量。

接下来 \(n\) 行,每行两个正整数 \(l_i,r_i\),表示结点 \(i\) 的左儿子编号和右儿子编号。

【输出】

输出一行,一个整数,表示所有子树中完全二叉树的数量。

【输入样例】

4
2 3
4 0
0 0
0 0

【输出样例】

4

【核心思想】

  1. 问题分析:给定一棵有根二叉树,需要统计所有子树中完全二叉树的数量。完全二叉树的定义是:除最后一层外,每一层都是满的;最后一层的结点都集中在左侧。这是一个树形动态规划 + 深度优先搜索(DFS)问题,核心在于自底向上判断每个子树是否满足完全二叉树的结构特征。

  2. 算法选择

    • 深度优先搜索(DFS):从根结点开始递归遍历,先处理子结点再处理父结点,实现自底向上的信息传递
    • 多状态维护:每个结点维护 \(cnt\)(子树结点数)、\(dep\)(子树高度)、\(flag\)(是否为完全二叉树)
  3. 关键步骤

    • 叶子结点判定\(l_u = 0\)\(r_u = 0\)):
      • \(cnt \leftarrow 1\)\(dep \leftarrow 1\)\(flag \leftarrow true\)
      • \(ans \leftarrow ans + 1\)
    • 递归处理子树:若左儿子存在则 \(dfs(l_u)\),若右儿子存在则 \(dfs(r_u)\)
    • 统计子树信息\(cnt \leftarrow 1 + cnt_{left} + cnt_{right}\)\(dep \leftarrow \max(dep_{left}, dep_{right}) + 1\)
    • 完全二叉树判定(以 \(u\) 为根的子树,要求左右子树均为完全二叉树):
      • 情况1\(dep_{left} = dep_{right}\)\(cnt_{left} = 2^{dep_{left}} - 1\)(左子树是满二叉树),则当前子树为完全二叉树
      • 情况2\(dep_{left} = dep_{right} + 1\)\(cnt_{right} = 2^{dep_{right}} - 1\)(右子树是满二叉树),则当前子树为完全二叉树
      • 满足任一情况:\(flag \leftarrow true\)\(ans \leftarrow ans + 1\)
    • 输出结果\(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个结点仅访问一次,每次处理为 \(O(1)\)
    • 空间复杂度:\(O(n)\),递归栈深度最坏为 \(O(n)\),结构体数组存储为 \(O(n)\)
  5. 树形 DP 与完全二叉树判定的核心思想

    • 自底向上信息聚合:通过 DFS 后序遍历,先获取子结点的 \(cnt\)\(dep\)\(flag\) 信息,再判断父结点是否满足完全二叉树条件
    • 完全二叉树的递归结构:一棵子树是完全二叉树,当且仅当左右子树都是完全二叉树,且满足以下两种结构之一:
      • 左右子树高度相等,且左子树是满二叉树(结点数恰好为 \(2^{dep} - 1\)),此时最后一层结点集中在左侧
      • 左子树高度比右子树大 \(1\),且右子树是满二叉树,此时最后一层只有左侧有结点
    • 满二叉树的判定:利用结点数公式 \(cnt = 2^{dep} - 1\) 判断子树是否为满二叉树,这是完全二叉树判定中的关键子条件
    • 计数策略:在判定每个结点为完全二叉树时立即累加 \(ans\),DFS 遍历所有子树,自然统计所有满足条件的子树
    • 适用于树形结构的递归性质判定、需要维护多维度状态信息的自底向上动态规划类问题

【解题思路】

【算法标签】

普及 #DFS-树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;

int n, ans;
struct tree
{
    int l, r, cnt, dep, flag;
} tr[N];

void dfs(int u)
{
    int lchild = tr[u].l, rchild = tr[u].r;
    // 如果是叶子节点
    if (lchild == 0 && rchild == 0)
    {
        tr[u].cnt = 1;  // 节点计数为1
        tr[u].dep = 1;  // 深度为1
        tr[u].flag = 1; // 标记为完全二叉树
        ans++;          // 计数加1
        return;
    }

    tr[u].cnt = 1;  // 初始化计数,包含当前节点自身

    // 遍历左子树
    if (tr[u].l)
    {
        dfs(tr[u].l);
        tr[u].cnt += tr[lchild].cnt;  // 累加左子树的节点数
        tr[u].dep = tr[lchild].dep + 1;  // 计算深度
    }

    // 遍历右子树
    if (tr[u].r)
    {
        dfs(tr[u].r);
        tr[u].cnt += tr[rchild].cnt;  // 累加右子树的节点数
        tr[u].dep = max(tr[u].dep, tr[rchild].dep + 1);  // 更新最大深度
    }

    // 处理只有一个子节点的情况
    if (tr[lchild].dep == 1 && tr[rchild].dep == 0)
    {
        tr[rchild].flag = 1;
    }

    // 如果左右子树都是完全二叉树
    if (tr[lchild].flag == 1 && tr[rchild].flag == 1)
    {
        // 情况1: 左右子树深度相等
        if (tr[lchild].dep == tr[rchild].dep && tr[lchild].cnt == (1 << tr[lchild].dep) - 1)
        {
            tr[u].flag = 1;  // 当前节点是完全二叉树的根
            ans++;
        }
        // 情况2: 左子树深度比右子树大1
        else if (tr[lchild].dep == tr[rchild].dep + 1 && tr[rchild].cnt == (1 << tr[rchild].dep) - 1)
        {
            tr[u].flag = 1;  // 当前节点是完全二叉树的根
            ans++;
        }
    }
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        int l, r;
        cin >> l >> r;
        tr[i].l = l, tr[i].r = r;
    }
    dfs(1);
    cout << ans << endl;
    return 0;
}

【运行结果】

4
2 3
4 0
0 0
0 0
4
posted @ 2026-07-09 23:26  团爸讲算法  阅读(11)  评论(0)    收藏  举报