Why does T(n)=2T(n/2)+O(n^2) total Θ(n^2) rather than Θ(n^2 log n)?
answer
- write the cost of each level
- compare a level to the one below
- squaring shrinks faster than branching grows
- one half plus one quarter plus one eighth
- root outweighs all its children combined
basics
~20 sPer-level work halves as you descend: n^2, then n^2/2, then n^2/4. The series sums to under 2n^2, so the root call dominates and the total is Θ(n^2). Multiplying per-call work by depth only works when levels cost the same.
solid answer
~40 sSum the recursion tree by levels instead of guessing. Level i holds 2^i calls on inputs of size n/2^i, and each does work proportional to the square of its own size: 2^i × c·(n/2^i)² = c·n²/2^i. So the level costs are c·n², c·n²/2, c·n²/4 — a decreasing geometric series that sums to under 2c·n². The root's own quadratic combine accounts for more than half the entire run, and the depth is irrelevant. The `× log n` reflex comes from the flat-level case where every level costs the same; here the levels shrink geometrically, so the tree is root-dominated. The practical consequence is concrete: profiling shows the top-level frame, and the only optimisation that pays is making the combine step cheaper — trimming levels off the bottom buys almost nothing.
code
pseudocode · 14 linesreport(d):
if is_file(d):
return [d]
left, right = subdirs(d) // each subtree holds about half the files
files = concat(report(left), report(right))
unique = []
for i in 0..length(files)-1:
seen = false
for j in 0..length(unique)-1:
if id(unique[j]) == id(files[i]):
seen = true
if seen == false:
append(unique, files[i])
return uniquego deeper
Recall that the total cost of a recursion is the sum of every call's own work, and that this sum is not always work-per-call times depth. Be able to write the per-level cost for a recurrence before reaching for a formula.
Explain the three tree regimes and compute the level costs for T(n)=2T(n/2)+O(n^2): n^2, n^2/2, n^2/4. Show that the halving series sums to a constant times the root's work, so the depth drops out.
Diagnose this in real code: spot a combine step whose cost is superlinear in the subtree it handles, connect the root-dominated shape to what the profile shows, and pick the optimisation that actually pays instead of tuning the base case.
Own the call about whether to fix it at all. Weigh the rewrite cost of the combine step against the input sizes the system genuinely sees, and set the expectation that a quadratic top-level step caps throughput no matter how the recursion below is tuned.
## Where such a recurrence comes from The attached fragment rolls a file listing up a balanced directory tree, and at each directory it removes duplicates by comparing every collected file against every file kept so far. Two recursive calls, each covering about half the files; a combine step that is quadratic in the number of files the call is handling. That is `T(n) = 2T(n/2) + O(n²)` and it is the shape of a whole family of real defects: a merge step that does a nested scan, an aggregation that re-sorts by comparing all pairs, a validation that cross-checks every collected item against every other one. The seductive wrong answer is "quadratic work, logarithmic depth, so n² log n". It over-charges, and the reason it over-charges is worth understanding, because the same reasoning error goes the other way in other recurrences and *under*-charges there. ## Sum the tree by levels Level i of the tree holds 2^i calls, each on about n/2^i files. Each of those calls does work proportional to the *square* of its own input: > level i cost = 2^i × c·(n/2^i)² = c·n²/2^i So the level costs run c·n², c·n²/2, c·n²/4, c·n²/8, … Squaring shrinks the per-node work by four while the node count only doubles, so half the work disappears with every level down. Summing the whole tree: > c·n²·(1 + ½ + ¼ + ⅛ + …) < 2c·n² The entire recursion costs less than twice what its single top-level call costs on its own. The total is **Θ(n²)**, and the number of levels never enters the arithmetic. This is a *root-dominated* tree. ## The three regimes, and the rule they replace Every recursion tree falls into one of three shapes, and you must identify which one before you compute anything: | Level costs as you descend | Dominated by | Total | |---|---|---| | Shrink geometrically (n², n²/2, n²/4 …) | the root | Θ(root work) | | Stay flat (n, n, n …) | nothing in particular | Θ(level work × depth) | | Grow geometrically (1, 2, 4 … up to n) | the leaves | Θ(leaf work) | "Multiply per-call work by the depth" is the middle row only. Applying it to the top row over-charges by a log factor, which is the error in this question. Applying it to the bottom row is where people miss that a constant-work-per-call binary recursion is already linear because of its leaf count. The habit worth building is: compute the level costs, look at their *ratio*, and only then decide whether to multiply or to sum a series. A quick way to spot the regime without writing the series: compare one call's own work against the total work of its children. Here a call on m files does c·m², and its two children do 2·c·(m/2)² = c·m²/2 — half as much. Any time a parent out-works all of its children combined by a constant ratio, the tree is root-dominated and the answer is just the root's own work. ## What this changes in practice **Where the time shows up.** Because the root accounts for more than half the run, a sampled profile points squarely at the outermost invocation, not at a hot leaf. Engineers used to leaf-dominated recursions sometimes distrust that profile and go hunting deeper. Trust it: in a root-dominated tree the profile is telling the truth, and the top frame really is the workload. **Which optimisation pays.** Three tempting changes, and only one of them matters: - *Raising the base-case cutoff* so small directories are handled directly — trims levels off the bottom, where less than a thousandth of the work lives. Nearly worthless here, even though it is a genuine win in flat-level recursions. - *Reducing the branching factor or balancing the tree better* — changes the depth, which does not appear in the answer at all. - *Making the combine step cheaper* — changes the root's own work, which **is** the answer. Deduplicating by identity through a hash-based set makes the combine linear, turning the recurrence into `T(n)=2T(n/2)+O(n)` and the whole run into Θ(n log n). Keeping the sublists sorted and merging them also gives a linear combine. Either way the fix is at the top, not the bottom. **Why the bug survives review.** Each call looks locally reasonable — a nested loop over "just this directory's files". The quadratic only becomes visible when you notice that the list a call handles grows with the whole subtree, so the outermost call's inner loop runs over all n files. Writing the recurrence down is exactly the discipline that surfaces it: f(n) is whatever this call does to its own input, and here that is n² at the root. ## Stating it in an interview "Level i does n²/2^i work, the series sums to about 2n², so it's Θ(n²) and the root dominates — the depth doesn't matter here. If I want a real speedup I have to attack the combine step, not the recursion." That answer shows you sum trees rather than pattern-match them, and it lands the operational point that the fix belongs at the top of the tree.
- What does the recurrence become if the duplicate check uses a hash-based set instead of a nested scan?The combine drops to about one pass over the collected files, so the recurrence becomes T(n)=2T(n/2)+O(n). Now every level costs about n and the levels are flat, so the total is Θ(n log n). Note that the tree also changes regime: with flat levels, trimming levels off the bottom with a base-case cutoff finally becomes worth something, which it was not before.
- You profile the slow version and the top-level frame holds most of the samples. Is that suspicious?No — it is what a root-dominated tree looks like. The outermost call does more work than the whole rest of the tree combined, so a sampled profile attributing the majority of self-time there is correct rather than an artefact of inlining or sampling bias. Chase the combine step in that frame; do not go looking for a hot leaf that does not exist.
- How would you spot the regime without writing out the series?Compare one call's own work to the combined work of its children. A call on m items does c·m² while its two children do 2·c·(m/2)² = c·m²/2, so the parent out-works its children by a constant ratio and the tree is root-dominated. If the children collectively out-work the parent, it is leaf-dominated; if they match, the levels are flat and the depth enters the answer.
A dropped-off relay: the first runner covers more than half the total distance, and every runner after covers half of what the one before did. Shortening the last legs changes almost nothing.
saying these in an interview costs you the question
- Multiplies per-call work by the depth in every recurrence
- Assumes deeper recursion always means more total work
- Tries to fix it by raising the base-case cutoff
- Distrusts a profile that blames the outermost call
- Counts only immediate items, missing that the list spans the subtree