记录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)思想的经典问题。我们需要统计整棵树中所有子树里,有多少棵是“完全二叉树”。
-
核心概念辨析:
- 满二叉树(Perfect Binary Tree):一棵深度为 kk 的二叉树,其所有叶子结点都在第 kk 层,且所有非叶子结点都有两个子结点。满二叉树的结点数一定是 2k−1 。
- 完全二叉树(Complete Binary Tree):除了最后一层外,其它各层的结点数都达到最大值,且最后一层的结点都连续集中在最左边。
-
状态定义与性质利用:
完全二叉树的定义天然具有递归性质。对于以结点 u 为根的子树,如果它是完全二叉树,必然满足以下三种情况之一:- 情况 A(是满二叉树):左子树和右子树都是满二叉树,且左右子树深度相同。
- 情况 B(左满右完,深度相同):左子树是满二叉树,右子树是完全二叉树,且左右子树深度相同。
- 情况 C(左完右满,左深右浅):左子树是完全二叉树,右子树是满二叉树,且左子树深度比右子树深度大 1。
-
算法设计:
我们采用后序遍历(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。
- 情况 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_perfect, is_complete | 记录子树的两种二叉树性质 | 将复杂的树形结构判断转化为 O(1) 的布尔状态查询 |
| 满二叉树判定 | is_perfect[l] && is_perfect[r] && 深度相同 | 核心情况 1 的状态转移 | 识别出最基础的满二叉树结构 |
| 完全二叉树判定 | 情况 2 和 情况 3 的 else if | 核心情况 2、3 的状态转移 | 完美契合完全二叉树“最后一层靠左连续排列”的数学定义 |
| 后序遍历 | dfs(l[u]); dfs(r[u]); | 自底向上计算子树信息 | 确保在判断父节点时,左右子节点的状态已经计算完毕 |
173

被折叠的 条评论
为什么被折叠?



