129 · Sum Root to Leaf Numbers
Sub-variant D with a transformed accumulator: the value flowing down is rebuilt at
every level as cur * 10 + val, and it is harvested only at
leaves. Watch what "leaf" means here — it is not "null", and conflating the two is
the bug this problem is built to expose.
1 — The problem
Each root-to-leaf path spells a number, digit by digit. Sum them all. The tree
[4,9,0,5,1] spells 495, 491 and 40, totalling 1026.
Two things flow, in opposite directions:
| Direction | Carrier | Value |
|---|---|---|
| Down | the cur parameter | the number spelled by the path so far, extended by one digit per level |
| Up | the return value | the sum of every complete number in this subtree |
The accumulator differs from LC
1448's in one important way: it is not idempotent. Applying
max twice with the same value changes nothing, so a sloppy 1448 still
works. Applying * 10 + twice appends a digit that is not there. Order and
count both matter, which makes this the better test of whether you actually understand where the
transformation happens.
2 — Building the number on the way down
Follow cur: 4, then 49, then 495 — harvested. Then the recursion
unwinds to 49 and descends again into 1, giving 491. The digit 9 is never removed by hand; it
disappears because the stack frame that held it returned.
Node 0 is a leaf, so it harvests 40 — a two-digit number from a two-node path, with a leading digit of 4 and a trailing 0. Nothing special is needed for the zero; the arithmetic handles it.
2.1 The leaf test, and why null is not it
There are two plausible places to harvest, and only one is right:
| Harvest at… | Result on [4,9,0,5,1] | Why |
|---|---|---|
nd.left == null && nd.right == null | 1026 — correct | Fires once per leaf, which is once per complete path. |
nd == null, returning cur | 2052 — every number counted twice | A leaf has two null children, so each complete path is harvested twice. A one-child node harvests a number that was never spelled. |
The doubling is the friendly failure. The nastier one is the single-child node: on a tree like
[1,2] the null-harvesting version also collects the incomplete path
through node 1's missing right child, adding a 1 that no leaf ever spelled.
Compare with LC 104, where
basing everything on null is exactly right. The distinction: 104
aggregates over nodes, and null is the identity for that. This problem aggregates over
paths, and a path ends at a leaf. Sub-variant E exists to keep those apart — see
LC 112, where the same trap has teeth.
2.2 Why no backtracking is needed
The obvious worry: after finishing the left subtree, does cur need to
be "un-extended" before going right? No — and the reason is worth internalising, because it
is what separates sub-variant D from sub-variant E.
curis anint, passed by value. The linecur = cur * 10 + nd.valrebinds a local, and the caller's copy is untouched. Each frame gets its own.- In LC 257 the accumulator is a
List, passed by reference, and all frames share one object. That is when you must remove after recursing.
The rule: immutable state flowing down needs no backtracking; mutable shared state does.
3 — Complexity and edge cases
- Time O(n), space O(h). Each node contributes one multiply-add.
- Single node: its value. It is a leaf, so line 5 fires immediately.
- A node with exactly one child is not a leaf and must not harvest. This is the case that separates the two base-case styles.
- Zeros:
[0,1]spells 01 = 1. No leading-zero handling is required — the arithmetic produces it. - Overflow: LeetCode caps depth at 10, so the largest number is 10 digits and
intis enough — barely. Deepen the tree and this is the first thing to break;longcosts nothing. - Common bug: writing
return cur + sum(left, cur) + sum(right, cur)— adding the running value at every node instead of only at leaves. Internal nodes are not complete numbers.
4 — Reference implementation
Java 21Accumulator down, sum up — matching the visualizer.9 lines
public int sumNumbers(TreeNode root) {
return sum(root, 0);
}
private int sum(TreeNode nd, int cur) {
if (nd == null) return 0; // absent branch adds nothing
cur = cur * 10 + nd.val; // extend on the way down
if (nd.left == null && nd.right == null) return cur; // harvest at leaves only
return sum(nd.left, cur) + sum(nd.right, cur);
}Both base cases are present and they do different jobs: line 6 handles a missing branch, line 8 handles a completed path. Most wrong solutions have only one of them.