skip to content

Why does a post-order tree DP that combines every child at each node still run in O(n)?

level: middleimportance: should knowfreq 45%

answer

  1. no subproblem is ever recomputed here
  2. charge the work to edges, not nodes
  3. each node is somebody's child once
  4. how many parent-child links exist?
  5. n-1 links times constant combine work

basics

~20 s

Count the work per edge, not per node. Every parent-child link is used exactly once, and a tree on n nodes has n-1 links, so the total combining work is O(n) as long as each node spends constant time per child.

solid answer

~40 s

The worry is that a node with many children makes the pass quadratic, but that double-counts. Across the entire traversal, the body of the "for each child" loop runs once per parent-child edge, and a tree on `n` nodes has exactly `n-1` edges — so the sum of all child counts is `n-1`, not `n^2`. The general bound is `O(n * S * c)`: `n` nodes, `S` stored states per node, `c` work per child per state. Linearity breaks only if the per-node combine is superlinear in the child count — pairing children against each other, or merging child tables in a capacity-style combine. Space is the other half of the answer: recursion depth equals the tree's height, which is `O(n)` on a path-shaped tree, so the auxiliary space is not automatically logarithmic.

go deeper

for a junior

Know that a tree DP is computed bottom-up: a node's answer is built from answers already finished for its children, and one pass over the whole tree is linear in the number of nodes.

for a middle

Be ready to derive the O(n) bound out loud by counting parent-child links rather than nodes, and to name the per-node combine step that would break linearity.

for a senior

Show that you check the shape of real input: a path-shaped tree makes recursion depth O(n), so say when you would convert the recursion to an explicit stack before it reaches production.

for a principal

Own the cost model for the whole family. Say what state count and combine cost your team's tree DPs are allowed to have, and flag the subtree-merge variants that quietly become quadratic at scale.

## What a tree DP actually is A dynamic program is a set of subproblems plus an order in which they can be solved, where each subproblem depends only on subproblems already solved. On a rooted tree that structure is handed to you for free: the natural subproblem is "the answer for the subtree rooted at node `v`", and the dependencies point strictly downward — `dp[v]` is a function of `dp[c]` for the children `c` of `v`. Solving children before parents is therefore not a stylistic choice about traversal order; it is the only order in which the dependencies are satisfied. That is the whole reason a bottom-up (post-order) evaluation is the shape of every DP on a tree. ## The cost accounting, done correctly The common wrong answer is: "each of the `n` nodes loops over its children, and a node can have up to `n-1` children, so it is `O(n^2)`." The flaw is that the two factors are not independent. Charge the work to edges instead of to nodes: each execution of the loop body corresponds to exactly one parent-child pair, and every node except the root is somebody's child exactly once. A tree on `n` nodes has exactly `n-1` edges, so the loop body executes `n-1` times **in total across the whole traversal**, no matter how lopsided the branching is. A star with one node and `n-1` leaves costs the same total as a path. The general form of the bound: | factor | meaning | typical value | | --- | --- | --- | | `n` | nodes visited, once each | the tree size | | `S` | states stored per node | 2 for an include/exclude pair | | `c` | work per child, per state | `O(1)` for a sum or a max | Total time is `O(n * S * c)`. With a constant state count and a constant-time combine, that is `O(n)`. ## Where linearity actually breaks The bound assumes the combine is constant work **per child**. Three realistic ways to lose it: 1. **Pairwise combines.** If a node compares every pair of its children (rather than keeping a running best or a running sum), its cost is quadratic in its child count, and a star-shaped tree makes the total `O(n^2)`. 2. **Merging child tables.** When each node carries a table indexed by something like a budget or a count, merging two child tables costs the product of their sizes. Bounded by subtree sizes, the standard argument gives `O(n^2)` overall; bounded by a capacity `K`, it gives `O(n * K)`. Both are correct answers — but neither is `O(n)`, and claiming linearity there is a real error. 3. **Sorting children.** Sorting each node's children costs `O(k log k)` locally and `O(n log n)` overall. Usually unnecessary: a running top-one or top-two scan does the same job in one pass. ## Space is not free A recursive post-order DP holds one frame per ancestor of the node being processed, so its auxiliary space is `O(h)` where `h` is the height. On a balanced tree that is `O(log n)`, but nothing guarantees balance: a tree built from a chain of links is a path, and `h = n`. On large inputs this is a genuine crash, not a theoretical footnote, and the fix is to run the post-order with an explicit stack (or to convert to an iterative order by processing nodes in reverse of a discovery order). If you claim `O(1)` space because "we only keep a couple of numbers per node", you have forgotten that the stored states themselves are `O(n * S)` and the recursion is `O(h)`. ## Why no memo table is needed DP on a general dependency graph needs a table or a visited marker because the same subproblem is reachable from several predecessors. In a rooted tree every node has exactly one parent, so every subproblem is reached exactly once and the traversal *is* the memoization — the results are consumed by the single caller that needed them. One consequence worth stating out loud: if the input is an undirected link list with no designated root, you must still avoid walking back up, which is done by passing the parent down rather than by maintaining a global marker structure. ## What the interviewer is checking They want to hear the edge-counting argument, not the recital of a memorised `O(n)`. A candidate who can say "the loop body runs once per edge, there are `n-1` edges" can also tell you, unprompted, exactly which combine step would spoil it — and that second half is what separates a memorised bound from an understood one.

  • When does a tree DP stop being linear?
    When the per-node combine is superlinear in the child count. Comparing every pair of a node's children is quadratic in its degree, and merging child tables costs the product of their sizes — that family lands at O(n^2), or O(n*K) when the table is capped at a capacity K. Any combine that is constant work per child keeps the total at O(n).
  • Does a tree DP need a memo table the way DP over a general dependency graph does?
    No. Each node has exactly one parent, so each subproblem is reached exactly once and the traversal itself acts as the memo. In a general dependency graph a subproblem has many predecessors and would be recomputed without a table. The tree case still needs the parent passed down so an undirected walk does not go back up.
  • What is the auxiliary space of the recursive version?
    O(h) for the call stack, where h is the height — O(log n) only if the tree happens to be balanced, and O(n) for a path-shaped tree, which is a genuine overflow risk on large inputs. Plus O(n * S) for the stored states. Converting the post-order to an explicit stack removes the recursion-depth risk.

saying these in an interview costs you the question

  • Says scanning children at every node makes it O(n^2)
  • Assumes the tree is balanced so depth is log n
  • Calls the space O(1) because only a few numbers per node are kept
  • Claims a memo table keyed by node is mandatory
  • Reports O(n) for a combine that merges child tables pairwise

context