leetcode-master/problems/0968.监控二叉树.md

360 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ"><img src="https://img.shields.io/badge/知识星球-代码随想录-blue" alt=""></a>
<a href="https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw"><img src="https://img.shields.io/badge/刷题-微信群-green" alt=""></a>
<a href="https://img-blog.csdnimg.cn/20201210231711160.png"><img src="https://img.shields.io/badge/公众号-代码随想录-brightgreen" alt=""></a>
<a href="https://space.bilibili.com/525438321"><img src="https://img.shields.io/badge/B站-代码随想录-orange" alt=""></a>
</p>
<p align="center"><strong>欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
## 968.监控二叉树
题目地址 : https://leetcode-cn.com/problems/binary-tree-cameras/
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1
![](https://img-blog.csdnimg.cn/20201229175736596.png)
输入:[0,0,null,0,0]
输出1
解释:如图所示,一台摄像头足以监控所有节点。
示例 2
![](https://img-blog.csdnimg.cn/2020122917584449.png)
输入:[0,0,null,0,null,0,null,null,0]
输出2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
* 给定树的节点数的范围是 [1, 1000]。
* 每个节点的值都是 0。
## 思路
这道题目首先要想,如何放置,才能让摄像头最小的呢?
从题目中示例,其实可以得到启发,**我们发现题目示例中的摄像头都没有放在叶子节点上!**
这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。
那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?
因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
**所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!**
局部最优推出全局最优,找不出反例,那么就按照贪心来!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
此时这道题目还有两个难点:
1. 二叉树的遍历
2. 如何隔两个节点放一个摄像头
### 确定遍历顺序
在二叉树中如何从低向上推导呢?
可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。
后序遍历代码如下:
```
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (终止条件) return ;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
逻辑处理 // 中
return ;
}
```
**注意在以上代码中我们取了左孩子的返回值右孩子的返回值即left 和 right 以后推导中间节点的状态**
### 如何隔两个节点放一个摄像头
此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移!
来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:
有如下三种:
* 该节点无覆盖
* 本节点有摄像头
* 本节点有覆盖
我们分别有三个数字来表示:
* 0该节点无覆盖
* 1本节点有摄像头
* 2本节点有覆盖
大家应该找不出第四个节点的状态了。
**一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。**
**因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?**
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。
那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。
**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了**
接下来就是递推关系。
那么递归的终止条件应该是遇到了空节点此时应该返回2有覆盖原因上面已经解释过了。
代码如下:
```
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
```
递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。
主要有如下四类情况:
* 情况1左右节点都有覆盖
左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。
如图:
![968.监控二叉树2](https://img-blog.csdnimg.cn/20201229203710729.png)
代码如下:
```
// 左右节点都有覆盖
if (left == 2 && right == 2) return 0;
```
* 情况2左右节点至少有一个无覆盖的情况
如果是以下情况,则中间节点(父节点)应该放摄像头:
left == 0 && right == 0 左右节点无覆盖
left == 1 && right == 0 左节点有摄像头,右节点无覆盖
left == 0 && right == 1 左节点有无覆盖,右节点摄像头
left == 0 && right == 2 左节点无覆盖,右节点覆盖
left == 2 && right == 0 左节点覆盖,右节点无覆盖
这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。
此时摄像头的数量要加一并且return 1代表中间节点放摄像头。
代码如下:
```
if (left == 0 || right == 0) {
result++;
return 1;
}
```
* 情况3左右节点至少有一个有摄像头
如果是以下情况,其实就是 左右孩子节点有一个有摄像头了那么其父节点就应该是2覆盖的状态
left == 1 && right == 2 左节点有摄像头,右节点有覆盖
left == 2 && right == 1 左节点有覆盖,右节点有摄像头
left == 1 && right == 1 左右节点都有摄像头
代码如下:
```
if (left == 1 || right == 1) return 2;
```
**从这个代码中可以看出如果left == 1, right == 0 怎么办其实这种条件在情况2中已经判断过了**,如图:
![968.监控二叉树1](https://img-blog.csdnimg.cn/2020122920362355.png)
这种情况也是大多数同学容易迷惑的情况。
4. 情况4头结点没有覆盖
以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图:
![968.监控二叉树3](https://img-blog.csdnimg.cn/20201229203742446.png)
所以递归结束之后还要判断根节点如果没有覆盖result++,代码如下:
```
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
```
以上四种情况我们分析完了,代码也差不多了,整体代码如下:
**以下我的代码注释很详细,为了把情况说清楚,特别把每种情况列出来。**
## C++代码
```C++
// 版本一
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
// 情况1
// 左右节点都有覆盖
if (left == 2 && right == 2) return 0;
// 情况2
// left == 0 && right == 0 左右节点无覆盖
// left == 1 && right == 0 左节点有摄像头,右节点无覆盖
// left == 0 && right == 1 左节点有无覆盖,右节点摄像头
// left == 0 && right == 2 左节点无覆盖,右节点覆盖
// left == 2 && right == 0 左节点覆盖,右节点无覆盖
if (left == 0 || right == 0) {
result++;
return 1;
}
// 情况3
// left == 1 && right == 2 左节点有摄像头,右节点有覆盖
// left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// left == 1 && right == 1 左右节点都有摄像头
// 其他情况前段代码均已覆盖
if (left == 1 || right == 1) return 2;
// 以上代码我没有使用else主要是为了把各个分支条件展现出来这样代码有助于读者理解
// 这个 return -1 逻辑不会走到这里。
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
// 情况4
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};
```
在以上代码的基础上,再进行精简,代码如下:
```C++
// 版本二
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
if (cur == NULL) return 2;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
if (left == 2 && right == 2) return 0;
else if (left == 0 || right == 0) {
result++;
return 1;
} else return 2;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};
```
大家可能会惊讶,居然可以这么简短,**其实就是在版本一的基础上使用else把一些情况直接覆盖掉了**。
在网上关于这道题解可以搜到很多这种神级别的代码,但都没讲不清楚,如果直接看代码的话,指定越看越晕,**所以建议大家对着版本一的代码一步一步来哈,版本二中看不中用!**。
## 总结
本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。
在二叉树上进行状态推导,其实难度就上了一个台阶了,需要对二叉树的操作非常娴熟。
这道题目是名副其实的hard大家感受感受哈哈。
## 其他语言版本
Java
```java
class Solution {
private int count = 0;
public int minCameraCover(TreeNode root) {
if (trval(root) == 0) count++;
return count;
}
private int trval(TreeNode root) {
if (root == null) return -1;
int left = trval(root.left);
int right = trval(root.right);
if (left == 0 || right == 0) {
count++;
return 2;
}
if (left == 2 || right == 2) {
return 1;
}
return 0;
}
}
```
Python
Go
-----------------------
* 作者微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw)
* B站视频[代码随想录](https://space.bilibili.com/525438321)
* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)
<div align="center"><img src=../pics/公众号.png width=450 alt=> </img></div>