Coding Interview Patterns Module 26 – Dynamic programming: advanced patterns
Dynamic programming on trees
The fewest cameras that see every hall of a tree-shaped museum: the problem, a brute force over sets of halls, and the signal that points to tree DP.
What you will learn
- Design three states per node for a covering problem and combine them from the leaves up
- Prove that the states give the fewest cameras, and compare them with the leaf-up greedy
- Compute a tree DP children first without recursion, for trees 100,000 levels deep
- Solve the party-invitation problem with two numbers per person and rebuild the guest list
- Extend the states to installation costs and to nodes with many children
Before you start
What you’ll learn · Before you start
What you will learn
- Design three states per node for a covering problem and combine them from the leaves up
- Prove that the states give the fewest cameras, and compare them with the leaf-up greedy
- Compute a tree DP children first without recursion, for trees 100,000 levels deep
- Solve the party-invitation problem with two numbers per person and rebuild the guest list
- Extend the states to installation costs and to nodes with many children
Before you start
On this page
Dynamic programming on a tree gives every node a few numbers that sum up the best plan for its part of the tree, and works them out from its children’s numbers, leaves first, so one pass solves the whole tree in O(n). It fits problems in which a choice at a node interacts only with the node’s parent and children: placing cameras, inviting people who must not meet their managers, counting colourings. The classic interview example is binary tree cameras, the fewest cameras that see every node when a camera sees its own node and every node one edge away. Trying sets of nodes takes exponential time; the tree DP takes one pass.
Cameras for a museum shaped like a tree
A museum’s halls form a binary tree: the entrance at the top, and each hall with doors to at most two halls below it. A camera in a hall sees that hall, the hall above it and the halls below it. Return the fewest cameras that see every hall. Limits: 1 ≤ n ≤ 10⁵ halls, and the museum may be a single corridor 10⁵ halls deep.
- Museum A,
["A", "B", "C", "D", "E", None, "F", None, None, None, None, "G"]in level order: 2, with cameras in B and F. - Museum B, a corridor of five halls: 2, in the second and the fourth hall.
- A museum of one hall: 1, its own camera.
Museum A, watched by two cameras
Text description of the diagram
The diagram shows museum A as a binary tree of seven halls. Each corridor is marked L or R for the side it leaves from.
- The entrance, A, is at the top. Below it are hall B on the left and hall C on the right.
- Under B: hall D on the left and hall E on the right.
- Under C: hall F on the right, and under F, hall G on the left.
Halls B and F hold the cameras and have a double border. The camera in B sees A above it and D and E below it; the camera in F sees C above it and G below it. The corridors along which a camera looks are drawn solid and thick; the corridor between A and C is dashed, because no camera looks along it, yet both of its ends are seen. One camera cannot see both D and G, so two is the fewest.
The brute force, and why it fails at this size
Try sets of halls, smallest sets first, and stop at the first set whose cameras see every hall. Checking one set takes O(n) steps, and a museum of n halls has 2ⁿ sets.
free/brute_force.py
"""The fewest cameras by brute force: try sets of halls, smallest sets first, and count the sets tried."""
import random
from itertools import combinations
from math import comb
from museum import build, halls_and_doors, random_museum
def fewest_cameras_brute(root):
"""The size of the smallest set of halls whose cameras see every hall, and how many sets were tried."""
order, doors = halls_and_doors(root)
tried = 0
for k in range(1, len(order) + 1):
for chosen in combinations(order, k):
tried += 1
seen = set(chosen)
for hall in chosen:
seen.update(doors[hall])
if len(seen) == len(order):
return k, tried
raise AssertionError("a camera in every hall sees every hall")
MUSEUMS = {
"museum A": ["A", "B", "C", "D", "E", None, "F", None, None, None, None, "G"],
"museum B, a corridor of five halls": ["A", "B", None, "C", None, "D", None, "E"],
"museum C, one hall": ["A"],
}
for label, names in MUSEUMS.items():
k, tried = fewest_cameras_brute(build(names))
print(f"{label}: {k} camera{'s' if k > 1 else ''}, {tried:,} set{'s' if tried > 1 else ''} of halls tried")
rng = random.Random(5)
for size in (10, 15, 20):
k, tried = fewest_cameras_brute(random_museum(rng, size))
print(f"a random museum of {size} halls: {k} cameras, {tried:,} sets tried")
# A corridor of n halls needs a camera in every third hall, so a smallest-first search first rejects every smaller set.
for n in (30, 60, 90):
need = -(-n // 3)
rejected = sum(comb(n, i) for i in range(need))
print(f"a corridor of {n} halls needs {need} cameras; sets rejected first: {rejected:.2e}") Output
museum A: 2 cameras, 17 sets of halls tried museum B, a corridor of five halls: 2 cameras, 8 sets of halls tried museum C, one hall: 1 camera, 1 set of halls tried a random museum of 10 halls: 3 cameras, 124 sets tried a random museum of 15 halls: 6 cameras, 7,319 sets tried a random museum of 20 halls: 8 cameras, 204,292 sets tried a corridor of 30 halls needs 10 cameras; sets rejected first: 2.30e+07 a corridor of 60 halls needs 20 cameras; sets rejected first: 3.58e+15 a corridor of 90 halls needs 30 cameras; sets rejected first: 6.02e+23
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
free/museum.py
"""Museums as binary trees of halls, built from a level-order list of hall names (None where a hall is missing)."""
from collections import deque
class Hall:
def __init__(self, name):
self.name = name
self.left = None
self.right = None
def build(names):
"""The museum of a level-order list: the two halls below each hall follow in turn, None where there is none."""
root = Hall(names[0])
waiting = deque([root])
i = 1
while i < len(names):
parent = waiting.popleft()
for side in ("left", "right"):
if i < len(names) and names[i] is not None:
child = Hall(names[i])
setattr(parent, side, child)
waiting.append(child)
i += 1
return root
def halls_and_doors(root):
"""Every hall from the entrance down, and for each hall the halls one door away (its parent and children)."""
order, doors, queue = [], {root: []}, deque([root])
while queue:
hall = queue.popleft()
order.append(hall)
for child in (hall.left, hall.right):
if child is not None:
doors[hall].append(child)
doors[child] = [hall]
queue.append(child)
return order, doors
def random_museum(rng, size):
"""A random museum of `size` halls named h0, h1, ...: each new hall hangs at a random free place."""
root = Hall("h0")
places = [(root, "left"), (root, "right")]
for k in range(1, size):
parent, side = places.pop(rng.randrange(len(places)))
child = Hall(f"h{k}")
setattr(parent, side, child)
places += [(child, "left"), (child, "right")]
return root Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
A random museum of 20 halls already took 204,292 sets. A corridor needs a camera in every third hall, so before a set of the right size can succeed, a smallest-first search rejects every smaller one: about 3.6 × 10¹⁵ sets for a corridor of 60 halls. At 10⁵ halls the number 2ⁿ has more than 30,000 digits.
What to notice
- A camera reaches one door away. Choices inside the part of the museum below a hall touch the rest only through that hall.
- So each part needs one fact about its top hall: it holds a camera, it is seen from below, or it waits for a camera in the hall above. Three numbers per hall, worked out from its children’s numbers, carry all that its parent needs.
- A greedy also exists. Working up from the leaves, it puts a camera above every hall that is still unseen, which is optimal while every camera costs the same.
Three numbers per hall, combined from the leaves up Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why three numbers are enough, proved by induction on parts
- A dry run on museum A
- The code in Python, JavaScript, Java, C++ and Go
- A corridor 100,000 halls deep, without recursion
- The greedy from the leaves up, and where it fails
- Why the greedy is optimal when cameras cost the same
- Installation costs, and the museum that breaks the greedy
- Edge cases and five common bugs
- Variants: a party without managers, halls with many doors
- Exercise: the fewest cameras for any museum
- The fewest cameras that see every hall
- A practice set with answers
- Check yourself
- Follow-up questions, answered
- Interview questions
- In the binary tree cameras problem, why does each node need three numbers rather than one or two?
- Halls of a tree-shaped museum now have installation costs from 1 to 100. A colleague keeps the leaf-up greedy but, at each unseen leaf, puts the camera in the cheaper of the leaf and its parent. Is the plan always cheapest? If not, show why and give a method that is, with its running time.
- Your recursive tree DP passes the sample tests, then a hidden test, a chain of 100,000 nodes, fails with RecursionError. Raising the recursion limit makes it pass on Python 3.14, but on a judge that runs Python 3.10 the process dies with no traceback. Explain both behaviours and fix the solution without changing its recurrence.
- An org chart has 100,000 employees, each with a cheer score that may be negative. Invite the set with the largest total so that nobody attends together with their direct manager. Give the DP, its cost, and how you return the guest list.
- Key takeaways
8 runnable examples (C++, Go, Java, JavaScript, Python, cameras.py, drills.py, greedy.py, museum.py) · 1 exercise with sample tests · 3 interview answers · about 26 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- A linear algorithm for the domination number of a tree (E. Cockayne, S. Goodman and S. Hedetniemi, Information Processing Letters 4(2)) (Elsevier)
- NIST Dictionary of Algorithms and Data Structures: postorder traversal (National Institute of Standards and Technology (NIST))
- NIST Dictionary of Algorithms and Data Structures: independent set (National Institute of Standards and Technology (NIST))
- sys.getrecursionlimit() and sys.setrecursionlimit() (The Python Standard Library) (Python Software Foundation)
- What's New in Python 3.11: inlined Python function calls (Python Software Foundation)
- itertools.combinations (The Python Standard Library) (Python Software Foundation)
- math.inf (The Python Standard Library) (Python Software Foundation)
- Java SE 25 API: Integer.MAX_VALUE (Oracle)
- std::min (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: min and max (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress