记录135

#include<bits/stdc++.h> // 引入输入输出流标准库
using namespace std; // 使用标准命名空间std
const int N=1e5+5; // 定义常量N,表示树结点的最大范围

int n; // 定义全局变量n,表示树的结点数量
int l[N],r[N]; // 定义数组l和r,分别存储每个结点的左儿子和右儿子编号
bool is_perfect[N]; // 标记以i为根的子树是否为满二叉树
bool is_complete[N]; // 标记以i为根的子树是否为完全二叉树
int depth[N]; // 记录以i为根的子树的最大深度
int ans=0; // 定义变量ans,用来记录完全二叉树的总数量

// 深度优先搜索(DFS)函数,用于后序遍历树并进行状态转移
void dfs(int u){
    if(u==0){ // 如果当前结点是空结点(编号为0)
        is_perfect[u]=true; // 空节点视为满二叉树(作为递归的基准条件)
        is_complete[u]=false; // 空节点不计入完全二叉树的数量
        depth[u]=0; // 空节点的深度为0
        return; // 直接返回
    }
    
    dfs(l[u]); // 递归处理左儿子
    dfs(r[u]); // 递归处理右儿子
    
    // 计算当前子树的深度(取左右子树最大深度 + 1)
    depth[u]=max(depth[l[u]],depth[r[u]])+1; 
    
    // 情况1:判断当前子树是否为满二叉树
    // 条件:左右子树都是满二叉树,且左右子树深度相同
    if(is_perfect[l[u]]&&is_perfect[r[u]]&&depth[l[u]]==depth[r[u]]){
        is_perfect[u]=true;
        is_complete[u]=true; // 满二叉树一定是完全二叉树
    }
    // 情况2:判断当前子树是否为完全二叉树(左满 + 右完全 + 深度相同)
    else if(is_perfect[l[u]]&&is_complete[r[u]]&&depth[l[u]]==depth[r[u]]){
        is_complete[u]=true;
    }
    // 情况3:判断当前子树是否为完全二叉树(左完全 + 右满 + 左深比右深多1)
    else if(is_complete[l[u]]&&is_perfect[r[u]]&&depth[l[u]]==depth[r[u]]+1){
        is_complete[u]=true;
    }
    
    // 如果以当前结点u为根的子树是完全二叉树,答案加1
    if(is_complete[u]){ 
        ans++; 
    }
}

int main(){ // 主函数入口
    ios::sync_with_stdio(false); // 关闭标准流同步,提升输入输出效率
    cin.tie(0); // 解除cin与cout的绑定,加快读取速度
    
    cin>>n; // 读入树的结点数量n
    for(int i=1;i<=n;i++){ // 循环读入每个结点的左右儿子信息
        cin>>l[i]>>r[i]; // 读入结点i的左儿子和右儿子编号
    }
    
    dfs(1); // 从根结点1开始进行深度优先搜索
    
    cout<<ans<<"\n"; // 输出所有子树中完全二叉树的数量
    return 0; // 程序正常结束
}

题目传送门https://www.luogu.com.cn/problem/P15801


前言

我是一名专注信奥赛(CSP-J/S、NOIP)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

核心解题思路

这道题是一道结合了树形结构性质动态规划(DP)思想的经典问题。我们需要统计整棵树中所有子树里,有多少棵是“完全二叉树”。

  1. 核心概念辨析

    • 满二叉树(Perfect Binary Tree):一棵深度为 kk 的二叉树,其所有叶子结点都在第 kk 层,且所有非叶子结点都有两个子结点。满二叉树的结点数一定是 2k−1 。
    • 完全二叉树(Complete Binary Tree):除了最后一层外,其它各层的结点数都达到最大值,且最后一层的结点都连续集中在最左边。
  2. 状态定义与性质利用
    完全二叉树的定义天然具有递归性质。对于以结点 u 为根的子树,如果它是完全二叉树,必然满足以下三种情况之一:

    • 情况 A(是满二叉树):左子树和右子树都是满二叉树,且左右子树深度相同。
    • 情况 B(左满右完,深度相同):左子树是满二叉树,右子树是完全二叉树,且左右子树深度相同。
    • 情况 C(左完右满,左深右浅):左子树是完全二叉树,右子树是满二叉树,且左子树深度比右子树深度大 1。
  3. 算法设计
    我们采用后序遍历(DFS),自底向上地计算每个结点的子树信息。对于每个结点 uu ,我们需要维护三个状态:子树深度 depth[u]、是否为满二叉树 is_perfect[u]、是否为完全二叉树 is_complete[u]。根据上述三种情况进行状态转移,并在遍历过程中累加答案。


代码分块详细解释

1. 头文件、常量与全局变量定义

#include<bits/stdc++.h> 
using namespace std; 
const int N=1e5+5; // 定义常量 N,表示树结点的最大范围

int n; // 定义全局变量 n,表示树的结点数量
int l[N], r[N]; // 定义数组 l 和 r,分别存储每个结点的左儿子和右儿子编号
bool is_perfect[N]; // 标记以 i 为根的子树是否为满二叉树
bool is_complete[N]; // 标记以 i 为根的子树是否为完全二叉树
int depth[N]; // 记录以 i 为根的子树的最大深度
int ans=0; // 定义变量 ans,用来记录完全二叉树的总数量
  • 详细分析:这部分定义了树形 DP 所需的核心状态数组。l[N] 和 r[N] 使用数组模拟了二叉树的指针结构,方便通过下标直接访问左右孩子。is_perfect 和 is_complete 两个布尔数组是解题的关键,它们将复杂的树结构判断转化为了简单的状态标记。

