大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。
题目
968. 监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入: [0,0,null,0,0]
输出: 1
解释: 如图所示,一台摄像头足以监控所有节点。
复制代码
示例 2:
输入: [0,0,null,0,null,0,null,null,0]
输出: 2
解释: 需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
复制代码
提示:
- 给定树的节点数的范围是
[1, 1000]
。 - 每个节点的值都是 0。
思路
- 要想实现每个节点都监控,那么我们要么监控当前节点,要么监控当前节点的某个子节点;
- 题目中要找到最小摄像头数量,所以我们可以用动态规划, 确保到达每个层级、每个节点的时候我们的解都是最优解,这样子到达最后一层的最后一个节点的时候,我们的答案自然是最佳答案;
- 那么动态规划就涉及到状态转移方程,举个例子,我们想要监听一个如下的树结构的根节点,那么我们就需要监听根节点或者监听左右两个子节点其中之一;
- 如果左右两个子节点还有子节点,那么可以分成几种情况: 在1处放置摄像头,然后再计算4-5-6三个节点需要花费多少个摄像头,或者在2处放摄像头,然后再计算3-5-6这三个节点需要花费多少个摄像头,或者在3处放摄像头,然后计算2-4需要花费多少个摄像头,只有这三种情况没有第四种了, 那么我们可以求这三种情况的最小值;
- 我们可以判断一下当前节点是否有左右子节点,没有的话就不进行计算,将当前的问题递归的进行求解,只要我们确保了每一步都是最优的,那么答案自然是最小值。
实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var minCameraCover = function(root, isMust = false, isCover = false) {
if (!root) {
return 0;
}
// 根据是否有子节点进行分类讨论
if (!root.left && !root.right) {
// 没有子节点,说明当前的监控完就完事了
// 如果没监控过就加个摄像头监听当前节点
return isCover ? 0 : 1;
} else if (!root.left || !root.right) {
// 如果必须监控的话
if (isMust) {
return minCameraCover(root.left || root.right, false, true) + 1;
}
// 如果已经监控过的话
if (isCover) {
return Math.min(
minCameraCover(root.left || root.right, false, false),
minCameraCover(root.left || root.right, false, true) + 1
);
}
// 如果只有一个子节点,这一轮没监控那么就必须子节点监控
return Math.min(
minCameraCover(root.left || root.right, false, true) + 1,
minCameraCover(root.left || root.right, true, false),
)
} else {
// 如果必须监控的话
if (isMust) {
return minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1;
}
// 如果已经监控过的话
if (isCover) {
return Math.min(minCameraCover(root.left, false, false) +
minCameraCover(root.right, false, false),
minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1);
}
// 如果有两个子节点,就从中二选一,或者直接监控根节点
return Math.min(
...[
minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1,
minCameraCover(root.left, true, false) +
minCameraCover(root.right, false, false),
minCameraCover(root.left, false, false) +
minCameraCover(root.right, true, false),
]
)
}
};
复制代码
翻车
好不容易整理完思路,把代码咔咔一通敲出来,结果发现超时了。尴尬,而且写了很多冗余代码,先精简一版看看行不行。
精简代码
/**
* @param {TreeNode} root
* @return {number}
*/
var minCameraCover = function(root, isMust = false, isCover = false) {
if (!root) {
return 0;
}
// 根据是否有子节点进行分类讨论
if (!root.left && !root.right) {
// 没有子节点,说明当前的监控完就完事了
// 如果没监控过就加个摄像头监听当前节点
return isCover ? 0 : 1;
} else {
const isLose = !root.left || !root.right;
// 如果必须监控的话
if (isMust) {
return minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1;
}
// 如果已经监控过的话
if (isCover) {
return Math.min(minCameraCover(root.left, false, false) +
minCameraCover(root.right, false, false),
minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1);
}
// 如果只有一个子节点,这一轮没监控那么就必须子节点监控
// 如果有两个子节点,就从中二选一,或者直接监控根节点
return isLose ?
Math.min(
minCameraCover(root.left || root.right, false, true) + 1,
minCameraCover(root.left || root.right, true, false),
)
: Math.min(
...[
minCameraCover(root.left, false, true) +
minCameraCover(root.right, false, true) + 1,
minCameraCover(root.left, true, false) +
minCameraCover(root.right, false, false),
minCameraCover(root.left, false, false) +
minCameraCover(root.right, true, false),
]
)
}
};
复制代码
再次翻车
这个版本的代码以及精简了很多了,可读性方面已经勉强可以接受了,但是还是超时,说明我们直接递归是走不通的,所以我们要找规律。其实我们自上而下的过程中,需要对比每一个节点是否最优,相当于每次要自上而下把每一条可以走的路全部走一遍,这样子性能是最低的。所以我们换种思路,自下而上走,思路是一样的。
再次起航
- 如果节点没有子节点,那么可以在自己身上挂摄像头,也可以在父节点身上挂摄像头,我们用贪心的思想,想求最少的摄像头,那么能在父节点挂摄像头就别挂在子节点身上;
- 我们可以用三个数字
0 - 1 - 2
来标识各个节点的监控情况,0
代表没有被监控,如果当前节点为0
,那么得考虑它有没有子元素,如果没有子元素可以在它父节点身上挂个摄像头; - 换位思考一下,每次我们只需要判断当前节点有没有未被监控的子节点,如果有的话就给当前节点挂摄像头,挂了摄像头的话我们需要统计数量,把节点的摄像头数量累加;
- 如果当前节点挂了摄像头,那么当前节点的值用
2
来标识,父节点可以判断子节点中是否存在摄像头,如果存在那么父节点可以用1
来表示已经监控过了,不过判断之前得确保,另一个节点是被监控状态的,也就是另一个子节点的值不能是0
; - 在子节点不为
0
的情况下,当前节点如果是1
则不用进行操作; - 最终每个节点的最优解,可以转换为左节点的最优解+右节点的最优解,如果当前需要监控就再加一。
最终代码
/**
* @param {TreeNode} root
* @return {number}
*/
var minCameraCover = function(root) {
let result = dfs(root);
// 递归后判断当前摄像头还是否需要监控
if (root && root.val === 0) {
result++
}
return result;
};
// 遍历二叉树,找子节点是否存在摄像头
function dfs(node) {
if (!node) {
return 0;
}
// 后序遍历,先访问左右子节点,判断他们是否被监控
const left = dfs(node.left);
const right = dfs(node.right);
let result = left + right;
// 约定好,摄像头的三个状态
// 0代表没有被监控到
// 1代表被监控到了
// 2代表装了摄像头
// 子节点没有被监控到的时候说明该父节点出马了
if (node.left && node.left.val === 0 || node.right && node.right.val === 0) {
node.val = 2;
result++;
} else // 子节点装了摄像头说明父节点也可以监控到
if (node.left && node.left.val === 2 || node.right && node.right.val === 2) {
node.val = 1;
}
return result;
}
复制代码
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。