监控二叉树

标签: 深度优先搜索 动态规划 二叉树

难度: Hard

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。


提示:

  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。

Submission

运行时间: 24 ms

内存: 16.3 MB

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    #def minCameraCover(self, root: Optional[TreeNode]) -> int:
    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

Explain

题解采用了递归的方法,利用后序遍历(先处理子节点,再处理当前节点)的方式来决定在哪些节点上安装摄像头。递归函数返回三种状态:0 表示当前节点未被覆盖,1 表示当前节点已安装摄像头,2 表示当前节点已被覆盖但未安装摄像头。根据子节点的状态来决定当前节点的操作: 1. 如果任一子节点未被覆盖(返回0),则在当前节点安装摄像头,并返回状态1。 2. 如果所有子节点都已被覆盖(返回2),则当前节点返回状态0。 3. 如果任一子节点安装了摄像头(返回1),则当前节点返回状态2,表示已被覆盖。 最后,若根节点在递归后仍未被覆盖,则在根节点额外安装一个摄像头。

时间复杂度: O(n)

空间复杂度: O(n) in the worst case, O(log(n)) in the best case

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def minCameraCover(self, root: TreeNode) -> int:
        result = [0]  # This list will store the number of cameras needed
        if self.traversal(root, result) == 0:  # If root is not covered
            result[0] += 1  # Place a camera at the root
        return result[0]

    def traversal(self, cur: TreeNode, result: list[int]) -> int:
        if not cur:  # If current node is None, it's considered covered
            return 2

        left = self.traversal(cur.left, result)
        right = self.traversal(cur.right, result)

        if left == 2 and right == 2:  # If both children are covered
            return 0  # Current node is not covered

        elif left == 0 or right == 0:  # If any child is not covered
            result[0] += 1  # Place a camera at the current node
            return 1  # Current node has a camera

        else:  # If at least one child has a camera or both are covered
            return 2  # Current node is covered

Explore

在递归函数中,一个节点是否已经被覆盖是通过其返回的状态值来判断的。如果一个节点返回状态1,说明在该节点上已经安装了摄像头,从而该节点及其子树均被覆盖;如果返回状态2,说明该节点已经被覆盖但未安装摄像头,这通常是因为它的某个子节点上安装了摄像头。如果返回状态0,这表示该节点未被覆盖,需要在其父节点或者该节点本身安装摄像头来确保覆盖。

尽管递归处理过程中设计了多种情况以确保大部分节点被覆盖,但还是存在一种情况:如果根节点的左右子节点都返回状态2(即它们都已被覆盖但未安装摄像头),根据递归逻辑,根节点本身会返回状态0(未被覆盖)。这是因为状态2的子节点假定它们的父节点或上方某处已被安装了摄像头。因此,需要在递归结束后检查根节点的状态,如果是0,说明需要在根节点额外安装一个摄像头来确保整个树被覆盖。

在递归终止条件中,不存在的节点(即cur为None的情况)被认为是已经覆盖(返回状态2),是因为不存在的节点不需要监控。这种设计简化了递归逻辑,免去了在递归过程中对空节点进行额外处理的需要。当一个节点的子节点为None时,可以认为这个子节点已经安全,不需要额外的摄像头覆盖。这使得算法可以专注于处理实际存在的节点。

当一个节点的左右子节点均返回状态2时,意味着这两个子节点都已被覆盖但它们自身并未安装摄像头。在这种情况下,这两个子节点的覆盖是由它们各自的子节点或更下层的节点上的摄像头保证的。因此,当前节点(父节点)本身并没有直接被任何摄像头覆盖,也没有其子节点在其上安装摄像头来提供覆盖。所以,当前节点的状态返回0,表示它未被覆盖,需要在其上或更高层节点安装摄像头以确保覆盖。