2. DFS 递归基(处理空节点)

void dfs(int u){
    if(u==0){ // 如果当前结点是空结点(编号为0)
        is_perfect[u]=true; // 空节点视为满二叉树(作为递归的基准条件)
        is_complete[u]=false; // 空节点不计入完全二叉树的数量
        depth[u]=0; // 空节点的深度为0
        return; // 直接返回
    }
  • 详细分析:这是递归的边界条件。在树形 DP 中,将空节点(u=0)视为深度为 0 的满二叉树是非常精妙的处理。因为满二叉树的定义要求左右子树深度相同,将空节点视为满二叉树,可以使得叶子结点的父节点能够顺利满足“左右子树都是满二叉树且深度相同(都为0)”的条件,从而正确判定叶子结点本身也是一棵满二叉树。

3. 后序遍历与深度计算

    dfs(l[u]); // 递归处理左儿子
    dfs(r[u]); // 递归处理右儿子
    
    // 计算当前子树的深度(取左右子树最大深度 + 1)
    depth[u] = max(depth[l[u]], depth[r[u]]) + 1; 
  • 详细分析:采用后序遍历(先处理子节点,再处理父节点)。在判断当前结点 uu 的性质之前,必须先获取其左右子树的深度和状态。当前子树的深度等于左右子树中较深的那个深度加 1。

4. 核心状态转移(三种情况判定)

    // 情况1:判断当前子树是否为满二叉树
    // 条件:左右子树都是满二叉树,且左右子树深度相同
    if(is_perfect[l[u]] && is_perfect[r[u]] && depth[l[u]] == depth[r[u]]){
        is_perfect[u] = true;
        is_complete[u] = true; // 满二叉树一定是完全二叉树
    }
    // 情况2:判断当前子树是否为完全二叉树(左满 + 右完全 + 深度相同)
    else if(is_perfect[l[u]] && is_complete[r[u]] && depth[l[u]] == depth[r[u]]){
        is_complete[u] = true;
    }
    // 情况3:判断当前子树是否为完全二叉树(左完全 + 右满 + 左深比右深多1)
    else if(is_complete[l[u]] && is_perfect[r[u]] && depth[l[u]] == depth[r[u]] + 1){
        is_complete[u] = true;
    }
    
    // 如果以当前结点 u 为根的子树是完全二叉树,答案加1
    if(is_complete[u]){ 
        ans++; 
    }
}
  • 详细分析:这是整个算法的灵魂。
    • 情况 1:如果左右子树都是满二叉树且深度相同,拼接起来依然是一棵更大的满二叉树。满二叉树是完全二叉树的特例,所以 is_complete 也置为 true
    • 情况 2:左子树是满的,右子树是完全的,且两者深度相同。这意味着左子树已经填满了,右子树作为完全二叉树,其结点也是靠左排列的。两者拼接后,整体依然满足完全二叉树的定义。
    • 情况 3:左子树是完全的,右子树是满的,且左子树比右子树深 1。这意味着左子树的最后一层(也是整棵树的最后一层)可能有结点,而右子树已经满了(没有最后一层)。这恰好符合完全二叉树“最后一层结点连续集中在最左边”的要求。
    • 打擂累加:只要当前结点被判定为完全二叉树,全局计数器 ans 就加 1。

5. 主函数:建图与启动

int main(){ 
    ios::sync_with_stdio(false); 
    cin.tie(0); 
    
    cin >> n; 
    for(int i = 1; i <= n; i++){ 
        cin >> l[i] >> r[i]; // 读入结点 i 的左儿子和右儿子编号
    }
    
    dfs(1); // 从根结点 1 开始进行深度优先搜索
    
    cout << ans << "\n"; 
    return 0; 
}
  • 详细分析:主函数负责读取每个结点的左右孩子信息,构建出整棵二叉树。随后调用 dfs(1) 从根节点启动递归。由于题目保证结点编号从 1 到 n 且根为 1,这种数组存储方式非常高效。最终输出累加的答案即可。

核心逻辑总结表

代码模块 核心变量/操作 精炼作用 解决的痛点
递归基 if(u==0) is_perfect=true 将空节点定义为满二叉树 简化了叶子结点和单分支结点的状态转移逻辑
状态数组 is_perfectis_complete 记录子树的两种二叉树性质 将复杂的树形结构判断转化为 O(1) 的布尔状态查询
满二叉树判定 is_perfect[l] && is_perfect[r] && 深度相同 核心情况 1 的状态转移 识别出最基础的满二叉树结构
完全二叉树判定 情况 2 和 情况 3 的 else if 核心情况 2、3 的状态转移 完美契合完全二叉树“最后一层靠左连续排列”的数学定义
后序遍历 dfs(l[u]); dfs(r[u]); 自底向上计算子树信息 确保在判断父节点时,左右子节点的状态已经计算完毕
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