Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Pick a subset of tree nodes with the largest total weight such that no two chosen nodes share an edge. The 'choose or skip' decision at each node depends only on its children, so a post-order DFS with two states per node solves it: dp[node][1] = best when you take node (children must be skipped), dp[node][0] = best when you skip node (each child takes its better option). The answer is max(dp[root][0], dp[root][1]). O(n) time.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can define per-node states that capture the adjacency constraint (taken forces children skipped) and combine subtree results in a single post-order pass — recognizing this is House Robber lifted onto a tree.
Where candidates slip
Using only one state per node (losing the information of whether the node was taken), returning dp[root][1] instead of the max of both states, or recursing without a parent guard and looping over the edge back to the parent.
How to stand out
Name it as House Robber III generalized, state the two recurrences crisply, justify post-order, and note that maximum independent set is NP-hard on general graphs but linear on trees because of the no-cycle structure.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachExplore the full treeCollapse the tree
How to read it:Each node returns (skip, take). Taking a node adds each child's skip; skipping adds each child's max.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Enumerate Every Independent Subset
1Hint 1Explore hintHide hint
Every node is either in the set or out. Try all 2^n assignments and keep the heaviest that is independent.
2Hint 2Explore hintHide hint
Check independence by verifying no edge has both endpoints chosen, then sum the weights.
3Hint 3Explore hintHide hint
Exponential — but it reveals the real structure: a node's validity depends only on its neighbors, so the decisions are local.
A · Core idea
The definitional approach is to try every subset.
Enumerate. Each node is independently in or out, so there are 2^n candidate subsets. For each, confirm no edge connects two chosen nodes and total the weights; keep the best valid total.
Why it is correct but useless at scale. It literally searches the entire solution space, so it cannot miss the optimum — but 2^n explodes past a couple dozen nodes.
The observation that rescues it. Validity is local: a chosen node only forbids its direct neighbors. On a tree, a node's only neighbors are its parent and children, and the subtrees hanging off different children share no edges. So the choice inside one child's subtree does not constrain another child's subtree — they interact solely through whether the parent is taken. That independence means we can solve each subtree once and combine, replacing the exponential enumeration with a single pass.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
path 0-1-2, weights = [4, 5, 4]{}
weight 0
valid8C · Code with teaching comments
Reference implementation previewimport java.util.*; class BruteForceMIS { int maxWeight(int n, int[][] edges, int[] w) { int best = 0;Explore the full codeHide the full code
D · Why it works and complexityO(2^n) worst case timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The leap from exponential to linear is the realization that the no-adjacency constraint is purely local on a tree: fixing one node's in/out status fully decouples its children's subtrees. Capture that one bit of context as a DP state and the search collapses.
ComplexityO(2^n) worst case time, O(n) space — There are 2^n subsets; validating and summing each is O(n), giving O(n·2^n). Hopeless beyond ~25 nodes, but it defines what 'optimal' means.
E · Edge cases and pitfalls3Treating it as feasible for large n — 2^n is exponential.Explore edge casesHide edge cases
Pitfalls
- Treating it as feasible for large n — 2^n is exponential.
- Forgetting the empty set (weight 0) as a valid baseline for all-negative weights.
- Re-checking the same subtree decisions across masks — the redundancy the DP removes.
Approach 2 of 2 · Optimal
Tree DP — Two States per Node (Optimal)
1Hint 1Explore hintHide hint
Define two values per node: dp[node][1] = best total in node's subtree WHEN node is taken; dp[node][0] = best WHEN node is skipped.
2Hint 2Explore hintHide hint
If you take the node, no child may be taken, so add each child's dp[child][0]. If you skip the node, each child is free, so add max(dp[child][0], dp[child][1]).
3Hint 3Explore hintHide hint
Post-order: compute both children's states first, then the parent's. The answer is max(dp[root][0], dp[root][1]).
A · Core idea
Encode the only thing a child needs to know about its parent's decision as a per-node state, and the subtrees decouple.
The two states. For each node define:
dp[node][1]— the maximum total weight innode's subtree given that `node` is taken.dp[node][0]— the maximum total weight innode's subtree given that `node` is skipped.
The recurrences. Process children first (post-order), then combine:
- If
nodeis taken, none of its children may be taken (they are adjacent to it). Sodp[node][1] = w[node] + Σ dp[child][0]over all children. - If
nodeis skipped, each child may independently do whatever is best for it. Sodp[node][0] = Σ max(dp[child][0], dp[child][1]).
Why the children are independent. Two children of the same node live in disjoint subtrees joined only through that node. Once the parent's taken/skipped status is fixed, no constraint links one child's subtree to another's, so we can optimize each child separately and add the results. This is precisely why a single bit of state per node is enough.
Why post-order. Each recurrence reads the children's dp values, so every child must be finished before its parent — left/right (or all children) first, then the node.
The answer. At the root there is no parent to constrain it, so either state is allowed: max(dp[root][0], dp[root][1]). Returning only dp[root][1] wrongly forces the root into the set.
Negative weights for free. Because dp[node][0] sums max(skip, take) and a taken negative node would only lower the total, the max naturally declines to take nodes that hurt — no special case needed.
This is House Robber on a tree. House Robber III is exactly this with weights on a binary tree; the line version (plain House Robber) is the same recurrence on a path. Recognizing the family is the fastest way to the right code.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
path 0-1-2 rooted at 0, weights = [4, 5, 4]Interactive diagram
States on the path 0-1-2
node 2 (leaf)Leaf 2 returns (0,4); node 1 returns (4,5); root 0 returns (5,8). Answer 8 = take ends 0 and 2.
8C · Code with teaching comments
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The whole trick is choosing a state that makes subtrees independent. 'Taken vs skipped' is the minimal context a parent must hand down, because adjacency only reaches one level. Once decisions decouple, an NP-hard general-graph problem becomes a linear post-order scan — a recurring lesson in tree DP.
ComplexityO(n) time, O(n) space — A single post-order DFS visits each node once and does O(deg) work combining its children; summed over the tree that is O(n) total (each edge handled once). The recursion stack and dp arrays are O(n) space.
E · Edge cases and pitfalls7single node weight 5 — Leaf: take=5, skip=0 → max(0,5)=5.Explore edge casesHide edge cases
Edge cases handled
single node weight 5Leaf: take=5, skip=0 → max(0,5)=5.all weights negativeEvery take is dominated by skip (0), so the answer is 0 — the empty set, which is valid.star graph (center + many leaves)Either take the center (skip all leaves) or skip the center (take every positive leaf); the DP compares both.
Pitfalls
- Using one state per node — you lose whether the node was taken, breaking the parent's recurrence.
- Returning dp[root][1] instead of max(dp[root][0], dp[root][1]).
- Recursing without a parent check — infinite loop over the parent edge on an undirected adjacency list.
- Deep recursion on a chain of 10⁵ nodes — use an iterative post-order to avoid stack overflow.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.*; class TreeMIS { private List<List<Integer>> adj; private long[] w;Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1One state per node
A child needs to know its own best total in BOTH cases — when the parent is taken (child forced to skip) and when the parent is skipped (child free). Collapsing to a single value loses the case the parent needs.
Explore the code comparisonHide the code comparison
One state per node
A child needs to know its own best total in BOTH cases — when the parent is taken (child forced to skip) and when the parent is skipped (child free). Collapsing to a single value loses the case the parent needs.
Don't write this
def dfs(node, parent):
best = w[node]
for c in children: best += dfs(c, node) # single value, no skip case
return bestWrite this instead
def dfs(node, parent):
take, skip = w[node], 0
for c in children:
ct, cs = dfs(c, node) # (take, skip) for the child
take += cs
skip += max(ct, cs)
return take, skip2Returning only the taken state
The root has no parent constraining it, so skipping the root can be better (e.g. a heavy node deep in the tree). Return the max of both root states.
Explore the code comparisonHide the code comparison
Returning only the taken state
The root has no parent constraining it, so skipping the root can be better (e.g. a heavy node deep in the tree). Return the max of both root states.
Don't write this
return dp[root][1]; // forces the root into the setWrite this instead
return Math.max(dp[root][0], dp[root][1]);3No parent guard on an undirected tree
Adjacency lists store each edge in both directions. Without skipping the parent, the DFS revisits the parent as a 'child' and recurses forever.
Explore the code comparisonHide the code comparison
No parent guard on an undirected tree
Adjacency lists store each edge in both directions. Without skipping the parent, the DFS revisits the parent as a 'child' and recurses forever.
Don't write this
for child in adj[node]:
dfs(child, node) # also recurses into the parentWrite this instead
for child in adj[node]:
if child == parent:
continue
dfs(child, node)Revise
Carry the pattern into the interview
Pattern to remember
Post-order tree DP with two states per node: taken vs not taken
Code pattern to memorize
dfs(node, parent):
take = weight[node]
skip = 0
for child != parent:
dfs(child, node)
take += dp[child][0] # child must be skipped
skip += max(dp[child][0], dp[child][1]) # child free to choose
dp[node][1] = take
dp[node][0] = skipRules to remember
- 1Two states per node: taken forces every child to the skipped state; skipped lets each child take its own max.
- 2Combine children independently — subtrees do not interact except through the parent's choice.
- 3Use post-order: a node's dp needs both children's dp values first.
- 4The answer is max(dp[root][0], dp[root][1]), not just dp[root][1].
- 5On a general (unrooted) tree, root it anywhere and pass the parent to avoid walking back up an edge.
Use this pattern when
- Optimize over subsets of tree nodes with an adjacency (no-two-adjacent) constraint
- House-Robber-style problems generalized from a line to a tree
- Any 'select/skip with a local conflict' optimization on a tree
- Counting or weighting where each node's choice only constrains its neighbors
Choose another pattern when
- Graph has cycles (not a tree) → maximum independent set is NP-hard in general; tree DP relies on no cycles
- Constraint spans non-adjacent nodes (e.g. distance ≤ 2) → states must encode more than taken/skipped
- You need the actual set, not just its weight → store back-pointers / reconstruct after the DP
- The line case (array, not tree) → 1-D House Robber DP is simpler
Interview takeaway
Tree DP is 'solve each subtree, combine at the parent'. Encoding the parent's choice as a small state (here, taken or skipped) makes the children independent, so one post-order pass with O(1) work per node gives an O(n) optimum to a problem that is NP-hard on general graphs.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternTree DP with per-node states that capture the parent's decision. Here two states (taken forces children skipped, skipped frees them) make subtrees independent, so one post-order pass gives an O(n) optimum to a problem that is NP-hard on general graphs. Same engine as House Robber III and tree vertex cover.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- House Robber IIIComing soonThis exact DP on a binary tree — return (robbed, notRobbed) from each node.
- House RobberComing soonThe line (path) special case: the same take/skip recurrence over an array.
- Minimum Vertex Cover on a TreeComing soonComplement view: minimum vertex cover = n − maximum independent set (unweighted) via the same DP.
- Binary Tree CamerasComing soonThree-state tree DP (covered/has-camera/uncovered) — same post-order combine, richer states.