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

736 lines
22 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://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 968.监控二叉树
[力扣题目链接](https://leetcode.cn/problems/binary-tree-cameras/)
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229175736596.png)
* 输入:[0,0,null,0,0]
* 输出1
* 解释:如图所示,一台摄像头足以监控所有节点。
示例 2
![](https://code-thinking-1253855093.file.myqcloud.com/pics/2020122917584449.png)
* 输入:[0,0,null,0,null,0,null,null,0]
* 输出2
* 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
* 给定树的节点数的范围是 [1, 1000]。
* 每个节点的值都是 0。
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[贪心算法,二叉树与贪心的结合,有点难...... LeetCode:968.监督二叉树](https://www.bilibili.com/video/BV1SA411U75i),相信结合视频在看本篇题解,更有助于大家对本题的理解**。
## 思路
这道题目首先要想,如何放置,才能让摄像头最小的呢?
从题目中示例,其实可以得到启发,**我们发现题目示例中的摄像头都没有放在叶子节点上!**
这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。
那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?
因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
**所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!**
局部最优推出全局最优,找不出反例,那么就按照贪心来!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
此时这道题目还有两个难点:
1. 二叉树的遍历
2. 如何隔两个节点放一个摄像头
### 确定遍历顺序
在二叉树中如何从低向上推导呢?
可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。
后序遍历代码如下:
```CPP
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (终止条件) return ;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
逻辑处理 // 中
return ;
}
```
**注意在以上代码中我们取了左孩子的返回值右孩子的返回值即left 和 right 以后推导中间节点的状态**
### 如何隔两个节点放一个摄像头
此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移!
来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:
有如下三种:
* 该节点无覆盖
* 本节点有摄像头
* 本节点有覆盖
我们分别有三个数字来表示:
* 0该节点无覆盖
* 1本节点有摄像头
* 2本节点有覆盖
大家应该找不出第四个节点的状态了。
**一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。**
**因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?**
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。
那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。
**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了**
接下来就是递推关系。
那么递归的终止条件应该是遇到了空节点此时应该返回2有覆盖原因上面已经解释过了。
代码如下:
```CPP
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
```
递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。
主要有如下四类情况:
* 情况1左右节点都有覆盖
左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。
如图:
![968.监控二叉树2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229203710729.png)
代码如下:
```CPP
// 左右节点都有覆盖
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代表中间节点放摄像头。
代码如下:
```CPP
if (left == 0 || right == 0) {
result++;
return 1;
}
```
* 情况3左右节点至少有一个有摄像头
如果是以下情况,其实就是 左右孩子节点有一个有摄像头了那么其父节点就应该是2覆盖的状态
* left == 1 && right == 2 左节点有摄像头,右节点有覆盖
* left == 2 && right == 1 左节点有覆盖,右节点有摄像头
* left == 1 && right == 1 左右节点都有摄像头
代码如下:
```CPP
if (left == 1 || right == 1) return 2;
```
**从这个代码中可以看出如果left == 1, right == 0 怎么办其实这种条件在情况2中已经判断过了**,如图:
![968.监控二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020122920362355.png)
这种情况也是大多数同学容易迷惑的情况。
4. 情况4头结点没有覆盖
以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图:
![968.监控二叉树3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229203742446.png)
所以递归结束之后还要判断根节点如果没有覆盖result++,代码如下:
```CPP
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
```
以上四种情况我们分析完了,代码也差不多了,整体代码如下:
**以下我的代码注释很详细,为了把情况说清楚,特别把每种情况列出来。**
C++代码如下:
```CPP
// 版本一
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;
}
};
```
在以上代码的基础上,再进行精简,代码如下:
```CPP
// 版本二
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;
}
};
```
* 时间复杂度: O(n),需要遍历二叉树上的每个节点
* 空间复杂度: O(n)
大家可能会惊讶,居然可以这么简短,**其实就是在版本一的基础上使用else把一些情况直接覆盖掉了**。
在网上关于这道题解可以搜到很多这种神级别的代码,但都没讲不清楚,如果直接看代码的话,指定越看越晕,**所以建议大家对着版本一的代码一步一步来,版本二中看不中用!**。
## 总结
本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。
在二叉树上进行状态推导,其实难度就上了一个台阶了,需要对二叉树的操作非常娴熟。
这道题目是名副其实的hard大家感受感受。
## 其他语言版本
### Java
```java
class Solution {
int res=0;
public int minCameraCover(TreeNode root) {
// 对根节点的状态做检验,防止根节点是无覆盖状态 .
if(minCame(root)==0){
res++;
}
return res;
}
/**
节点的状态值:
0 表示无覆盖
1 表示 有摄像头
2 表示有覆盖
后序遍历,根据左右节点的情况,来判读 自己的状态
*/
public int minCame(TreeNode root){
if(root==null){
// 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
return 2;
}
int left=minCame(root.left);
int right=minCame(root.right);
// 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
if(left==2&&right==2){
//(2,2)
return 0;
}else if(left==0||right==0){
// 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头
// (0,0) (0,1) (0,2) (1,0) (2,0)
// 状态值为 1 摄像头数 ++;
res++;
return 1;
}else{
// 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头
// 那么本节点就是处于被覆盖状态
return 2;
}
}
}
```
### Python
贪心(版本一)
```python
class Solution:
# Greedy Algo:
# 从下往上安装摄像头跳过leaves这样安装数量最少局部最优 -> 全局最优
# 先给leaves的父节点安装然后每隔两层节点安装一个摄像头直到Head
# 0: 该节点未覆盖
# 1: 该节点有摄像头
# 2: 该节点有覆盖
def minCameraCover(self, root: TreeNode) -> int:
# 定义递归函数
result = [0] # 用于记录摄像头的安装数量
if self.traversal(root, result) == 0:
result[0] += 1
return result[0]
def traversal(self, cur: TreeNode, result: List[int]) -> int:
if not cur:
return 2
left = self.traversal(cur.left, result)
right = self.traversal(cur.right, result)
# 情况1: 左右节点都有覆盖
if left == 2 and 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 or right == 0:
result[0] += 1
return 1
# 情况3:
# left == 1 && right == 2 左节点有摄像头,右节点有覆盖
# left == 2 && right == 1 左节点有覆盖,右节点有摄像头
# left == 1 && right == 1 左右节点都有摄像头
if left == 1 or right == 1:
return 2
```
贪心版本二利用elif精简代码
```python
class Solution:
# Greedy Algo:
# 从下往上安装摄像头跳过leaves这样安装数量最少局部最优 -> 全局最优
# 先给leaves的父节点安装然后每隔两层节点安装一个摄像头直到Head
# 0: 该节点未覆盖
# 1: 该节点有摄像头
# 2: 该节点有覆盖
def minCameraCover(self, root: TreeNode) -> int:
# 定义递归函数
result = [0] # 用于记录摄像头的安装数量
if self.traversal(root, result) == 0:
result[0] += 1
return result[0]
def traversal(self, cur: TreeNode, result: List[int]) -> int:
if not cur:
return 2
left = self.traversal(cur.left, result)
right = self.traversal(cur.right, result)
# 情况1: 左右节点都有覆盖
if left == 2 and 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 左节点覆盖,右节点无覆盖
elif left == 0 or right == 0:
result[0] += 1
return 1
# 情况3:
# left == 1 && right == 2 左节点有摄像头,右节点有覆盖
# left == 2 && right == 1 左节点有覆盖,右节点有摄像头
# left == 1 && right == 1 左右节点都有摄像头
else:
return 2
```
### Go
```go
const inf = math.MaxInt64 / 2
func minCameraCover(root *TreeNode) int {
var dfs func(*TreeNode) (a, b, c int)
dfs = func(node *TreeNode) (a, b, c int) {
if node == nil {
return inf, 0, 0
}
lefta, leftb, leftc := dfs(node.Left)
righta, rightb, rightc := dfs(node.Right)
a = leftc + rightc + 1
b = min(a, min(lefta+rightb, righta+leftb))
c = min(a, leftb+rightb)
return
}
_, ans, _ := dfs(root)
return ans
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
```
### Javascript
```Javascript
var minCameraCover = function(root) {
let result = 0
function traversal(cur) {
if(cur === null) {
return 2
}
let left = traversal(cur.left)
let right = traversal(cur.right)
if(left === 2 && right === 2) {
return 0
}
if(left === 0 || right === 0) {
result++
return 1
}
if(left === 1 || right === 1) {
return 2
}
return -1
}
if(traversal(root) === 0) {
result++
}
return result
};
```
### TypeScript
```typescript
function minCameraCover(root: TreeNode | null): number {
/** 0-无覆盖, 1-有摄像头, 2-有覆盖 */
type statusCode = 0 | 1 | 2;
let resCount: number = 0;
if (recur(root) === 0) resCount++;
return resCount;
function recur(node: TreeNode | null): statusCode {
if (node === null) return 2;
const left: statusCode = recur(node.left),
right: statusCode = recur(node.right);
let resStatus: statusCode = 0;
if (left === 0 || right === 0) {
resStatus = 1;
resCount++;
} else if (left === 1 || right === 1) {
resStatus = 2;
} else {
resStatus = 0;
}
return resStatus;
}
};
```
### C
```c
/*
**函数后序遍历二叉树。判断一个结点状态时,根据其左右孩子结点的状态进行判断
**状态0为没有被摄像头覆盖到。1为此结点处应设置摄像头。2为此结点已被摄像头覆盖
*/
int traversal(struct TreeNode* node, int* ans) {
//递归结束条件传入结点为NULL假设此结点能被摄像头覆盖。这样方便与对叶子结点的判断将叶子结点设为0
if(!node)
return 2;
//后序遍历二叉树,记录左右孩子的状态。根据左右孩子状态更新结点自身状态
int left = traversal(node->left, ans);
int right = traversal(node->right, ans);
//若左右孩子都可以被摄像头覆盖将父亲结点状态设为0
if(left == 2 && right == 2) {
return 0;
}
//若左右孩子有一个结点状态为没有被覆盖0则将父亲结点状态设置为摄像头
if(left == 0 || right == 0) {
(*ans)++;
return 1;
}
//若左右孩子有一个为摄像头证明父亲结点可以被覆盖。将父亲结点状态变为2
if(left == 1 || right == 1)
return 2;
//逻辑不会走到-1语句不会执行
return -1;
}
int minCameraCover(struct TreeNode* root){
int ans = 0;
//在对整个二叉树遍历后。头结点可能未被覆盖这时候如果函数返回值为0证明头结点未被覆盖。说明头结点也需要添置摄像头ans++
if(traversal(root, &ans) == 0)
ans++;
return ans;
}
```
### Scala
```scala
object Solution {
def minCameraCover(root: TreeNode): Int = {
var result = 0
def traversal(cur: TreeNode): Int = {
// 空节点,该节点有覆盖
if (cur == null) return 2
var left = traversal(cur.left)
var right = traversal(cur.right)
// 情况1左右节点都有覆盖
if (left == 2 && right == 2) {
return 0
}
// 情况2
if (left == 0 || right == 0) {
result += 1
return 1
}
// 情况3
if (left == 1 || right == 1) {
return 2
}
-1
}
if (traversal(root) == 0) {
result += 1
}
result
}
}
```
### Rust
```Rust
/// 版本一
impl Solution {
pub fn min_camera_cover(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
let mut res = 0;
if Self::traversal(&root, &mut res) == 0 {
res += 1;
}
res
}
pub fn traversal(cur: &Option<Rc<RefCell<TreeNode>>>, ans: &mut i32) -> i32 {
// 0 未覆盖 1 节点已设置摄像头 2 节点已覆盖
if let Some(node) = cur {
let node = node.borrow();
let left = Self::traversal(&node.left, ans);
let right = Self::traversal(&node.right, ans);
// 左右节点都被覆盖
if left == 2 && right == 2 {
return 0; // 无覆盖
}
// left == 0 right == 0 左右无覆盖
// left == 0 right == 1 左节点无覆盖 右节点有摄像头
// left == 1 right == 0 左节点有摄像头 左节点无覆盖
// left == 0 right == 2 左节点无覆盖 右节点有覆盖
// left == 2 right == 0 左节点有覆盖 右节点无覆盖
if left == 0 || right == 0 {
*ans += 1;
return 1;
}
// left == 1 right == 1 左节点有摄像头 右节点有摄像头
// left == 1 right == 2 左节点有摄像头 右节点覆盖
// left == 2 right == 1 左节点覆盖 右节点有摄像头
if left == 1 || right == 1 {
return 2; // 已覆盖
}
} else {
return 2;
}
-1
}
}
/// 版本二
enum NodeState {
NoCover = 0,
Camera = 1,
Covered = 2,
}
impl Solution {
pub fn min_camera_cover(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
let mut res = 0;
let state = Self::traversal(&root, &mut res);
match state {
NodeState::NoCover => res + 1,
_ => res,
}
}
pub fn traversal(cur: &Option<Rc<RefCell<TreeNode>>>, ans: &mut i32) -> NodeState {
if let Some(node) = cur {
let node = node.borrow();
let left_state = Self::traversal(&node.left, ans);
let right_state = Self::traversal(&node.right, ans);
match (left_state, right_state) {
(NodeState::NoCover, _) | (_, NodeState::NoCover) => {
*ans += 1;
NodeState::Camera
}
(NodeState::Camera, _) | (_, NodeState::Camera) => NodeState::Covered,
(_, _) => NodeState::NoCover,
}
} else {
NodeState::Covered
}
}
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>