【LeetCode贪心#12】图解监控二叉树(正宗hard题,涉及贪心分析、二叉树遍历以及状态转移)
监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
- 输入:[0,0,null,0,0]
- 输出:1
- 解释:如图所示,一台摄像头足以监控所有节点。
示例 2:
- 输入:[0,0,null,0,null,0,null,null,0]
- 输出:2
- 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
- 给定树的节点数的范围是 [1, 1000]。
- 每个节点的值都是 0。
思路
难题警告
题意解读
由题目和示例可知,本题的目标是在二叉树的合适的节点上"装摄像头",那么装了摄像头的节点可以覆盖的范围是本层加上其两个子节点(如果有的话)和一个父节点,即一共可以覆盖三层
由此可以推知,我们应该尽量在叶子节点的父节点处放置摄像头,这样可以充分利用其三层覆盖范围
所以我们可以从下往上推,在叶子节点的父节点放摄像头,然后其父节点的上一个节点(即父节点的父节点)再隔一个节点放置摄像头
对应颜色的摄像头的覆盖范围如图所示,我们在橙色的节点处放置了摄像头,其有两个叶子节点,因此这是符合我们的推断的
该摄像头节点的覆盖范围包含了其父节点,因此我们无需在其父节点的父节点处放置摄像头,因为这会导致覆盖范围重叠,进而浪费一个覆盖范围,这不符合我们设置最少摄像头的目标
贪心点
这里还会出现一个问题,就是在根节点处放摄像头其实也浪费了一个覆盖范围(就是其父节点的范围)
所以如果严格按照设置最少摄像头的目标来做的话,此时我们应该将摄像头设置在根节点的父节点
但是,实际上这么做只是在局部上满足了最优解,在全局上并不是最优的
因为叶子节点的数量远大于根节点的数量,如果按照“在根节点的父节点设置摄像头”的规则来做的话,那么会影响到叶子节点设置摄像头的方法,进而影响最后总摄像头的数量
因此,不能为了节省根节点浪费的一个范围牺牲其他更多更合理的摄像头设置位置
总结一下摄像头安放原则:优先在叶子节点的父节点安放摄像头,然后从下往上推,每隔两个节点再放一个摄像头,直到遍历至根节点
状态设定
(仅用于模拟,无需选择最优状态)
通过上面的分析,我们遍历二叉树的顺序自然的应该选择后序遍历
问题又来了,怎么控制每隔两个节点放一个摄像头呢?
这里可以设置几个状态,然后记录每个节点的状态,根据左右子节点的状态去确定父节点的状态
通过节点间的状态关系,来确定某处是否应该安放摄像头
根据分析,可以得出以下三种节点状态:
- 没被覆盖(0)
- 有摄像头(1)
- 被覆盖(2)
这里要么节点是被覆盖,要么是没被覆盖,没必要设置一个没有摄像头的状态(有摄像头其实也是被覆盖的)
然后这里需要讨论二叉树中空节点的状态设置问题
以上述二叉树为例,因为就三种状态嘛,那这个NULL就有三种情况,看看哪种情况符合我们的要求
情况1: NULL节点没被覆盖(0)
因为这个NULL节点被设置为无覆盖状态,那按题意一定要有一个摄像头去覆盖它,因此NULL的父节点,也就是叶子节点就需要放置摄像头
而这与我们前面设计的放置原则(优先在叶子节点的父节点安放摄像头)有冲突,且推到后面,根节点处会没有摄像头(反正就是不合适)
因此情况1否了
情况2: NULL节点有摄像头(1)
这种情况也不行,因为如果NULL节点有摄像头,就意味着叶子节点已经被覆盖了
那么下一步需要隔两个节点再放置摄像头,那叶子节点的父节点就又没有摄像头了,即又不满足设计初衷
否了
情况3: NULL节点被覆盖(2)
前两种都不行,那这个肯定行了,简单推一下就知道这种情况是满足条件的
因此,空节点应该被设置为有覆盖状态(2)
状态转移
规定好状态之后,如何在遍历过程中对每个节点的状态进行转换并统计摄像头个数呢?
这里又有几种情况需要讨论:(后序遍历)
情况1:左右子节点都有覆盖
某个节点的左右子节点均被覆盖时,覆盖它们的摄像头肯定是不同的,因此当前节点应该设置为无覆盖(0),等待之后遍历到父节点设置摄像头将其覆盖
情况2:左右子节点至少有一个无覆盖
不论左右,当某个节点的子节点出现无覆盖状态时,当前节点都应该设置为摄像头状态(1),这样才能不漏掉这个没被覆盖的节点
情况3: 左右子节点至少有一个有摄像头
因为摄像头的覆盖范围是三层,某个节点的子节点有摄像头,那该节点肯定是被覆盖到了的,因此节点状态设为有覆盖(2)
情况4:遍历完毕根节点还是没被覆盖
确实是有这种情况,此时需要对根节点进行单独修改,放置一个摄像头(1)
虽然这样会浪费一个覆盖范围,但是也没办法
至此,所有状态转移的情况分析完毕(够复杂够阴间了吧...)
代码
要使用后序遍历,因此可以直接使用二叉树后序递归遍历的模板(详见)
为了复习之前的知识,这里还是使用递归三部曲的方式写一下代码
递归三部曲
1、确定递归函数的参数和返回值
根据分析,我们在遍历二叉树的过程中是通过判断当前节点的状态(依据是左右子节点的状态)来决定是否要设置摄像头的
因此递归的返回值应该是一个节点的状态(0、1、2其中之一),因此递归函数有返回值int
输入参数肯定是待判断状态的节点
class Solution {
private:
int res = 0;//记录摄像头数量
int traversal(TreeNode* cur){//确定递归函数参数与返回值
}
public:
int minCameraCover(TreeNode* root) {
}
};
2、确定终止条件
遇到叶子节点就终止,和遍历模板中一致
class Solution {
private:
int res = 0;//记录摄像头数量
int traversal(TreeNode* cur){//确定递归函数参数与返回值
if(cur == NULL) return 2;//空节点按规则返回2状态
}
public:
int minCameraCover(TreeNode* root) {
}
};
3、确定单层处理逻辑
这里就是完成左右中的后序遍历逻辑就行
需要在中的位置处理我们的3中左右子节点的情况,来给当前节点返回一个状态(第四种状态在主函数中处理)
class Solution {
private:
int res = 0;//记录摄像头数量
int traversal(TreeNode* cur){//确定递归函数参数与返回值
//确定终止条件,遇到叶子节点终止,该节点有覆盖(因为是空节点)
if(cur == NULL) return 2;
//确定单层处理逻辑,即左右中
int left = traversal(cur->left);//左
int right = traversal(cur->right);//右
//中,处理4种左右节点的情况
if(left == 2 && right == 2) return 0;//左右子节点都有覆盖(2)
if(left == 0 || right == 0){//左右子节点至少有一个无覆盖
res++;
return 1;
}
if(left == 1 || right == 1) return 2;//左右子节点至少有一个有摄像头(1)
return -1;//需要一个返回值,但代码不会运行到这
}
public:
int minCameraCover(TreeNode* root) {
}
};
注意,递归函数有返回值,需要用变量接一下,并且最后需要给一个返回值-1(尽管不会执行到此处)
完整代码
剩下的一种情况是遍历结束根节点没被摄像头覆盖,我们可以在主函数中处理该情况
只要在调用递归函数后判断一下最终返回值是否为0即可(注意返回值不是res)
如果是0表示当前二叉树在遍历结束后,根节点的状态没有变为被覆盖(2),要多设置一个摄像头(就让res再加1即可)
class Solution {
private:
int res = 0;//记录摄像头数量
int traversal(TreeNode* cur){//确定递归函数参数与返回值
//确定终止条件,遇到叶子节点终止,该节点有覆盖(因为是空节点)
if(cur == NULL) return 2;
//确定单层处理逻辑,即左右中
int left = traversal(cur->left);//左
int right = traversal(cur->right);//右
//中,处理4种左右节点的情况
if(left == 2 && right == 2) return 0;//左右子节点都有覆盖(2)
if(left == 0 || right == 0){//左右子节点至少有一个无覆盖
res++;
return 1;
}
if(left == 1 || right == 1) return 2;//左右子节点至少有一个有摄像头(1)
return -1;//需要一个返回值,但代码不会运行到这
}
public:
int minCameraCover(TreeNode* root) {
//在这里处理root无覆盖的情况
if(traversal(root) == 0){//调用递归进行后序遍历,如果根节点没被覆盖,额外加个摄像头
res++;
return res;
}
return res;
}
};
【LeetCode贪心#12】图解监控二叉树(正宗hard题,涉及贪心分析、二叉树遍历以及状态转移)的更多相关文章
- LeetCode:二叉树的前、中、后序遍历
描述: ------------------------------------------------------- 前序遍历: Given a binary tree, return the pr ...
- leetcode 55. Jump Game、45. Jump Game II(贪心)
55. Jump Game 第一种方法: 只要找到一个方式可以到达,那当前位置就是可以到达的,所以可以break class Solution { public: bool canJump(vecto ...
- leetcode 117. 填充每个节点的下一个右侧节点指针 II(二叉树,DFS)
题目链接 https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/ 题目大意 给定一个二叉树 s ...
- 数据结构与算法系列研究五——树、二叉树、三叉树、平衡排序二叉树AVL
树.二叉树.三叉树.平衡排序二叉树AVL 一.树的定义 树是计算机算法最重要的非线性结构.树中每个数据元素至多有一个直接前驱,但可以有多个直接后继.树是一种以分支关系定义的层次结构. a.树是n ...
- 刷题-力扣-107. 二叉树的层序遍历 II
107. 二叉树的层序遍历 II 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/binary-tree-level-order-tr ...
- 二叉树-你必须要懂!(二叉树相关算法实现-iOS)
这几天详细了解了下二叉树的相关算法,原因是看了唐boy的一篇博客(你会翻转二叉树吗?),还有一篇关于百度的校园招聘面试经历,深刻体会到二叉树的重要性.于是乎,从网上收集并整理了一些关于二叉树的资料,及 ...
- 数据结构二叉树的java实现,包括二叉树的创建、搜索、删除和遍历
根据自己的学习体会并参考了一些网上的资料,以java写出了二叉树的创建.搜索.删除和遍历等操作,尚未实现的功能有:根据先序和中序遍历,得到后序遍历以及根据后序和中序遍历,得到先序遍历,以及获取栈的深度 ...
- 二叉树转换成森林&森林变成二叉树
一,树转换成二叉树 1,将全部兄弟结点连起来; 2,保留第一个兄弟结点与父节点的连接,断开其它兄弟结点与父节点的连接,然后以根节点为轴依照顺时针方向旋转45度 watermark/2/text/aHR ...
- 【javascript】操作给定的二叉树,将其变换为源二叉树的镜像。
操作给定的二叉树,将其变换为源二叉树的镜像. 输入描述: 二叉树的镜像定义:源二叉树 8 / \ 6 10 / \ / \ 5 7 9 11 镜像二叉树 8 / \ 10 6 / \ / \ 11 9 ...
- 剑指offer58:对称的二叉树。判断一颗二叉树是不是对称的,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的
1 题目描述 请实现一个函数,用来判断一颗二叉树是不是对称的.注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的. 2 思路和方法 定义一种遍历算法,先遍历右子结点再遍历左子结点:如对称先序 ...
随机推荐
- go理论知识总结
基于const常量理解个中类型的内存分配引入参考 官方:Constant expressions may contain only constant operands and are evaluate ...
- python机器学习——随机森林算法
背景与原理: 首先我们需要知道集成学习的概念,所谓集成学习,就是使用一系列学习器进行学习,并且通过某种规则把这些学习器的学习结果整合起来从而获得比单个学习器学习效果更好的机器学习方法.这样的方法可以用 ...
- linux火狐添加flash插件
从官网上 adobe flash player 全版本通用安装 找到 linux 版本的 .gz 包, 复制下载链接 linux命令 su - cd /tmp wget -P /tmp https: ...
- 如何使用autotools/automake自动生成Makefile文件
前言: Linux下编程时,为了方便编译,往往使用Makefile文件自动完成编译,但是Makefile文件本身的书写十分复杂,规则很多.好在Linux为我们提供了自动生成功能完善的Makefile文 ...
- Absolute Path Traversal 错误解决
Absolute Path Traversal (APT) 是一种常见的安全漏洞,攻击者可以通过该漏洞访问应用程序的文件系统中的文件, 包括敏感信息,从而可能导致应用程序遭受攻击. 一.使用专门的文件 ...
- 注释中的Unicode编码也会被转义
现象 public class Unicode { public static void main(String[] args) { // \u000d System.out.println(&quo ...
- 异常(Java)
异常 基本介绍 什么是异常? 在实际工作中,遇到的情况不可能是非常完美.比如:你写的某个模块,用户输入不一定符合你的要求.你的程序要打开某个文件,这个文件可能不存在或者文件格式不对 软件程序在运行过程 ...
- echarts 图表动态刷新数据
需求:每次重新加载数据,图表柱状图从零开始加载 用 myChart.clear(); 这个方式解决. 在setOption 之前用 示例: myChart.clear(); myChart.setOp ...
- Matlab %补充---用的多的函数
Input promat = 'This is a sentence.' x = input(prompt) %显示prompt中的文本并等待用户输入数值或者表达式后按Return %如果用户什么都 ...
- spacy
官方文档: https://spacy.io/api Spacy功能简介 可以用于进行分词,命名实体识别,词性识别等等,但是首先需要下载预训练模型 pip install --user spacy p ...