题解:洛谷 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
【核心思想】
-
问题分析:给定一棵有根二叉树,需要统计所有子树中完全二叉树的数量。完全二叉树的定义是:除最后一层外,每一层都是满的;最后一层的结点都集中在左侧。这是一个树形动态规划 + 深度优先搜索(DFS)问题,核心在于自底向上判断每个子树是否满足完全二叉树的结构特征。
-
算法选择:
- 深度优先搜索(DFS):从根结点开始递归遍历,先处理子结点再处理父结点,实现自底向上的信息传递
- 多状态维护:每个结点维护 \(cnt\)(子树结点数)、\(dep\)(子树高度)、\(flag\)(是否为完全二叉树)
-
关键步骤:
- 叶子结点判定(\(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\)
- 叶子结点判定(\(l_u = 0\) 且 \(r_u = 0\)):
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个结点仅访问一次,每次处理为 \(O(1)\)
- 空间复杂度:\(O(n)\),递归栈深度最坏为 \(O(n)\),结构体数组存储为 \(O(n)\)
-
树形 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
浙公网安备 33010602011771号