Back to Trees & BST
Q17Question 17 of 25

All Paths for a Sum (Collect Every Root-to-Leaf Path Matching a Target)

Key Concepts
  • DFS with a running path list collects every root-to-leaf path whose node values sum to the target
  • Backtracking adds a node before recursing and removes it after both children return, reusing one list
  • At a matching leaf, store a defensive copy of the path list, never the live reference
  • Time is O(n * h) in the worst case because copying a path of height h can happen at up to n leaves
  • Space is O(h) for the recursion stack plus O(n * h) for the stored result paths
  • Forgetting the defensive copy is the most common bug, as all stored paths end up pointing at the same emptied list

Definition

Depth-first with a running path list: copy the path at every matching leaf, and backtrack by removing the last node on the way back up.
  • All Paths for a Sum extends the single-boolean path-sum check into a collection problem that returns every qualifying path.
  • Running path list tracks the current root-to-node sequence; each node is appended on entry and removed on exit.
  • Backtracking means the add-then-remove discipline that lets one mutable list serve every branch of the tree.
  • Defensive copy at a matching leaf stores new ArrayList<>(currentPath) so the live list can keep changing safely.
  • Time is O(n) to visit plus O(h) per qualifying leaf to copy, giving O(n * h) worst case.
  • Space is O(h) for the stack plus the size of all stored paths.

At the National Bank of India, compliance needs the full list of every approval chain from the branch manager down to a clerk whose processing fees add up to the audit budget. Rajesh walks down each chain writing desk names on a notepad. Whenever a complete chain hits the budget, he photocopies the notepad into the audit report. Then he erases the last desk and tries the next branch, reusing the same notepad for every path.

Understanding the Concepts

What is the All Paths for a Sum problem?

All Paths for a Sum asks you to find every root-to-leaf path in a binary tree where the node values along the path add up to a given target. Unlike the simpler Path Sum problem that returns only true or false, this variant must return the actual paths. The approach is a depth-first traversal that carries a mutable list representing the current path, copies it whenever a leaf matches the target, and backtracks by removing the last element after exploring both children.

JAVA
package com.bank.service;

import java.util.ArrayList;
import java.util.List;

public class ApprovalPathFinder {
    // Entry point: find all root-to-leaf paths summing to target
    public List<List<Integer>> findAllPaths(TreeNode root, int target) {
        List<List<Integer>> results = new ArrayList<>();
        dfs(root, target, new ArrayList<>(), results);
        return results;
    }
}

Why does this problem exist?

In real banking systems, a loan approval flows through multiple desks, each charging a fee. The compliance team sometimes needs every chain of desks whose total fees match a specific budget figure, not just whether one exists. This is exactly the collection version of path sum: enumerate all qualifying paths rather than stop at the first.

How does backtracking work internally?

Backtracking is the add-then-remove discipline on a single mutable list. When you enter a node, you append it to the path. When you leave a node after exploring both children, you remove the last element. This means the list always reflects the current root-to-node path, and you never allocate a fresh list at every recursive call.

JAVA
private void dfs(TreeNode node, int remaining,
                 List<Integer> currentPath,
                 List<List<Integer>> results) {
    if (node == null) return;

    currentPath.add(node.val);  // Step 1: add current node

    // Step 2: check if this is a matching leaf
    if (node.left == null && node.right == null
            && remaining == node.val) {
        results.add(new ArrayList<>(currentPath)); // defensive copy
    }

    // Step 3: recurse into children with reduced target
    dfs(node.left, remaining - node.val, currentPath, results);
    dfs(node.right, remaining - node.val, currentPath, results);

    // Step 4: backtrack by removing the current node
    currentPath.remove(currentPath.size() - 1);
}
TEXT
Example tree:       5
                   / \
                  4    8
                 /    / \
                11   13   4
               / \       / \
              7   2     5   1

Target = 22
Paths found: [5,4,11,2] and [5,8,4,5]

Why must you store a defensive copy?

The most common bug in this pattern is storing the live list reference instead of a copy. Because the live list keeps changing as backtracking continues, every stored reference would end up pointing at the same eventually-empty list. The fix is new ArrayList<>(currentPath) at every matching leaf.

JAVA
// WRONG: stores the live reference
results.add(currentPath);  // all entries point to same list

// CORRECT: stores a snapshot
results.add(new ArrayList<>(currentPath));  // each entry is independent

What is the time and space complexity?

Each node is visited exactly once during the DFS, which is O(n). However, at every matching leaf you copy the current path, which has length up to h (the tree height). In the worst case there can be O(n) leaves, so the total copying cost is O(n * h). For a balanced tree h is O(log n), and for a skewed tree h is O(n), giving O(n^2) in the worst case.

  • The space breaks down as:
  • O(h) for the recursion call stack.
  • O(h) for the running path list.
  • O(k * h) for the k result paths stored.
JAVA
// Banking example: model approval desks with BigDecimal fees
package com.bank.model;

import java.math.BigDecimal;

public class ApprovalDesk {
    public String deskName;
    public BigDecimal fee;
    public ApprovalDesk left;
    public ApprovalDesk right;

    public ApprovalDesk(String name, BigDecimal fee) {
        this.deskName = name;
        this.fee = fee;
    }
}

How is this different from Path Sum (boolean version)?

