968. Binary Tree Cameras
Problem Description
You have a binary tree and need to install security cameras on some of its nodes. Each camera placed on a node can monitor:
- The node itself
- Its parent node (if it exists)
- Its immediate children nodes (if they exist)
Your task is to find the minimum number of cameras needed to monitor every single node in the tree.
For example, if you place a camera on a node, it will cover that node, the node directly above it, and the nodes directly below it. The goal is to strategically place cameras so that all nodes are monitored while using as few cameras as possible.
The solution uses dynamic programming with three states for each node:
- State
a
: The node has a camera installed on it - State
b
: The node doesn't have a camera but is monitored by at least one of its children - State
c
: The node doesn't have a camera and is not monitored by its children
The algorithm recursively calculates the minimum cameras needed for each state at every node, considering:
- If a node has a camera (
a
), both its children are automatically monitored, so we can choose any state for them - If a node is monitored by its children (
b
), at least one child must have a camera - If a node is not monitored by its children (
c
), both children must be monitored by their own children
The final answer is the minimum between placing a camera at the root or having the root monitored by its children.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure.
DFS
- We arrive at DFS (Depth-First Search) as our algorithm choice.
Conclusion: The flowchart suggests using a DFS approach for solving the Binary Tree Cameras problem.
This makes perfect sense because:
- We need to traverse the entire tree to determine camera placements
- The solution requires us to make decisions at each node based on information from its children (bottom-up approach)
- DFS allows us to recursively process subtrees and aggregate results back up to parent nodes
- The dynamic programming states (whether a node has a camera, is monitored by children, or is not monitored) are naturally computed in a post-order DFS traversal where we process children before their parents
The DFS pattern enables us to:
- Visit each node exactly once
- Calculate the optimal camera placement for each subtree before deciding on parent nodes
- Propagate the monitoring states from leaves up to the root
- Choose the minimum number of cameras needed based on the three possible states at each node
Intuition
The key insight is to think about this problem from the bottom up - starting from the leaves and working our way to the root. Why? Because a camera's coverage extends both upward and downward, creating dependencies between parent and child nodes.
Consider what happens at each node. We have three possible situations:
- Place a camera at this node
- Don't place a camera, but get monitored by a child's camera
- Don't place a camera and don't get monitored by children (rely on parent's camera)
The greedy approach of always placing cameras as high as possible doesn't work because we might miss optimal placements. Instead, we need to consider all possibilities and choose the best one.
Here's the critical observation: if we know the optimal camera placement for all subtrees, we can determine the optimal placement for the current node. This naturally leads to a recursive solution where we solve smaller subproblems first.
For each node, we need to track different states because the parent's decision depends on what happened with the children. If a child has a camera, it monitors its parent. If a child is unmonitored, the parent must either have a camera or be monitored by its other child.
The clever part is recognizing that we need exactly three states:
- State
a
: Having a camera here covers this node, its parent, and its children - State
b
: Being monitored by children means at least one child must have a camera - State
c
: Not being monitored by children means we're counting on the parent to monitor us
By calculating the minimum cameras needed for each state at every node and propagating these values up the tree, we can find the global minimum. The final answer is the minimum between placing a camera at the root (state a
) or having the root monitored by its children (state b
). We can't leave the root unmonitored (state c
) since it has no parent to monitor it.
Learn more about Tree, Depth-First Search, Dynamic Programming and Binary Tree patterns.
Solution Approach
The implementation uses dynamic programming with DFS to solve this problem. Let's walk through how the solution works:
Function Structure:
We define a recursive dfs(root)
function that returns three values for each node:
a
: Minimum cameras needed if we place a camera at this nodeb
: Minimum cameras needed if this node is monitored by its childrenc
: Minimum cameras needed if this node is not monitored by its children
Base Case:
When we reach a null node, we return [inf, 0, 0]
. The inf
for state a
indicates it's impossible to place a camera on a null node. The zeros for states b
and c
mean no cameras are needed for non-existent nodes.
Recursive Case: For each non-null node, we first recursively solve for the left and right subtrees:
la, lb, lc = dfs(root.left)
ra, rb, rc = dfs(root.right)
Then we calculate the three states for the current node:
-
State
a
(camera at current node):- Formula:
a = min(la, lb, lc) + min(ra, rb, rc) + 1
- If we place a camera here, both children are automatically monitored
- We can choose any state for the children (hence the
min
), plus 1 for the current camera
- Formula:
-
State
b
(monitored by children):- Formula:
b = min(la + rb, lb + ra, la + ra)
- At least one child must have a camera to monitor the parent
- We consider three scenarios: left has camera, right has camera, or both have cameras
- We pick the minimum among these options
- Formula:
-
State
c
(not monitored by children):- Formula:
c = lb + rb
- Neither child has a camera pointing at this node
- Both children must be monitored by their own children (state
b
)
- Formula:
Final Answer:
After computing the states for the root, we return min(a, b)
. We exclude state c
because the root has no parent to monitor it, so it must either have a camera or be monitored by its children.
The algorithm runs in O(n)
time since we visit each node once, and uses O(h)
space for the recursion stack, where h
is the height of the tree.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach. Consider this binary tree:
1 / \ 2 3 / 4
We'll compute the three states (a, b, c) for each node, working from the leaves up.
Step 1: Process leaf node 4
- Node 4 has no children, so
dfs(null)
returns[inf, 0, 0]
for both left and right - State a:
min(inf, 0, 0) + min(inf, 0, 0) + 1 = 0 + 0 + 1 = 1
(camera at node 4) - State b:
min(inf + 0, 0 + inf, inf + inf) = min(inf, inf, inf) = inf
(can't be monitored by non-existent children) - State c:
0 + 0 = 0
(no children to monitor) - Node 4 returns:
[1, inf, 0]
Step 2: Process leaf node 3
- Node 3 has no children, similar to node 4
- Node 3 returns:
[1, inf, 0]
Step 3: Process node 2
- Left child (node 4) returns
[1, inf, 0]
, right child (null) returns[inf, 0, 0]
- State a:
min(1, inf, 0) + min(inf, 0, 0) + 1 = 0 + 0 + 1 = 1
(camera at node 2) - State b:
min(1 + 0, inf + inf, 1 + inf) = min(1, inf, inf) = 1
(node 4 has camera, monitoring node 2) - State c:
inf + 0 = inf
(node 4 must be in state b, which is impossible) - Node 2 returns:
[1, 1, inf]
Step 4: Process root node 1
- Left child (node 2) returns
[1, 1, inf]
, right child (node 3) returns[1, inf, 0]
- State a:
min(1, 1, inf) + min(1, inf, 0) + 1 = 1 + 0 + 1 = 2
(camera at node 1, optimal states for children) - State b:
min(1 + inf, 1 + 1, 1 + 1) = min(inf, 2, 2) = 2
(either child has camera) - State c:
1 + inf = inf
(impossible since node 3 can't be in state b) - Node 1 returns:
[2, 2, inf]
Final Answer:
min(a, b) = min(2, 2) = 2
The optimal solution uses 2 cameras. One valid placement is:
- Camera at node 2 (monitors nodes 1, 2, and 4)
- Camera at node 3 (monitors node 3)
This example shows how the algorithm considers all possibilities at each node and propagates the optimal solutions upward, ultimately finding the minimum number of cameras needed.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9from math import inf
10
11class Solution:
12 def minCameraCover(self, root: Optional[TreeNode]) -> int:
13 """
14 Find minimum number of cameras needed to monitor all nodes in binary tree.
15 Each camera can monitor its parent, itself, and its immediate children.
16 """
17
18 def dfs(node: Optional[TreeNode]) -> tuple[float, int, int]:
19 """
20 Returns tuple of three values for each node:
21 - camera_here: min cameras if we place a camera at this node
22 - covered_no_camera: min cameras if this node is covered but has no camera
23 - not_covered: min cameras if this node is not covered
24
25 For null nodes, we return (inf, 0, 0) because:
26 - inf: cannot place camera on null node
27 - 0: null node is considered covered without needing cameras
28 - 0: null node doesn't need coverage
29 """
30
31 # Base case: null node
32 if node is None:
33 return inf, 0, 0
34
35 # Recursively solve for left and right subtrees
36 left_camera, left_covered, left_not_covered = dfs(node.left)
37 right_camera, right_covered, right_not_covered = dfs(node.right)
38
39 # Case 1: Place camera at current node
40 # Camera covers current node and both children
41 # Children can be in any state since they're covered by this camera
42 camera_here = min(left_camera, left_covered, left_not_covered) + \
43 min(right_camera, right_covered, right_not_covered) + 1
44
45 # Case 2: Current node is covered but has no camera
46 # At least one child must have a camera to cover current node
47 covered_no_camera = min(
48 left_camera + right_covered, # left child has camera
49 left_covered + right_camera, # right child has camera
50 left_camera + right_camera # both children have cameras
51 )
52
53 # Case 3: Current node is not covered
54 # Both children must be covered (but don't have cameras)
55 # This forces parent to have a camera
56 not_covered = left_covered + right_covered
57
58 return camera_here, covered_no_camera, not_covered
59
60 # Get results for root node
61 camera_at_root, root_covered, _ = dfs(root)
62
63 # Root must be covered, so return minimum of:
64 # - placing camera at root
65 # - root being covered by its children
66 return min(camera_at_root, root_covered)
67
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 public int minCameraCover(TreeNode root) {
18 // Get the minimum cameras needed for each state at root
19 int[] result = dfs(root);
20
21 // Root must be either covered with camera or monitored by children
22 // Cannot leave root unmonitored
23 return Math.min(result[0], result[1]);
24 }
25
26 /**
27 * DFS to calculate minimum cameras needed for each node state
28 * Returns array of 3 values:
29 * [0] - Minimum cameras if current node has a camera
30 * [1] - Minimum cameras if current node is monitored (covered but no camera)
31 * [2] - Minimum cameras if current node is not monitored
32 */
33 private int[] dfs(TreeNode root) {
34 // Base case: null node
35 if (root == null) {
36 // Camera at null: impossible (use large value)
37 // Monitored null: 0 cameras needed
38 // Unmonitored null: 0 cameras needed
39 return new int[] {1 << 29, 0, 0};
40 }
41
42 // Recursively get states for left and right children
43 int[] leftStates = dfs(root.left);
44 int[] rightStates = dfs(root.right);
45
46 // Case 1: Current node has a camera
47 // Children can be in any state (camera/monitored/unmonitored)
48 int withCamera = 1 +
49 Math.min(Math.min(leftStates[0], leftStates[1]), leftStates[2]) +
50 Math.min(Math.min(rightStates[0], rightStates[1]), rightStates[2]);
51
52 // Case 2: Current node is monitored (no camera, but covered)
53 // At least one child must have a camera to monitor this node
54 int monitored = Math.min(
55 Math.min(leftStates[0] + rightStates[1], leftStates[1] + rightStates[0]),
56 leftStates[0] + rightStates[0]
57 );
58
59 // Case 3: Current node is not monitored
60 // Both children must be monitored (cannot leave children unmonitored)
61 int unmonitored = leftStates[1] + rightStates[1];
62
63 return new int[] {withCamera, monitored, unmonitored};
64 }
65}
66
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12
13// State definition for each node
14struct NodeState {
15 int withCamera; // Minimum cameras needed if current node has a camera
16 int coveredNoCamera; // Minimum cameras needed if current node is covered but has no camera
17 int notCovered; // Minimum cameras needed if current node is not covered
18};
19
20class Solution {
21public:
22 int minCameraCover(TreeNode* root) {
23 // Get the state of the root node
24 auto [withCamera, coveredNoCamera, notCovered] = dfs(root);
25
26 // Root must be covered, so we take minimum of:
27 // - root has camera
28 // - root is covered without camera
29 return min(withCamera, coveredNoCamera);
30 }
31
32private:
33 NodeState dfs(TreeNode* root) {
34 // Base case: null node
35 if (!root) {
36 // null node doesn't need coverage
37 // withCamera: impossibly high cost (we can't place camera on null)
38 // coveredNoCamera: 0 (null is considered covered)
39 // notCovered: 0 (null doesn't need coverage)
40 return {1 << 29, 0, 0};
41 }
42
43 // Recursively get states for left and right children
44 auto [leftWithCamera, leftCoveredNoCamera, leftNotCovered] = dfs(root->left);
45 auto [rightWithCamera, rightCoveredNoCamera, rightNotCovered] = dfs(root->right);
46
47 // Case 1: Current node has a camera
48 // If current node has camera, both children are covered
49 // Children can be in any state
50 int withCamera = 1 +
51 min({leftWithCamera, leftCoveredNoCamera, leftNotCovered}) +
52 min({rightWithCamera, rightCoveredNoCamera, rightNotCovered});
53
54 // Case 2: Current node is covered but doesn't have a camera
55 // At least one child must have a camera to cover current node
56 int coveredNoCamera = min({
57 leftWithCamera + rightWithCamera, // Both children have cameras
58 leftWithCamera + rightCoveredNoCamera, // Left has camera, right is covered
59 leftCoveredNoCamera + rightWithCamera // Right has camera, left is covered
60 });
61
62 // Case 3: Current node is not covered
63 // Both children must be covered (but not necessarily have cameras)
64 // Parent will cover this node
65 int notCovered = leftCoveredNoCamera + rightCoveredNoCamera;
66
67 return {withCamera, coveredNoCamera, notCovered};
68 }
69};
70
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Calculates the minimum number of cameras needed to monitor all nodes in a binary tree.
17 * Each camera can monitor its parent, itself, and its immediate children.
18 *
19 * @param root - The root node of the binary tree
20 * @returns The minimum number of cameras needed
21 */
22function minCameraCover(root: TreeNode | null): number {
23 /**
24 * DFS helper function that returns three states for each node:
25 * [0] - Minimum cameras needed if current node has a camera
26 * [1] - Minimum cameras needed if current node is monitored (but doesn't have camera)
27 * [2] - Minimum cameras needed if current node is not monitored
28 *
29 * @param node - Current node being processed
30 * @returns Array of three numbers representing the three states
31 */
32 const dfs = (node: TreeNode | null): number[] => {
33 // Base case: null node doesn't need monitoring
34 // Return large number for "has camera" state since null can't have camera
35 if (!node) {
36 return [1 << 29, 0, 0];
37 }
38
39 // Recursively process left and right subtrees
40 const [leftWithCamera, leftMonitored, leftNotMonitored] = dfs(node.left);
41 const [rightWithCamera, rightMonitored, rightNotMonitored] = dfs(node.right);
42
43 // Case 1: Current node has a camera
44 // It monitors both children, so children can be in any state
45 const currentWithCamera = 1 +
46 Math.min(leftWithCamera, leftMonitored, leftNotMonitored) +
47 Math.min(rightWithCamera, rightMonitored, rightNotMonitored);
48
49 // Case 2: Current node is monitored (but doesn't have camera)
50 // At least one child must have a camera to monitor current node
51 const currentMonitored = Math.min(
52 leftWithCamera + rightWithCamera, // Both children have cameras
53 leftWithCamera + rightMonitored, // Left has camera, right is monitored
54 leftMonitored + rightWithCamera // Right has camera, left is monitored
55 );
56
57 // Case 3: Current node is not monitored
58 // Both children must be monitored (they can't be not monitored)
59 const currentNotMonitored = leftMonitored + rightMonitored;
60
61 return [currentWithCamera, currentMonitored, currentNotMonitored];
62 };
63
64 // Process the entire tree starting from root
65 const [rootWithCamera, rootMonitored, _] = dfs(root);
66
67 // Root must be either monitored or have a camera (can't be not monitored)
68 return Math.min(rootWithCamera, rootMonitored);
69}
70
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the function performs constant-time operations:
- Recursive calls to left and right children
- Computing values
a
,b
, andc
using simple arithmetic operations andmin()
comparisons
Since we visit all n
nodes exactly once and perform O(1)
work at each node, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity is dominated by the recursive call stack. In the worst case, when the binary tree is completely unbalanced (forming a linked list), the recursion depth can reach n
levels, requiring O(n)
space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n)
, but we consider the worst-case scenario for space complexity analysis.
Additionally, the function uses only a constant amount of extra space for variables (la
, lb
, lc
, ra
, rb
, rc
, a
, b
, c
) at each recursive level, which doesn't change the overall space complexity.
Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling the Root Node's Final State
Pitfall: A common mistake is returning min(a, b, c)
or just a
for the final answer, forgetting that the root node has no parent to monitor it.
Why it's wrong: State c
represents a node that is NOT monitored. Since the root has no parent, if we choose state c
, the root would remain unmonitored forever, violating the problem's requirement.
Solution: Always return min(a, b)
for the root, excluding state c
:
camera_at_root, root_covered, root_not_covered = dfs(root)
# Correct: Root must be monitored
return min(camera_at_root, root_covered)
# Wrong: Would leave root potentially unmonitored
# return min(camera_at_root, root_covered, root_not_covered)
2. Incorrect Base Case for Null Nodes
Pitfall: Returning incorrect values for null nodes, such as [0, 0, 0]
or [1, 0, 0]
.
Why it's wrong: The first value must be inf
because you cannot place a camera on a non-existent node. Using 0 or 1 would incorrectly suggest it's possible or cheap to place a camera there.
Solution: Always return [inf, 0, 0]
for null nodes:
if node is None: return inf, 0, 0 # Correct # return 0, 0, 0 # Wrong: suggests we can place a camera on null
3. Misunderstanding State b
Calculation
Pitfall: Calculating state b
as simply la + ra
or min(la, ra)
, forgetting that at least one child MUST have a camera to monitor the parent.
Why it's wrong: State b
means the node is monitored by its children. This requires at least one child to have a camera pointing upward.
Solution: Consider all valid combinations where at least one child has a camera:
# Correct: At least one child must have a camera
covered_no_camera = min(
left_camera + right_covered, # left has camera
left_covered + right_camera, # right has camera
left_camera + right_camera # both have cameras
)
# Wrong: Doesn't ensure a child has a camera
# covered_no_camera = left_covered + right_covered
4. Forgetting to Add 1 When Placing a Camera
Pitfall: When calculating state a
, forgetting to add 1 for the camera being placed at the current node.
Why it's wrong: Each camera costs 1, so we must account for it in our count.
Solution: Always add 1 when placing a camera:
# Correct
camera_here = min(la, lb, lc) + min(ra, rb, rc) + 1
# Wrong: Forgets to count the current camera
# camera_here = min(la, lb, lc) + min(ra, rb, rc)
5. Using Global Variables Incorrectly
Pitfall: Some might try to use a global counter or mutable default arguments to track cameras, leading to incorrect results when the function is called multiple times.
Solution: Use the return value approach with tuples to maintain state properly:
# Correct: Return states as tuple
def dfs(node):
# ... calculations ...
return camera_here, covered_no_camera, not_covered
# Wrong: Using global counter
# self.cameras = 0 # This persists between test cases!
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Donât Miss This!