Path problems
Path problems on trees: root-to-leaf paths, path sums, lowest common ancestor, the distance between two nodes and the boundary traversal.
Learning goals
- Collect root-to-leaf paths with choose-explore-unchoose
- Decide a root-to-leaf sum with a shrinking target
- Find a lowest common ancestor in one pass
- Compute distances through the lowest common ancestor
- Produce the boundary of a tree in anticlockwise order
Prerequisites
A path in a tree is unique, so path problems are cleaner than in graphs. They are depth-first searches where the current path is a piece of state, and the choose-explore-unchoose pattern of backtracking applies directly.
Root-to-leaf paths
To list every root-to-leaf path, keep the current path in a list: add the node on the way down (**choose**), record a copy when a leaf is reached, recurse into both children (**explore**), and remove the node on the way up (**unchoose**). A tree has one path per leaf. In the first preset, “1 2 3 # 5” has the 2 paths 1→2→5 and 1→3.
The same shape appears in the backtracking chapter; here the 'options' at each node are simply the two children, so the search never repeats a state and needs no pruning.
1static List<String> rootToLeaf(TreeNode root) {2List<String> out = new ArrayList<>();3walk(root, new ArrayList<>(), out);4return out;5}6static void walk(TreeNode n, List<Integer> path, List<String> out) {7if (n == null) return;8path.add(n.val); // choose9if (n.left == null && n.right == null) out.add(path.toString());10else { walk(n.left, path, out); walk(n.right, path, out); }11path.remove(path.size() - 1); // unchoose12}
Best
O(n)
Average
O(n · h)
Worst
O(n²)
Space
O(h) plus the output
Path problems: root-to-leaf paths, path sum, lowest common ancestor, distance, boundary
Interactive · edit the inputbinary tree · 9 nodes
Path sum: is there a root-to-leaf path adding up to 22?
1static void paths(TreeNode n, List<Integer> path, List<String> out) {2if (n == null) return;3path.add(n.val); // choose4if (n.left == null && n.right == null) out.add(path.toString());5else { paths(n.left, path, out); paths(n.right, path, out); }6path.remove(path.size() - 1); // unchoose7}8static boolean hasPathSum(TreeNode n, int remaining) {9if (n == null) return false;10remaining -= n.val;11if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a root-to-leaf path12return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);13}14static TreeNode lca(TreeNode n, int a, int b) {15if (n == null || n.val == a || n.val == b) return n;16TreeNode l = lca(n.left, a, b), r = lca(n.right, a, b);17return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them18}19static int distance(TreeNode root, int a, int b) {20TreeNode m = lca(root, a, b);21return depth(m, a, 0) + depth(m, b, 0); // edges from the LCA down to each node22}23static int depth(TreeNode n, int v, int d) {24if (n == null) return -1;25if (n.val == v) return d;26int l = depth(n.left, v, d + 1);27return l >= 0 ? l : depth(n.right, v, d + 1);28}29static List<Integer> boundary(TreeNode root) { // anticlockwise from the root30List<Integer> out = new ArrayList<>();31if (root == null) return out;32if (!isLeaf(root)) out.add(root.val);33for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge34addLeaves(root, out); // all leaves, left to right35Deque<Integer> right = new ArrayDeque<>();36for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);37out.addAll(right); // right edge, bottom to top38return out;39}40static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }41static void addLeaves(TreeNode n, List<Integer> out) {42if (n == null) return;43if (isLeaf(n)) out.add(n.val);44addLeaves(n.left, out); addLeaves(n.right, out);45}
Live state
problem
Path sum
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L8 | problem=Path sum | Path sum: is there a root-to-leaf path adding up to 22? |
| 2 | L10 | remaining=17 | Take 5: 22 - 5 = 17 still to find. |
| 3 | L10 | remaining=13 | Take 4: 17 - 4 = 13 still to find. |
| 4 | L10 | remaining=2 | Take 11: 13 - 11 = 2 still to find. |
| 5 | L10 | remaining=-5 | Take 7: 2 - 7 = -5 still to find. |
| 6 | L11 | remaining=-5 | 7 is a leaf but -5 remains: this path does not work. |
| 7 | L10 | remaining=0 | Take 2: 2 - 2 = 0 still to find. |
| 8 | L11 | remaining=0 | 2 is a leaf and nothing remains: the path 5 → 4 → 11 → 2 adds up to 22. |
| 9 | L12 | result=true | true: 5 + 4 + 11 + 2 = 22. |
Path sum
'Is there a root-to-leaf path with sum S?' subtracts each node's value from the target on the way down and asks the question again of the children. Only a **leaf** with exactly 0 remaining answers yes: a partial path that reaches the target at an internal node does not count.
The “Path sum 22 (exists)” preset finds 5 + 4 + 11 + 2 = 22. The “Path sum 9 (reached early: no)” preset shows 5 + 4 = 9 at node 4, which is internal, so the search continues and answers false after trying every leaf. The recursion short-circuits as soon as one side returns true.
1static boolean hasPathSum(TreeNode n, int remaining) {2if (n == null) return false;3remaining -= n.val;4if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a path5return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);6}
Best
O(h)
Average
O(n)
Worst
O(n)
Space
O(h)
Path problems: root-to-leaf paths, path sum, lowest common ancestor, distance, boundary
Interactive · edit the inputbinary tree · 9 nodes
Path sum: is there a root-to-leaf path adding up to 22?
1static void paths(TreeNode n, List<Integer> path, List<String> out) {2if (n == null) return;3path.add(n.val); // choose4if (n.left == null && n.right == null) out.add(path.toString());5else { paths(n.left, path, out); paths(n.right, path, out); }6path.remove(path.size() - 1); // unchoose7}8static boolean hasPathSum(TreeNode n, int remaining) {9if (n == null) return false;10remaining -= n.val;11if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a root-to-leaf path12return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);13}14static TreeNode lca(TreeNode n, int a, int b) {15if (n == null || n.val == a || n.val == b) return n;16TreeNode l = lca(n.left, a, b), r = lca(n.right, a, b);17return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them18}19static int distance(TreeNode root, int a, int b) {20TreeNode m = lca(root, a, b);21return depth(m, a, 0) + depth(m, b, 0); // edges from the LCA down to each node22}23static int depth(TreeNode n, int v, int d) {24if (n == null) return -1;25if (n.val == v) return d;26int l = depth(n.left, v, d + 1);27return l >= 0 ? l : depth(n.right, v, d + 1);28}29static List<Integer> boundary(TreeNode root) { // anticlockwise from the root30List<Integer> out = new ArrayList<>();31if (root == null) return out;32if (!isLeaf(root)) out.add(root.val);33for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge34addLeaves(root, out); // all leaves, left to right35Deque<Integer> right = new ArrayDeque<>();36for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);37out.addAll(right); // right edge, bottom to top38return out;39}40static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }41static void addLeaves(TreeNode n, List<Integer> out) {42if (n == null) return;43if (isLeaf(n)) out.add(n.val);44addLeaves(n.left, out); addLeaves(n.right, out);45}
Live state
problem
Path sum
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L8 | problem=Path sum | Path sum: is there a root-to-leaf path adding up to 22? |
| 2 | L10 | remaining=17 | Take 5: 22 - 5 = 17 still to find. |
| 3 | L10 | remaining=13 | Take 4: 17 - 4 = 13 still to find. |
| 4 | L10 | remaining=2 | Take 11: 13 - 11 = 2 still to find. |
| 5 | L10 | remaining=-5 | Take 7: 2 - 7 = -5 still to find. |
| 6 | L11 | remaining=-5 | 7 is a leaf but -5 remains: this path does not work. |
| 7 | L10 | remaining=0 | Take 2: 2 - 2 = 0 still to find. |
| 8 | L11 | remaining=0 | 2 is a leaf and nothing remains: the path 5 → 4 → 11 → 2 adds up to 22. |
| 9 | L12 | result=true | true: 5 + 4 + 11 + 2 = 22. |
Lowest common ancestor basics
The **lowest common ancestor** (LCA) of two nodes p and q is the deepest node that has both of them in its subtree (a node counts as its own descendant). One recursive pass finds it: return the node itself if it is p or q; otherwise ask both subtrees. If both sides return something, this node is where the two paths split, so it is the LCA; if only one does, pass that result up.
For the tree “3 5 1 6 2 0 8 # # 7 4” the LCA of 7 and 4 is 2, and the LCA of 5 and 4 is 5 itself: node 5 is above 4, so the search stops at 5 and never looks below it. The animation prints the value returned by every call under its node.
1static TreeNode lca(TreeNode n, int p, int q) {2if (n == null || n.val == p || n.val == q) return n;3TreeNode l = lca(n.left, p, q), r = lca(n.right, p, q);4return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them5}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Path problems: root-to-leaf paths, path sum, lowest common ancestor, distance, boundary
Interactive · edit the inputbinary tree · 9 nodes
Path sum: is there a root-to-leaf path adding up to 22?
1static void paths(TreeNode n, List<Integer> path, List<String> out) {2if (n == null) return;3path.add(n.val); // choose4if (n.left == null && n.right == null) out.add(path.toString());5else { paths(n.left, path, out); paths(n.right, path, out); }6path.remove(path.size() - 1); // unchoose7}8static boolean hasPathSum(TreeNode n, int remaining) {9if (n == null) return false;10remaining -= n.val;11if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a root-to-leaf path12return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);13}14static TreeNode lca(TreeNode n, int a, int b) {15if (n == null || n.val == a || n.val == b) return n;16TreeNode l = lca(n.left, a, b), r = lca(n.right, a, b);17return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them18}19static int distance(TreeNode root, int a, int b) {20TreeNode m = lca(root, a, b);21return depth(m, a, 0) + depth(m, b, 0); // edges from the LCA down to each node22}23static int depth(TreeNode n, int v, int d) {24if (n == null) return -1;25if (n.val == v) return d;26int l = depth(n.left, v, d + 1);27return l >= 0 ? l : depth(n.right, v, d + 1);28}29static List<Integer> boundary(TreeNode root) { // anticlockwise from the root30List<Integer> out = new ArrayList<>();31if (root == null) return out;32if (!isLeaf(root)) out.add(root.val);33for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge34addLeaves(root, out); // all leaves, left to right35Deque<Integer> right = new ArrayDeque<>();36for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);37out.addAll(right); // right edge, bottom to top38return out;39}40static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }41static void addLeaves(TreeNode n, List<Integer> out) {42if (n == null) return;43if (isLeaf(n)) out.add(n.val);44addLeaves(n.left, out); addLeaves(n.right, out);45}
Live state
problem
Path sum
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L8 | problem=Path sum | Path sum: is there a root-to-leaf path adding up to 22? |
| 2 | L10 | remaining=17 | Take 5: 22 - 5 = 17 still to find. |
| 3 | L10 | remaining=13 | Take 4: 17 - 4 = 13 still to find. |
| 4 | L10 | remaining=2 | Take 11: 13 - 11 = 2 still to find. |
| 5 | L10 | remaining=-5 | Take 7: 2 - 7 = -5 still to find. |
| 6 | L11 | remaining=-5 | 7 is a leaf but -5 remains: this path does not work. |
| 7 | L10 | remaining=0 | Take 2: 2 - 2 = 0 still to find. |
| 8 | L11 | remaining=0 | 2 is a leaf and nothing remains: the path 5 → 4 → 11 → 2 adds up to 22. |
| 9 | L12 | result=true | true: 5 + 4 + 11 + 2 = 22. |
Distance between nodes
The path between two nodes in a tree goes up to their lowest common ancestor and down again, so distance(p, q) = depth(p) + depth(q) - 2 · depth(lca). Equivalently, add the depth of each node *measured from the LCA*.
For 6 and 4 in the same tree the LCA is 5; 6 is 1 edge below it and 4 is 2 edges below it (through 2), so the distance is 1 + 2 = 3 edges. The animation highlights the two arms in different colours.
1static int distance(TreeNode root, int p, int q) {2TreeNode m = lca(root, p, q);3return depthFrom(m, p, 0) + depthFrom(m, q, 0); // edges from the LCA down to each node4}5static TreeNode lca(TreeNode n, int p, int q) {6if (n == null || n.val == p || n.val == q) return n;7TreeNode l = lca(n.left, p, q), r = lca(n.right, p, q);8return l != null && r != null ? n : (l != null ? l : r);9}10static int depthFrom(TreeNode n, int v, int d) {11if (n == null) return -1;12if (n.val == v) return d;13int l = depthFrom(n.left, v, d + 1);14return l >= 0 ? l : depthFrom(n.right, v, d + 1);15}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Path problems: root-to-leaf paths, path sum, lowest common ancestor, distance, boundary
Interactive · edit the inputbinary tree · 9 nodes
Path sum: is there a root-to-leaf path adding up to 22?
1static void paths(TreeNode n, List<Integer> path, List<String> out) {2if (n == null) return;3path.add(n.val); // choose4if (n.left == null && n.right == null) out.add(path.toString());5else { paths(n.left, path, out); paths(n.right, path, out); }6path.remove(path.size() - 1); // unchoose7}8static boolean hasPathSum(TreeNode n, int remaining) {9if (n == null) return false;10remaining -= n.val;11if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a root-to-leaf path12return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);13}14static TreeNode lca(TreeNode n, int a, int b) {15if (n == null || n.val == a || n.val == b) return n;16TreeNode l = lca(n.left, a, b), r = lca(n.right, a, b);17return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them18}19static int distance(TreeNode root, int a, int b) {20TreeNode m = lca(root, a, b);21return depth(m, a, 0) + depth(m, b, 0); // edges from the LCA down to each node22}23static int depth(TreeNode n, int v, int d) {24if (n == null) return -1;25if (n.val == v) return d;26int l = depth(n.left, v, d + 1);27return l >= 0 ? l : depth(n.right, v, d + 1);28}29static List<Integer> boundary(TreeNode root) { // anticlockwise from the root30List<Integer> out = new ArrayList<>();31if (root == null) return out;32if (!isLeaf(root)) out.add(root.val);33for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge34addLeaves(root, out); // all leaves, left to right35Deque<Integer> right = new ArrayDeque<>();36for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);37out.addAll(right); // right edge, bottom to top38return out;39}40static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }41static void addLeaves(TreeNode n, List<Integer> out) {42if (n == null) return;43if (isLeaf(n)) out.add(n.val);44addLeaves(n.left, out); addLeaves(n.right, out);45}
Live state
problem
Path sum
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L8 | problem=Path sum | Path sum: is there a root-to-leaf path adding up to 22? |
| 2 | L10 | remaining=17 | Take 5: 22 - 5 = 17 still to find. |
| 3 | L10 | remaining=13 | Take 4: 17 - 4 = 13 still to find. |
| 4 | L10 | remaining=2 | Take 11: 13 - 11 = 2 still to find. |
| 5 | L10 | remaining=-5 | Take 7: 2 - 7 = -5 still to find. |
| 6 | L11 | remaining=-5 | 7 is a leaf but -5 remains: this path does not work. |
| 7 | L10 | remaining=0 | Take 2: 2 - 2 = 0 still to find. |
| 8 | L11 | remaining=0 | 2 is a leaf and nothing remains: the path 5 → 4 → 11 → 2 adds up to 22. |
| 9 | L12 | result=true | true: 5 + 4 + 11 + 2 = 22. |
Boundary traversal
The **boundary** of a tree, read anticlockwise from the root, is: the root, the **left edge** from top to bottom (without leaves), all the **leaves** from left to right, and the **right edge** from bottom to top (without leaves and without the root). For “20 8 22 4 12 # 25 # # 10 14” the result is [20, 8, 4, 10, 14, 25, 22].
Three details cause most bugs: leaves are added only once, by the leaf pass; the left edge follows the left child but falls back to the right child when there is no left one; and the right edge is collected top to bottom but output reversed.
1static List<Integer> boundary(TreeNode root) {2List<Integer> out = new ArrayList<>();3if (root == null) return out;4if (!isLeaf(root)) out.add(root.val);5for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge6addLeaves(root, out);7Deque<Integer> right = new ArrayDeque<>();8for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);9out.addAll(right); // right edge, bottom to top10return out;11}12static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }13static void addLeaves(TreeNode n, List<Integer> out) {14if (n == null) return;15if (isLeaf(n)) out.add(n.val);16addLeaves(n.left, out);17addLeaves(n.right, out);18}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Path problems: root-to-leaf paths, path sum, lowest common ancestor, distance, boundary
Interactive · edit the inputbinary tree · 9 nodes
Path sum: is there a root-to-leaf path adding up to 22?
1static void paths(TreeNode n, List<Integer> path, List<String> out) {2if (n == null) return;3path.add(n.val); // choose4if (n.left == null && n.right == null) out.add(path.toString());5else { paths(n.left, path, out); paths(n.right, path, out); }6path.remove(path.size() - 1); // unchoose7}8static boolean hasPathSum(TreeNode n, int remaining) {9if (n == null) return false;10remaining -= n.val;11if (n.left == null && n.right == null) return remaining == 0; // only a leaf ends a root-to-leaf path12return hasPathSum(n.left, remaining) || hasPathSum(n.right, remaining);13}14static TreeNode lca(TreeNode n, int a, int b) {15if (n == null || n.val == a || n.val == b) return n;16TreeNode l = lca(n.left, a, b), r = lca(n.right, a, b);17return l != null && r != null ? n : (l != null ? l : r); // both sides found one: n splits them18}19static int distance(TreeNode root, int a, int b) {20TreeNode m = lca(root, a, b);21return depth(m, a, 0) + depth(m, b, 0); // edges from the LCA down to each node22}23static int depth(TreeNode n, int v, int d) {24if (n == null) return -1;25if (n.val == v) return d;26int l = depth(n.left, v, d + 1);27return l >= 0 ? l : depth(n.right, v, d + 1);28}29static List<Integer> boundary(TreeNode root) { // anticlockwise from the root30List<Integer> out = new ArrayList<>();31if (root == null) return out;32if (!isLeaf(root)) out.add(root.val);33for (TreeNode n = root.left; n != null && !isLeaf(n); n = n.left != null ? n.left : n.right) out.add(n.val); // left edge34addLeaves(root, out); // all leaves, left to right35Deque<Integer> right = new ArrayDeque<>();36for (TreeNode n = root.right; n != null && !isLeaf(n); n = n.right != null ? n.right : n.left) right.push(n.val);37out.addAll(right); // right edge, bottom to top38return out;39}40static boolean isLeaf(TreeNode n) { return n.left == null && n.right == null; }41static void addLeaves(TreeNode n, List<Integer> out) {42if (n == null) return;43if (isLeaf(n)) out.add(n.val);44addLeaves(n.left, out); addLeaves(n.right, out);45}
Live state
problem
Path sum
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L8 | problem=Path sum | Path sum: is there a root-to-leaf path adding up to 22? |
| 2 | L10 | remaining=17 | Take 5: 22 - 5 = 17 still to find. |
| 3 | L10 | remaining=13 | Take 4: 17 - 4 = 13 still to find. |
| 4 | L10 | remaining=2 | Take 11: 13 - 11 = 2 still to find. |
| 5 | L10 | remaining=-5 | Take 7: 2 - 7 = -5 still to find. |
| 6 | L11 | remaining=-5 | 7 is a leaf but -5 remains: this path does not work. |
| 7 | L10 | remaining=0 | Take 2: 2 - 2 = 0 still to find. |
| 8 | L11 | remaining=0 | 2 is a leaf and nothing remains: the path 5 → 4 → 11 → 2 adds up to 22. |
| 9 | L12 | result=true | true: 5 + 4 + 11 + 2 = 22. |
Practice ladder
Now try it yourself
Easy
- Path Sum
- Binary Tree Paths
- Lowest Common Ancestor of a Binary Search Tree
Medium
- Lowest Common Ancestor of a Binary Tree
- Path Sum II
- Boundary of Binary Tree
- Path Sum III
Advanced
- All Nodes Distance K in Binary Tree
- Binary Tree Maximum Path Sum
Key takeaways
- Root-to-leaf paths use choose, recurse, unchoose; a tree has one path per leaf.
- A path sum must end at a leaf: reaching the target at an internal node does not count.
- The LCA is the first node whose two subtrees both report a target.
- distance(p, q) goes through the LCA: the two depths below it add.
- Boundary traversal is root, left edge, leaves, right edge reversed, with leaves added once.