Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Planning DP  ›  Lesson

DP on Trees & the Night Depots

DP on Trees 34 minSolve each branch once, bottom-up, and hand the parent one answer per state
You're building a piece ofMeridian — the day planner
This piece — night_depots(): Chooses which depots stay open overnight when neighbouring depots can't both open.
Scenario At night Meridian keeps only some depots open. Two depots joined by a direct road share one night security patrol, so they can't both be open, and every depot has a different overnight demand. The night plan picks the depots that serve the most demand without breaking that rule.
Your task
Build night_depots(tree). Meridian's depots form a tree: each node is {"name": str, "demand": int, "children": [nodes]}. Choose depots to keep open overnight so that no depot is open together with its direct parent, maximising total demand. Return {"demand": total, "open": names}, with the open depots listed in pre-order (a depot before its children, children in list order). For ties, compute (take, skip) for every depot, then decide from the root down: a depot opens only if its parent is closed and its take is strictly greater than its skip. Example: a hub of demand 3 over two leaves Dock and Mill of demand 2 each → {"demand": 4, "open": ["Dock", "Mill"]}.

DP on Trees & the Night Depots

In short: a tree DP solves leaves first and works up to the root, because a node's answer only depends on its children, and no two children's branches overlap. One number per node often isn't enough, since the parent may care about something the child can't see from a single best score — so each node returns one answer per state the parent cares about (here, a pair: the best demand if this depot is open, and if it's closed). The parent combines its children's pairs and hands its own pair further up.

Six depots as a tree, each badged t=take s=skip after a post-order sweep: leaves 1/0, North 6/2, South 5/1, Hub 10/11, with Hub's closed branch (11) circled as the winner

Meridian's depots form a tree. The central hub supplies district depots, which supply local ones, and every depot has exactly one parent it gets its stock from. At night only some depots stay open. Two depots joined by a direct road share one night security patrol, so a depot and its parent can't both be open. Every depot has a different overnight demand. Which depots should open?

This is the grid problem again in a different shape. On a grid, a cell's answer came from the cells above and to the left. On a tree, a depot's answer comes from its children, and each child's answer is about its own branch, which no other branch overlaps. Solve the leaves first and work up to the root, and every branch is solved exactly once.

One number isn't enough

Try "the best demand in this depot's branch". Then the hub asks its child North: what's the best you can do? North says 6, but that 6 comes from North being open, and an open hub isn't allowed to use it. The child has to answer two questions, one for each thing the parent might do:

  • take: the best demand in my branch if I'm open. Then all my children must be closed: demand + Σ child.skip.
  • skip: the best demand in my branch if I'm closed. Then each child does whatever is better for it: Σ max(child.take, child.skip).

The answer for the whole city is max(root.take, root.skip). Here it is on six depots, with each depot badged t=take s=skip as the post-order sweep reaches it:

watch it move · tree

Post-order fills every child's pair before its parent's, and the parent combines them. Demands are Hub 7, North 6, South 5, and 1 at each leaf.

Dock is a leaf. Open, it serves its demand of 1. Closed, it serves 0. Binary tree, 6 nodes, height 3. At node Dock.

1 / 8

The pair is what makes this work. Each child hands up one answer per state the parent cares about, and the parent never has to look further down than its own children.

python

The code handles any number of children, since the loop doesn't care. The pictures in this lesson use at most two per depot only so they fit on screen.

Predict a small one before you run anything:

What does this print?
def solve(node):
    take, skip = node["demand"], 0
    for child in node["children"]:
        t, s = solve(child)
        take += s
        skip += max(t, s)
    return take, skip

chain = {"demand": 4, "children": [{"demand": 5, "children": [{"demand": 4, "children": []}]}]}
print(max(solve(chain)))

And one where you read the pair itself:

What does this print?
def solve(node):
    take, skip = node["demand"], 0
    for child in node["children"]:
        t, s = solve(child)
        take += s
        skip += max(t, s)
    return take, skip

leaf = lambda d: {"demand": d, "children": []}
star = {"demand": 5, "children": [leaf(2), leaf(2), leaf(2)]}
print(solve(star))

Now it's your turn to break it. Someone "simplified" the take line:

broken — fix it

The best legal plan opens the two ends for 8, but this prints 13, which means all three depots are open, including two linked pairs. Fix the line that lets an open depot count an open child.

Drawing your own sweep

Build the tree from dictionaries with left and right, so the drawing can show it, and badge each depot the moment its pair is known. Change a demand, or hang a new depot somewhere, and run it again:

your code, drawn · tree

Your post-order sweep. A depot's badge appears only after both of its children have theirs.

Choosing, not just scoring

The pairs give the best total, but the night plan needs names. Recover them top-down, the reverse of how the pairs were computed:

  1. The root has no parent, so it's free. It opens if take > skip.
  2. A depot whose parent opened must stay closed.
  3. A depot whose parent is closed is free, and opens if take > skip.

A tie (take == skip) needs a rule, or two correct programs will print different plans. The night depots rule is ties stay closed, because a closed depot leaves its children free and costs nothing to staff.

Recursion depth is the one thing that can bite. A long chain of 5,000 depots is 5,000 nested calls, and Python stops at about 1,000. The fix is to number the depots so every parent comes before its children (a BFS order does this), then sweep the numbers backwards. Every child is then finished before its parent, with no recursion at all:

step through it
1parent = [-1, 0, 0, 1, 1, 2] # depot 0 is the hub
2demand = [7, 6, 5, 1, 1, 1]
3take = demand[:]
4skip = [0] * 6
5for v in range(5, 0, -1): # children have bigger numbers than parents
6 p = parent[v]
7 take[p] += skip[v]
8 skip[p] += max(take[v], skip[v])
9print(max(take[0], skip[0]))

The same shape, flipped

The pair pattern handles any constraint between a node and its parent. Here's one where the rule points the other way, written as an interviewer would pose it:

Interview-style problem: fewest patrols. A patrol stationed at a depot watches every road touching that depot. Given the depot tree, return the fewest depots that need a patrol so that every road is watched from at least one end.

Constraints: up to 1,000 depots; the tree is given as nested {"name", "demand", "children"} nodes, and demand is ignored.

Examples: a chain of four depots → 2 (the second and the third). A centre with four spokes → 1. A single depot with no roads → 0.

Before, an open depot forced its children closed. Now a depot without a patrol forces every child to have one. The pair becomes (fewest with a patrol here, fewest without), and the combine lines swap roles. That's the build exercise.

❓ Cross-question: "What if the number of depots I can open is limited?" Then a pair isn't enough, and each depot returns a whole list: the best demand for every count of depots opened in its branch. Merging a child into its parent is then a knapsack step that splits the count between them. A knapsack inside a tree DP sounds exotic, but it's what the boss exercise asks for.

Why not just try every plan?

Each depot is open or closed, so there are 2ⁿ plans to check. The DP does a constant amount of work per depot. If checking one plan takes a microsecond:

depots n trying every plan
20 about 1 second
30 about 18 minutes
37 about 38 hours
40 about 13 days

Every extra depot doubles it. The tree DP handles 37 depots in 37 steps, and a million depots in about a second. A small city already has more than 37 depots.

Each depot does a constant amount of work — combine its children's pairs once — and every depot is visited exactly once in the post-order sweep, so the whole DP is O(n) time. The state itself (one pair per node) costs O(n) space to store, and the recursion that computes it holds one stack frame per depot on the current root-to-leaf path, so the call stack also uses O(n) in the worst case — a long chain — which is exactly why the lesson's "sweep the numbers backwards" trick trades that stack for a plain array.

Tree DP State per node Time
branch totals, deepest chain one number O(n)
night depots, fewest patrols a pair (open / closed) O(n)
open at most B depots a list of B + 1 numbers O(n · B) with care
best headquarters (rerooting) two sweeps, one number each O(n)

Idioms & real-world patterns

⚡ Advanced: rerooting

"Which depot should be headquarters, the one closest in total to every other?" Doing a tree DP from every depot costs O(n²). Rerooting does it in two sweeps. The first sweep, up from the leaves, finds the root's total and each branch's size. Then moving HQ across one road changes the total by a formula: the depots in the child's branch all get one road closer and everyone else gets one road further.

python

The capstone exercise does it with real road lengths, where the change is km × (n − 2 · size).

The input is plain JSON

A tree arrives as nested dictionaries with a children list, which is what an API or a config file would give you. Nothing needs a Node class, and the grader compares plain values.


Quick practice

What does this print?
def solve(node):
    take, skip = node["demand"], 0
    for child in node["children"]:
        t, s = solve(child)
        take += s
        skip += max(t, s)
    return take, skip

def D(name, demand, *children):
    return {"name": name, "demand": demand, "children": list(children)}

chain = D("A", 2, D("B", 9, D("C", 2, D("D", 9))))
print(max(solve(chain)))
broken — fix it

The best legal plan opens B and D for 9 + 9 = 18, but this prints 9 because the `take += s` line is missing, so an open depot never counts what its closed children were worth. Add the line back.

🎯 Your turn

Write night_depots(tree). Each node is {"name", "demand", "children"}. Open depots so that no depot is open together with its direct parent, maximising total demand. Return {"demand": total, "open": names_in_pre_order}.

  • a hub (3) over Dock (2) and Mill (2) → {"demand": 4, "open": ["Dock", "Mill"]}

Ties: decide from the root down. A depot opens only if its parent is closed and its take is strictly greater than its skip.

Then press ▶ Run, tap the Live App try chips, and hit ✓ Check. Green means the night plan is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upDemand under each depot

Every depot reports the overnight demand of its whole branch: itself plus every depot below it. Write branch_totals(tree) returning a dict from depot name to that total. A node is {"name", "demand", "children"}. For a hub with demand 4 over two leaves of demand 1 and 2, the result is {"Hub": 7, …}.

tree-dppostorder
DrillThe longest feeder line

Each non-root depot stores km, the length of the road to its parent (the root's km is 0). Write deepest_chain(tree) returning the greatest road distance from the root down to any depot. A node is {"name", "km", "children"}.

tree-dptree-recursion
BuildFewest patrols

A patrol stationed at a depot watches every road touching it. Write min_patrols(tree) returning the fewest depots that need a patrol so that every road (parent–child link) is watched from at least one end. A node is {"name", "demand", "children"} (demand is ignored).

Constraints: up to 1,000 depots.

tree-dpdp
BossOpen on a budget

Meridian can afford to open at most budget depots tonight, and a depot can only open if its parent is open (supplies flow down from the root). Write budget_depots(tree, budget) returning the highest total demand of such a set. With budget 0 nothing opens and the answer is 0. A node is {"name", "demand", "children"}.

Constraints: up to 60 depots, budget ≤ 60.

tree-dpknapsack
CapstoneWhere to put headquarters

Meridian will move its headquarters to whichever depot has the smallest total road distance to every other depot. Each non-root node stores km to its parent. Write best_hq(tree) returning {"hq": name, "km": total}. On a tie, pick the depot that comes first in pre-order (a node before its children, children in list order). Aim for O(n): one sweep up, one sweep down.

Constraints: up to 2,000 depots.

rerootingtree-dptrees
ExplainWhy two numbers per depot?

The night-depot DP returns two numbers per depot, take and skip, instead of just "the best for this branch". Explain why one number is not enough, why the children must be solved before the parent, and why opening the highest-demand depots first can lose.

night_depots.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
night_depots(tree) → dictThe largest overnight demand with no two linked depots both open, and which depots open, in pre-order.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.