Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 7 of 35 minutes free
Free: Cameras for a museum shaped like a tree · The brute force, and why it fails at this size · What to notice. With a pass: the other 8 sections and 3 interview answers, about 28 minutes.

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: seven halls as a binary tree; cameras in B and F see all seven, so two cameras are enough.A (entrance)CB: cameraF: cameraEDGRLRRLL

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.

Sets of halls tried smallest first, and the sets a corridor rejects first

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

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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.