questions B (backspace - burst-balloons)

This commit is contained in:
2025-05-24 22:06:49 +01:00
parent f757e28b24
commit 2123791ec3
67 changed files with 13945 additions and 0 deletions

View File

@@ -0,0 +1,236 @@
title: Binary Tree Cameras
slug: binary-tree-cameras
difficulty: hard
leetcode_id: 968
leetcode_url: https://leetcode.com/problems/binary-tree-cameras/
categories:
- trees
- dynamic-programming
patterns:
- dfs
- greedy
description: |
You are given the `root` of a binary tree. We install cameras on the tree nodes where each camera at a node can monitor its parent, itself, and its immediate children.
Return *the minimum number of cameras needed to monitor all nodes of the tree*.
constraints: |
- The number of nodes in the tree is in the range `[1, 1000]`
- `Node.val == 0`
examples:
- input: "root = [0,0,null,0,0]"
output: "1"
explanation: "One camera is enough to monitor all nodes if placed as shown. Place a camera on the node at depth 1 (the left child of root), and it monitors its parent (root), itself, and both its children."
- input: "root = [0,0,null,0,null,0,null,null,0]"
output: "2"
explanation: "At least two cameras are needed to monitor all nodes of the tree. The tree forms a longer chain, so strategically placing cameras at every third level minimises the count."
explanation:
intuition: |
Imagine you're a security manager placing cameras in a building shaped like an upside-down tree. Each camera can see the room it's in, the room directly above (parent), and rooms directly below (children). Your goal is to use the **fewest cameras** while ensuring every room is monitored.
The key insight is to think **from the leaves upward**. Leaves are the most "vulnerable" nodes — they have no children to place cameras on that could cover them. If we place a camera on a leaf, we're wasting its monitoring power (it has no children to watch). Instead, it's more efficient to place cameras on the **parents of leaves**, which can monitor multiple nodes at once.
This leads to a greedy strategy: process the tree bottom-up using DFS. At each node, we decide its state based on what its children need. We classify each node into one of three states:
- **State 0 (Needs coverage)**: This node has no camera and isn't covered by a child's camera
- **State 1 (Has camera)**: This node has a camera installed
- **State 2 (Covered)**: This node is covered by a child's camera (doesn't need its own)
The greedy choice is: only place a camera when absolutely necessary — specifically, when a child needs coverage.
approach: |
We solve this using a **Post-order DFS with Greedy State Tracking**:
**Step 1: Define node states**
- `0`: Node needs to be covered (uncovered leaf or uncovered node)
- `1`: Node has a camera
- `2`: Node is covered (by a child's camera)
 
**Step 2: Perform post-order DFS**
- Process children first (left, then right), then the current node
- This ensures we make decisions bottom-up
- `null` nodes return state `2` (covered) — they don't need monitoring
 
**Step 3: Determine each node's state**
- If **any child needs coverage** (state `0`): place a camera here → return state `1`
- If **any child has a camera** (state `1`): this node is covered → return state `2`
- Otherwise: this node needs coverage → return state `0`
 
**Step 4: Handle the root**
- After DFS completes, if root needs coverage (state `0`), add one more camera
- The root has no parent to cover it, so we must handle this edge case
 
**Why this works**: By delaying camera placement until a child explicitly needs coverage, we ensure cameras are placed as "high" as possible in the tree, maximising their coverage. A camera at a parent covers more nodes than a camera at a leaf.
common_pitfalls:
- title: Placing Cameras on Leaves
description: |
A natural first instinct might be to place cameras on leaf nodes to ensure they're covered. However, this is wasteful:
- A leaf camera only covers itself and its parent (no children)
- A camera on the leaf's parent covers the leaf, the parent, and potentially the parent's parent
For example, in a chain of 3 nodes `A → B → C` (where C is a leaf):
- Placing camera on C: covers C and B (need another for A) = 2 cameras
- Placing camera on B: covers A, B, and C = 1 camera
wrong_approach: "Place cameras on all leaves"
correct_approach: "Place cameras on parents of uncovered nodes"
- title: Forgetting the Root Edge Case
description: |
After the DFS completes, the root might return state `0` (needs coverage). This happens when the root's children are both covered but neither has a camera directly.
For example, a tree with just a root node: DFS returns state `0` for the root (it's a leaf that needs coverage), but there's no parent to cover it. We must add a camera.
Always check: if `dfs(root) == 0`, increment the camera count.
wrong_approach: "Assume DFS handles all nodes"
correct_approach: "Check root state after DFS and add camera if needed"
- title: Wrong State Transitions
description: |
The order of checking states matters:
1. First check if any child needs coverage (state `0`) — must place camera
2. Then check if any child has camera (state `1`) — we're covered
3. Default: we need coverage (state `0`)
If you check in the wrong order, you might skip placing a necessary camera or place unnecessary ones.
wrong_approach: "Check states in arbitrary order"
correct_approach: "Prioritise: uncovered children first, then covered check, then default"
key_takeaways:
- "**Bottom-up greedy**: Process leaves first and make decisions that propagate upward — cameras placed higher cover more nodes"
- "**State machine on trees**: Using discrete states (`0`, `1`, `2`) for each node simplifies complex decision logic into clear transitions"
- "**Post-order DFS pattern**: When a node's decision depends on its children's states, process children first (post-order)"
- "**Similar problems**: This greedy tree coverage pattern appears in problems like House Robber III, distributing coins in a tree, and tree colouring problems"
time_complexity: "O(n). We visit each node exactly once during the post-order DFS traversal."
space_complexity: "O(h) where h is the height of the tree. The recursion stack can grow up to the tree's height. In the worst case (skewed tree), this is O(n); for a balanced tree, it's O(log n)."
solutions:
- approach_name: Post-order DFS with Greedy States
is_optimal: true
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def min_camera_cover(root: TreeNode) -> int:
# State meanings:
# 0 = node needs to be covered
# 1 = node has a camera
# 2 = node is covered (by child's camera)
cameras = 0
def dfs(node: TreeNode) -> int:
nonlocal cameras
# Null nodes are considered "covered" — they don't need monitoring
if not node:
return 2
# Post-order: process children first
left_state = dfs(node.left)
right_state = dfs(node.right)
# If any child needs coverage, we MUST place a camera here
if left_state == 0 or right_state == 0:
cameras += 1
return 1 # This node now has a camera
# If any child has a camera, this node is covered
if left_state == 1 or right_state == 1:
return 2 # Covered by child's camera
# Both children are covered but no camera nearby
# This node needs to be covered by its parent
return 0
# Run DFS and handle root edge case
root_state = dfs(root)
# If root still needs coverage, add a camera
# (root has no parent to cover it)
if root_state == 0:
cameras += 1
return cameras
explanation: |
**Time Complexity:** O(n) — Each node is visited exactly once.
**Space Complexity:** O(h) — Recursion stack depth equals tree height.
We use post-order DFS to process children before parents. Each node returns a state indicating whether it needs coverage, has a camera, or is already covered. The greedy choice to place cameras only when children need coverage ensures minimal camera usage. The root check handles the edge case where the root itself needs coverage.
- approach_name: Dynamic Programming with Three States
is_optimal: false
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def min_camera_cover(root: TreeNode) -> int:
# For each node, compute minimum cameras needed for three scenarios:
# dp[0] = cameras needed if this node is NOT covered and has no camera
# dp[1] = cameras needed if this node HAS a camera
# dp[2] = cameras needed if this node is covered (by child) but no camera here
def dfs(node: TreeNode) -> tuple[int, int, int]:
if not node:
# Null: doesn't need camera, considered covered
# State 0 returns infinity (not valid for real nodes)
# States 1 and 2 return 0 (no camera needed)
return float('inf'), 0, 0
left = dfs(node.left)
right = dfs(node.right)
# dp[0]: Node uncovered, no camera
# Children must be covered (state 2) — they can't have cameras
# pointing at us since we're not covered
dp0 = left[2] + right[2]
# dp[1]: Node has camera (covers itself, parent, and children)
# Children can be in any valid state — take minimum
dp1 = 1 + min(left) + min(right)
# dp[2]: Node covered by child's camera, no camera here
# At least one child must have a camera (state 1)
# The other child can be covered or have camera
dp2 = min(
left[1] + min(right[1], right[2]), # Left has camera
right[1] + min(left[1], left[2]) # Right has camera
)
return dp0, dp1, dp2
result = dfs(root)
# Root must be covered: either has camera (state 1) or covered (state 2)
return min(result[1], result[2])
explanation: |
**Time Complexity:** O(n) — Each node visited once.
**Space Complexity:** O(h) — Recursion stack.
This DP approach explicitly tracks all three states for each node. While it achieves the same optimal result, it's more complex than the greedy solution. The greedy approach is preferred for its simplicity and clearer logic. This solution is included to show how the problem can be framed as classical tree DP.