Why does a post-order tree DP that combines every child at each node still run in O(n)?
answer
- no subproblem is ever recomputed here
- charge the work to edges, not nodes
- each node is somebody's child once
- how many parent-child links exist?
- n-1 links times constant combine work
basics
~20 sCount 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 sThe 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
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.
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.
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.
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