Placing Cameras in a Binary Tree — A Complete Guide
Master LeetCode 968: Binary Tree Cameras with a clear bottom-up strategy. Learn the state-based approach, interview-ready code, and step-by-step walkthroughs that ensure you understand both intuition and implementation.
LeetCode 968: Binary Tree Cameras
- Each camera covers its parent, itself, and immediate children.
- Goal: cover every node with the fewest cameras possible.
Core Challenges
When solving this problem, two difficulties stand out:
-
Optimal placement strategy
Cameras are most effective above leaves (at their parents), since one camera can cover multiple leaves and itself. -
Information flow upward
To decide whether a parent needs a camera, each child must report its state. Without a systematic state system, the logic becomes messy.
The insight: model this as a bottom-up state machine.
State Representation
Every node returns one of three states:
UNCOVERED (0)
: This node is not covered by any camera.HAS_CAMERA (1)
: We place a camera at this node.COVERED (2)
: This node is covered by one of its children’s cameras.
Why only these three?
Because they represent the minimal complete set of information a parent needs:
- “My child is UNCOVERED → I must act.”
- “My child has a camera → I’m already safe.”
- “My child is COVERED → I might still need coverage.”
Interview-Friendly Implementation (Concise)
/**
* States:
* 0 = UNCOVERED
* 1 = HAS_CAMERA
* 2 = COVERED
*/
var minCameraCover = function(root) {
let cameras = 0;
function dfs(node) {
if (!node) return 2; // null nodes are COVERED by default
const left = dfs(node.left);
const right = dfs(node.right);
if (left === 0 || right === 0) {
cameras++;
return 1; // place camera here
}
if (left === 1 || right === 1) {
return 2; // covered by child’s camera
}
return 0; // uncovered
}
if (dfs(root) === 0) cameras++; // root fix-up
return cameras;
};
Why this works in interviews:
- Short, elegant, and correct.
- Shows understanding of recursion and state machines.
- Easy to explain line-by-line.
Educational Implementation (Verbose & Clear)
const State = {
UNCOVERED: 0,
HAS_CAMERA: 1,
COVERED: 2,
};
function minCameraCover(root) {
let cameras = 0;
function dfs(node) {
if (!node) return State.COVERED;
const left = dfs(node.left);
const right = dfs(node.right);
if (left === State.UNCOVERED || right === State.UNCOVERED) {
cameras++;
return State.HAS_CAMERA;
}
if (left === State.HAS_CAMERA || right === State.HAS_CAMERA) {
return State.COVERED;
}
return State.UNCOVERED;
}
if (dfs(root) === State.UNCOVERED) {
cameras++;
}
return cameras;
}
Step-by-Step Examples
We’ll trace each rule with small tree diagrams. Notation:
- U = UNCOVERED (0)
- C = HAS_CAMERA (1)
- V = COVERED (2)
1. Leaf Node → Uncovered
L
/ \
null null
L
'snull
children returnV
.L
sees(V, V)
→ returnsU
.
B) “Any child UNCOVERED → place camera here” (Rule 1 / the cameras++; return 1)
Parent with an uncovered leaf
P
/
L (leaf)
L
(leaf) returnsU
.P
seesU
from a child → places a camera, returnsC
.
C) “Any child HAS_CAMERA → current is COVERED” (Rule 2 / the return 2)
Three-node chain
G
|
P
|
L (leaf)
L
(leaf) returnsU
.P
seesU
from childL
→ places camera, returnsC
.G
seesC
from childP
→ returnsV
.
D) Root fix-up — root ends UNCOVERED even when both subtrees are COVERED
Non-trivial example
A (root)
/ \
B E
/ \
C F
/ \
D G
Post-order trace:
dfs(D)
&dfs(G)
(leaves) → returnU
.dfs(C)
&dfs(F)
seeU
→ place cameras, returnC
.dfs(B)
&dfs(E)
seeC
→ returnV
.dfs(A)
sees(V, V)
→ returnsU
.
Complexity
- Time: O(n) — each node visited once
- Space: O(h) — recursion stack (tree height)
key Takeaways
- Use 3 states: uncovered, has camera, covered. Nothing more is needed.
- Treat null as covered → simplifies leaf logic.
- Place cameras only when a child is uncovered.
- Always apply the root fix-up to handle edge cases.
Interview tip: If asked “Why three states?” → answer that they form the minimal information set for parents to make optimal decisions.