Path Sum returns true or false and short-circuits on the first match. All Paths for a Sum must explore every branch because multiple qualifying paths can exist. Path Sum needs no path list and no copying; All Paths for a Sum needs both.

| Aspect | Path Sum (boolean) | All Paths for a Sum |
|--------|-------------------|--------------------|
| Return type | boolean | List of Lists |
| Short-circuit | Yes, on first match | No, must explore all |
| Path tracking | Not needed | Running list + backtrack |
| Defensive copy | Not needed | Required at each match |
| Time | O(n) | O(n * h) worst case |

What are common mistakes?

The first mistake is forgetting the defensive copy, as discussed above. The second mistake is adding the node value to the path instead of the node itself when the problem asks for node values. The third mistake is checking the sum condition at every node instead of only at leaves, which would report prefix paths that do not reach a leaf.

JAVA
// WRONG: checks at every node, not just leaves
if (remaining == node.val) {
    results.add(new ArrayList<>(currentPath));
}

// CORRECT: checks only at leaf nodes
if (node.left == null && node.right == null && remaining == node.val) {
    results.add(new ArrayList<>(currentPath));
}

What about iterative approaches?

You can replace the recursion with an explicit stack that stores tuples of (node, remaining target, path snapshot). However, because you must copy the path at each stack push, this approach uses more memory than the backtracking version that reuses one list. The recursive backtracking solution is preferred unless the tree is extremely deep and risks a stack overflow.

What are edge cases?

  • An empty tree returns an empty list of paths.
  • A single-node tree where the node value equals the target returns a list containing one path with that single value.
  • A tree where no path matches returns an empty list.
  • Negative node values are valid and the algorithm handles them without changes because it subtracts each value from the remaining target.
JAVA
// Edge case: single node tree
//   Target = 5, Tree = [5]
//   Result: [[5]]

// Edge case: no matching path
//   Target = 100, Tree = [1,2,3]
//   Result: []

What should a senior engineer know?

The total work is not linear when many paths qualify. Copying a path costs O(h) and there can be O(n) leaves, so the worst-case cost is O(n * h). For a balanced binary tree this is O(n log n), and for a skewed tree it is O(n^2). This cost is inherent to producing the paths themselves. If you only need the count of qualifying paths rather than the paths themselves, you can avoid the copying cost entirely and solve it in O(n).

Another senior point is that in production systems like the National Bank of India audit service, Rajesh would stream the qualifying paths rather than collecting them all in memory, using a callback or an iterator pattern, to avoid holding O(k * h) paths in the heap simultaneously.

Visual Overview

DIAGRAM
  Backtracking with a running path list
  ======================================

  Tree:        5                Target = 22
              / \
             4    8
            /    / \
           11   13   4
          / \       / \
         7   2     5   1

  DFS descent (path list grows):
  ┌─────────────────────────────────────┐
  │  Enter 5  → path = [5]             │
  │  Enter 4  → path = [5,4]           │
  │  Enter 11 → path = [5,4,11]        │
  │  Enter 2  → path = [5,4,11,2]      │
  │  Leaf! sum=22 ✓ → COPY to results  │
  │  Backtrack → path = [5,4,11]       │
  │  Enter 7  → path = [5,4,11,7]      │
  │  Leaf! sum=27 ✗                    │
  │  Backtrack → path = [5,4,11]       │
  │  Backtrack → path = [5,4]          │
  │  Backtrack → path = [5]            │
  │  Enter 8  → path = [5,8]           │
  │  ...continues exploring right...    │
  └─────────────────────────────────────┘

  Key operations at each node:
  ┌──────────┐    ┌──────────┐    ┌───────────────┐
  │ ADD node │───▶│ RECURSE  │───▶│ REMOVE node   │
  │ to path  │    │ children │    │ (backtrack)   │
  └──────────┘    └──────────┘    └───────────────┘

Key Takeaway

All Paths for a Sum is the collection version of the path-sum problem: instead of a single boolean, you return every root-to-leaf path whose node values add up to the target. The approach is a depth-first traversal that carries one running path list. As you descend you append the current node to that list and subtract its value from the target, and when you reach a leaf whose remaining target is zero you store a defensive copy of the list in the results. After exploring both children you remove the current node, which is the backtracking step that lets you reuse the same list for every path.

The detail that separates correct implementations from buggy ones is the defensive copy. If you store the live list reference, continued backtracking keeps mutating it, and every stored path ends up pointing at the same emptied list. Always use new ArrayList<>(currentPath) at every matching leaf.

The total work is not strictly linear because copying a path of height h across up to n leaves gives O(n * h), and that cost is inherent to producing the paths themselves rather than merely counting them. For a balanced tree this is O(n log n), and for a skewed tree it can reach O(n^2).

At the National Bank of India, Rajesh applied this to audit the approval hierarchy, where compliance needed every desk chain whose processing fees summed exactly to the agreed budget. Each desk was a node with a BigDecimal fee. The backtracking traversal from the top desk emitted each matching chain into the audit report. Describing the reuse-and-backtrack discipline together with the defensive copy is what tells an interviewer you can implement backtracking correctly under real requirements.

Remember one sentence: backtrack by adding before recursion and removing after, and always store a copy of the path at a matching leaf, never the live list.

---

Share
Use ← → arrow keys to navigate