Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Root the tree and compute every node's subtree size with a single DFS. You may cut the edge above any non-root node whose subtree has an even number of nodes — doing so peels off an even component and leaves the rest even too. The maximum number of removable edges is simply the count of non-root nodes with an even subtree size. O(V).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you root the tree, see that a single DFS subtree-size pass suffices, and can argue why cutting every even subtree is both valid and maximal — plus whether you make the traversal iterative for large trees.
Where candidates slip
Counting the root's subtree (it has no parent edge to cut), assuming cuts conflict and reaching for DP, or recursing and overflowing the stack on a skewed tree.
How to stand out
Prove the greedy: cutting an even subtree leaves an even remainder because N is even, so the cuts never interfere. Then note the answer is a plain count of even non-root subtrees, computed in O(V).
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 approachFirst decision previewNode u (not root)size[u] even?YesNoExplore the full treeCollapse the tree
How to read it:Subtrees {3,4} (size 2) and {6,8,9,10} (size 4) are even; cutting their parent edges leaves {1,2,5,7} (size 4).
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
Recursive DFS Subtree Sizes (Baseline)
1Hint 1Explore hintHide hint
Root the tree at node 1. What single number per node tells you whether its parent edge is cuttable?
2Hint 2Explore hintHide hint
A subtree's size is 1 plus the sizes of its children's subtrees — a textbook post-order recursion.
3Hint 3Explore hintHide hint
While returning each size, if it is even and the node is not the root, increment a cut counter.
A · Core idea
Subtree sizes are the whole game, and a post-order DFS computes them in one sweep.
Why subtree size decides each edge. In a rooted tree, the only edge that isolates node u's subtree from everything else is the edge from u to its parent. Cutting it produces a component that is exactly u's subtree. So that cut is legal — yields an even component — precisely when size[u] is even.
Why the greedy is also maximal. Suppose you cut the parent edges of every non-root node with an even subtree. Each such cut removes an even-sized piece, and because N is even, the part left behind after removing an even piece is still even. So no cut spoils another's validity, and every legal cut can be taken simultaneously. You cannot do better than cutting every legal edge, so counting them is optimal.
Why the root is excluded. The root has no parent, hence no edge above it to cut. Its subtree (the whole tree, size N, which is even) is never a separable piece, so it must not be counted.
The recursion shape. dfs(u, parent) returns size(u): start at 1, add dfs(child, u) for each neighbor that is not the parent, and if the returned size is even and u is not the root, bump the counter. Skipping the parent is how an undirected edge list is treated as a rooted tree without a separate visited array.
The caveat. On a skewed tree (a long chain), the recursion nests O(V) deep. With small N here that is safe, but as a habit you should note that a deep tree can overflow the call stack — which motivates the iterative version.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
N = 4, edges = [[1,2],[2,3],[3,4]]Interactive diagram
Post-order sizes up a chain
dfs(4)Sizes returned bottom-up: 1, 2, 3, 4. Only the size-2 subtree at node 3 is a cut.
1C · Code with teaching comments
Reference implementation previewprivate List<List<Integer>> adj; private int cuts; public int evenForest(int n, int[][] edges) { adj = new ArrayList<>();Explore the full codeHide the full code
D · Why it works and complexityO(V) timeO(V) recursion stack spaceOpen the invariant and proofExplore the proofHide the proof
One post-order pass yields every subtree size, and the cut rule is a one-line parity test. The greedy is justified by N being even: removing an even chunk always leaves an even chunk, so the cuts never collide.
ComplexityO(V) time, O(V) recursion stack space — Each node is visited once and its size computed from its children → O(V) time. The recursion stack can be as deep as the tree height, which is O(V) on a skewed (chain) tree.
E · Edge cases and pitfalls3Counting the root's even subtree — it has no parent edge to cut.Explore edge casesHide edge cases
Pitfalls
- Counting the root's even subtree — it has no parent edge to cut.
- Recursing without a parent guard — infinite loop on the undirected edges.
- Deep recursion on a chain — risk of stack overflow; switch to iterative.
Approach 2 of 2 · Optimal
Iterative DFS Subtree Sizes (Optimal)
1Hint 1Explore hintHide hint
The recursion only used the stack to defer a parent until its children finished. Can you get the same post-order effect without the call stack?
2Hint 2Explore hintHide hint
Do an iterative DFS from the root recording parents and a visit order. A node always appears before its children in that order.
3Hint 3Explore hintHide hint
Walk the order in REVERSE: each node's children are already processed, so its size is final. Count it if even and non-root, then add its size to its parent.
A · Core idea
The logic is identical to the recursion; only the mechanism changes so a deep tree cannot blow the call stack.
Step 1 — order and parents. Run an iterative DFS (or BFS) from the root using an explicit stack. When you pop a node, append it to an order list and push each neighbor that is not its parent, recording the parent as you go. Because the tree is acyclic, each node is pushed exactly once; the parent check replaces a visited array. In the resulting order, every node appears before all of its descendants.
Step 2 — sizes in reverse. Initialize size[u] = 1 for all nodes. Iterate order in reverse. When you reach node u, all of its descendants have already been processed, so size[u] has already accumulated every child's subtree — it is final. If u is not the root and size[u] is even, increment the cut count. Then fold u's size into its parent: size[parent[u]] += size[u]. By the time you reach any node, its full subtree size is known, exactly mirroring post-order recursion.
Why this is the safe default. The only difference from the recursive version is that the deferral lives on an explicit heap-allocated stack and an array sweep, not on the call stack. A chain of 10⁵ nodes — or any skewed tree — processes without risk of overflow. The time and the answer are identical.
Why the parity rule is unchanged. Cutting the parent edge of an even subtree still peels off an even component and leaves an even remainder (N is even), so every even non-root subtree is an independent, valid, and necessary cut. The count is the maximum number of removable edges.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
N = 6, edges = [[1,2],[2,3],[3,4],[4,5],[5,6]]Array state
init
all sizes 1 (index 1..6)
2C · Code with teaching comments
D · Why it works and complexityO(V) timeO(V) spaceOpen the invariant and proofExplore the proofHide the proof
Recording a DFS order and accumulating sizes in reverse reproduces post-order without recursion. It is the same O(V) computation, made stack-safe — the pattern to reach for whenever a tree might be deep.
ComplexityO(V) time, O(V) space — The forward DFS pushes and pops each node once → O(V). The reverse accumulation visits each node once → O(V). Arrays for parent, size, and the order list are O(V). No recursion stack.
E · Edge cases and pitfalls7N = 2, edges = [[1,2]] — size[2] = 1 (odd) → no cut. A two-node tree is already one even component; answer 0.Explore edge casesHide edge cases
Edge cases handled
N = 2, edges = [[1,2]]size[2] = 1 (odd) → no cut. A two-node tree is already one even component; answer 0.star: one center, N−1 leavesEvery leaf subtree is size 1 (odd) → 0 cuts unless a leaf chain makes an even subtree.deep chain of N nodesIterative DFS handles the O(N) depth with no overflow; recursion might not.
Pitfalls
- Counting the root (index 1) — exclude it explicitly.
- Processing the order forward instead of reversed — parents summed before children, wrong sizes.
- Forgetting the parent guard while building the order — would revisit via the undirected edge.
- Recursive DFS on a long chain — overflow; the explicit stack avoids it.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int evenForest(int n, int[][] edges) { List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i <= n; i++) adj.add(new ArrayList<>()); for (int[] e : edges) { adj.get(e[0]).add(e[1]);Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Counting the root
The root has no parent edge, so its subtree (the whole tree, size N) can never be separated. Including it overcounts by one whenever N is even — which it always is here.
Explore the code comparisonHide the code comparison
Counting the root
The root has no parent edge, so its subtree (the whole tree, size N) can never be separated. Including it overcounts by one whenever N is even — which it always is here.
Don't write this
if size[u] % 2 == 0: # also counts the root
cuts += 1Write this instead
if u != root and size[u] % 2 == 0:
cuts += 12Missing the parent guard
The edges are undirected, so adj[u] includes the parent. Without skipping it, the traversal walks back up and revisits nodes, corrupting subtree sizes (or looping forever).
Explore the code comparisonHide the code comparison
Missing the parent guard
The edges are undirected, so adj[u] includes the parent. Without skipping it, the traversal walks back up and revisits nodes, corrupting subtree sizes (or looping forever).
Don't write this
for (int v : adj.get(u)) {
size += dfs(v, u); // recurses back into the parent
}Write this instead
for (int v : adj.get(u)) {
if (v != parent) { // only descend into children
size += dfs(v, u);
}
}3Deep recursion overflow
A skewed tree (a chain) has height equal to N. A recursive DFS nests N call frames and can overflow the stack; an explicit-stack iterative DFS keeps that depth on the heap.
Explore the code comparisonHide the code comparison
Deep recursion overflow
A skewed tree (a chain) has height equal to N. A recursive DFS nests N call frames and can overflow the stack; an explicit-stack iterative DFS keeps that depth on the heap.
Don't write this
def dfs(u, parent):
size = 1
for v in adj[u]:
if v != parent:
size += dfs(v, u) # depth = tree height
return sizeWrite this instead
stack = [1]
order = []
while stack:
u = stack.pop()
order.append(u)
for v in adj[u]:
if v != parent[u]:
parent[v] = u
stack.append(v)
# accumulate sizes over reversed(order)Revise
Carry the pattern into the interview
Pattern to remember
Root the tree, DFS subtree sizes, count even non-root subtrees
Code pattern to memorize
root at 1, compute size[u] = 1 + sum(size[child])
answer = number of nodes u (u != root) with size[u] even
# total nodes N is even, so the leftover is always even tooRules to remember
- 1The total node count
Nis even (guaranteed) — that is what makes an all-even partition possible. - 2You may cut the parent edge of node
uexactly whensize[u]is even; count every such non-root node. - 3The root has no parent edge, so it never contributes a cut even if
size[root] = Nis even. - 4Cuts are independent: removing one even subtree leaves both pieces even, so greedily taking all of them is optimal.
- 5Use an iterative DFS (explicit stack) — a deep or skewed tree can overflow a recursive call stack.
Use this pattern when
- Tree problems answerable from **subtree sizes** computed in one post-order pass
- Partitioning a tree by cutting edges under a parity / size constraint
- Greedy cut decisions that are locally safe and globally independent
- Any 'remove edges so each piece satisfies P' on a tree where P depends only on subtree counts
Choose another pattern when
- The graph has cycles → it is not a tree; subtree sizes are undefined
- Cuts interact (one affects another's validity) → greedy fails, needs DP
- You need an actual minimum or weighted partition → tree DP over (cut, not-cut) states
- Constraint depends on node values, not counts → size alone is insufficient
Interview takeaway
Subtree sizes from one DFS answer a surprising range of tree questions. Here, an even subtree can always be detached without harming the rest, so the answer is just how many even subtrees hang below the root — a clean greedy backed by the evenness of N.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternSubtree-Size DFS with a Greedy Cut Rule. Recognize it when a tree question reduces to a per-node decision based on its subtree count, and the decisions do not interfere. One post-order pass (iterative for safety) yields the sizes; a parity or threshold test yields the answer. The same subtree-size engine powers Diameter, Centroid, and many tree DPs.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Diameter of Binary TreeComing soonAnother single post-order pass — combine child heights instead of counting even subtrees.
- Tree DP: Maximum Independent SetComing soonWhen cut decisions interact, move from a greedy count to (include, exclude) tree DP.
- Centroid of a TreeComing soonAlso driven by subtree sizes — find the node whose removal balances the pieces.
- Count of Subtrees With a Given PropertyComing soonGeneralize the parity test to any predicate evaluated on subtree aggregates.