





这是一道非常典型的树形 DFS + 树形 DP(后序递归)题。
有的同学看到都会觉得:
"这不是二叉树吗?是不是要 BFS?"
其实不是。
真正的突破口只有一句话:
一个节点是不是满二叉树,取决于它的左右子树。
看到这句话,应该可以想到:
后序 DFS!
今天我还是按照我的风格,用故事把它讲透,让同学们以后遇到"树形 DP"知道该怎么思考。
🌳 第一幕:满二叉树王国
1、很久很久以前。
(1)森林里住着很多小树精灵。
每个精灵都有两个孩子的位置:
左孩子
右孩子
(2)但是:
有的孩子存在。
有的孩子不存在。
例如:
A
/ \
B C
左孩子 右孩子
(3)现在国王发布任务:
统计整片森林里,有多少棵"满二叉树"。
注意:
不是只有整棵树。
而是:
每一个节点,都算是对应一棵子树。
🌳 第二幕:什么叫满二叉树?
1、有的同学容易和:
完全二叉树
平衡二叉树
混淆。
所以先讲清楚。
2、第一种
○
(1)只有一个点。
是不是满二叉树?
(2)答案:
是!
(3)因为:
叶子都一样高。
没有违反规则。
3、第二种
○
/ \
○ ○
是不是满二叉树?
也是。
4、第三种
○
/ \
○ ○
/ \ / \
○ ○ ○ ○
是不是满二叉树?
也是。
5、那什么时候不是?
(1)例如:
○
/
○
不是。
(2)为什么?
因为:
根只有一个孩子。
违反:
每个非叶子,必须要有两个孩子。
(3)还有:
○
/ \
○ ○
/
○
也不是。
(4)为什么?
因为:
所有叶子深度不同。
6、所以:
满二叉树必须同时满足:
① 每个非叶子都有两个孩子
② 所有叶子深度相同
🌳 第三幕:暴力可以吗?
1、这道题节点数为:
100000
对于每个节点:
重新遍历一次子树。
复杂度:
O(n²)
一定超时。
怎么办?
2、有没有办法:
每个节点只访问一次?
答案:
DFS!
🌳 第四幕:老师的小侦探
1、我们来到某个节点:
A
/ \
B C
2、汉克老师问:
A是不是满二叉树?
(1)能直接知道吗?
不知道。
(2)必须先问:
B
是不是?
(3)再问:
C
是不是?
(4)所以:
必须:
先孩子
后爸爸
3、这就是:
后序遍历
顺序:
左
↓
右
↓
自己
🌳 第五幕:每个节点要记录什么?
1、有的同学第一次做树DP,
总想记录很多东西。
其实这里只需要两个信息。
2、第一个
高度
记:
h[u]
表示:
以u为根,高度是多少。
(1)例如:
○
高度:
1
(2)例如:
○
/ \
○ ○
高度:
2
3、怎么算高度?
(1)孩子高度已经知道了。
所以:
h[u]
=
max(
左孩子高度
右孩子高度
)+1
(2)这是不是很熟?
因为:
DFS回来以后,
孩子信息已经有了。
🌳 第六幕:第二个信息
1、记录:
chk[u]
表示:
以u为根是不是满二叉树。
只有:
true
false
2、那么:
什么时候true?
汉克老师继续问:
根:
A
成为满二叉树,
必须满足什么?
3、第一:
左边必须已经是满二叉树。
chk[left]
==true
4、第二:
右边也是。
chk[right]
==true
5、第三:
左右高度一样。
(1)例如:
○
/ \
○ ○
左高:
2
右高:
2
可以。
(2)如果:
○
/ \
○ ○
/
○
左:
3
右:
2
叶子高度不同。
不是满二叉树。
6、于是:
最后得到判断公式:
chk[u]
=
chk[left]
&&
chk[right]
&&
h[left]==h[right]
是不是特别全面?
这是问题的核心判断。
🌳 第七幕:为什么叶子天然就是满二叉树?
1、来看:
○
(1)左右都是:
null
(2)DFS到:
null
直接返回。
(3)于是:
左高度
0
右高度:
0
相等。
所以:
chk=true
2、因此:
所有叶子,
天然都是满二叉树。
答案一定至少有:
叶子数量
棵。
🌳 第八幕:整个DFS流程
1、例如:
1
/ \
2 3
/ \
4 5
(1)我们从根开始:
dfs(1)
(2)先去左。
dfs(2)
(3)继续:
dfs(4)
(4)4没有孩子。
回来。
(5)得到:
h[4]=1
chk[4]=true
(6)继续:
dfs(5)
回来:
h[5]=1
chk[5]=true
(7)现在:
节点2终于可以计算:
左高:
1
右高:
1
一样。
于是:
h[2]=2
chk[2]=true
(8)继续:
节点3。
是叶子。
h=1
chk=true
(9)最后:
节点1。
左:
2
右:
1
不同。
所以:
chk[1]=false
(10)于是有:
4
5
3
2
共:
4
棵。
🌳 第九幕:为什么叫树形DP?
1、有的同学问:
汉克老师。
这里只有DFS呀?
为什么你说这是树形DP呢?
2、因为动态规划本质:
就是:
大问题依赖小问题。
(1)这里:
节点1
依赖:
节点2
节点3
(2)节点2又依赖:
节点4
节点5
是不是:
和DP是一模一样的?
3、而且状态长在树上。
所以叫:
树形DP。
🌳 第十幕:参考程序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int leftChild[N], rightChild[N];
// h[i]:以 i 为根的子树高度
int h[N];
// full[i]:以 i 为根是否为满二叉树
bool full[N];
// 答案
int ans = 0;
void dfs(int u)
{
if (u == 0) return; // 空节点
// 后序遍历:先左右子树
dfs(leftChild[u]);
dfs(rightChild[u]);
// 计算高度
h[u] = max(h[leftChild[u]], h[rightChild[u]]) + 1;
// 判断是否为满二叉树
full[u] = full[leftChild[u]]
&& full[rightChild[u]]
&& (h[leftChild[u]] == h[rightChild[u]]);
if (full[u])
ans++;
}
int main()
{
cin >> n;
// 空树看作满二叉树,便于递推
full[0] = true;
for (int i = 1; i <= n; i++)
cin >> leftChild[i] >> rightChild[i];
dfs(1);
cout << ans << endl;
return 0;
}
🌟 思维导图
满二叉树统计
│
▼
每个节点统计一次
│
▼
后序DFS
│
┌─────────────┴─────────────┐
▼ ▼
记录高度 h[u] 是否满 chk[u]
│ │
▼ ▼
h=max(左,右)+1 左满 && 右满 && 左高==右高
│ │
└─────────────┬─────────────┘
▼
如果成立,答案+1
🌈 举一反三
如果一个节点的答案,需要依赖左右子树的答案,那就可以想到"后序 DFS + 树形 DP"。
以后遇到下面这些经典问题时,都可以尝试下同样的方法:
判断平衡二叉树
求二叉树高度
求二叉树直径
求最大独立集
求树上最长路径
判断各种子树性质
它们有一个共同特点:
先计算孩子,再计算父亲。