LeetCode 113 - Path Sum II
This problem asks us to find all root-to-leaf paths in a binary tree where the sum of the node values equals a given targetSum. A binary tree is provided through the root node, and every node contains an integer value. We are also given an integer targetSum.
Difficulty: 🟡 Medium
Topics: Backtracking, Tree, Depth-First Search, Binary Tree
Solution
Problem Understanding
This problem asks us to find all root-to-leaf paths in a binary tree where the sum of the node values equals a given targetSum.
A binary tree is provided through the root node, and every node contains an integer value. We are also given an integer targetSum. Our task is to explore every valid path that starts at the root and ends at a leaf node, then return only the paths whose total sum equals targetSum.
The important detail is that we are not looking for any arbitrary sequence of nodes. The path must satisfy two strict conditions:
- It must begin at the root.
- It must end at a leaf node, meaning a node with no left or right child.
The output should contain the values of nodes in each valid path, not references to the nodes themselves. Since multiple valid paths may exist, we return a list of lists.
For example, in the first example:
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
With targetSum = 22, there are two valid root-to-leaf paths:
5 → 4 → 11 → 2 = 22
5 → 8 → 4 → 5 = 22
So the answer becomes:
[[5,4,11,2],[5,8,4,5]]
The constraints tell us that the tree may contain up to 5000 nodes, which is large enough that inefficient repeated traversal could become expensive. Node values and target sums can also be negative, meaning we cannot rely on pruning strategies such as stopping early when a sum exceeds the target. Since negative values are allowed, a larger sum can later decrease, and vice versa.
Several edge cases are important to consider upfront. The tree may be empty (root = None), in which case there are no paths. A tree may contain only one node, meaning the root is also a leaf. Negative values can create paths where sums fluctuate unexpectedly. Finally, multiple valid paths may exist, and we must ensure they are stored independently instead of accidentally modifying previously saved paths.
Approaches
Brute Force Approach
A straightforward brute-force solution is to first generate every root-to-leaf path in the tree and then calculate the sum of each path afterward.
To do this, we would recursively traverse the tree and maintain a path list. Whenever we reach a leaf node, we would store the full path. After collecting all root-to-leaf paths, we would iterate over them and compute their sums, selecting only the ones that equal targetSum.
This approach works because it exhaustively checks every possible root-to-leaf path. Since every valid answer must appear in this collection, correctness is guaranteed.
However, it performs unnecessary work. We store paths even if they obviously will not contribute to the answer, and we repeatedly compute sums after traversal instead of maintaining a running total during recursion.
Optimal Approach, Depth-First Search with Backtracking
The key insight is that we can evaluate path sums while traversing the tree, rather than after collecting every path.
A depth-first search (DFS) naturally fits this problem because we are exploring root-to-leaf paths. During traversal, we maintain:
- The current path
- The remaining sum needed to reach
targetSum
At each node:
- Add the current node value to the path.
- Subtract the node value from the remaining target.
- If we reach a leaf node and the remaining target becomes zero, we have found a valid path.
- Backtrack after exploring children by removing the node from the path.
Backtracking is essential because the same path list is reused across recursive calls. Without removing nodes after exploration, sibling branches would incorrectly inherit previous values.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(N × H) | O(N × H) | Generates all root-to-leaf paths, then recomputes sums |
| Optimal | O(N) | O(H) excluding output | DFS computes sums during traversal and backtracks efficiently |
Here:
Nis the number of nodes.His the tree height.
Algorithm Walkthrough
- Start a depth-first traversal from the root node.
We recursively explore the tree because each recursive call naturally represents moving deeper into a path.
2. Maintain a current_path list.
Every time we visit a node, append its value to this list. This tracks the exact sequence of nodes from the root to the current position. 3. Track the remaining target.
Instead of repeatedly summing the path, subtract the current node value from the remaining target sum.
If the original target is 22 and the current node value is 5, the remaining target becomes 17.
4. Check whether the node is a leaf.
A node is a leaf if both left and right are None.
If we are at a leaf and the remaining target equals the node value, then the entire path sum equals targetSum.
5. Save a copy of the path.
We append current_path[:] to the result list.
A copy is necessary because backtracking will later modify current_path. Without copying, all stored results would point to the same mutable list.
6. Recurse into the left and right children.
Continue exploring deeper paths while passing the updated remaining target. 7. Backtrack after recursion.
Remove the current node from current_path using pop().
This restores the state before returning to the parent node, allowing sibling branches to start with the correct path.
Why it works
The algorithm works because DFS explores every root-to-leaf path exactly once. At every recursive step, current_path accurately represents the path from the root to the current node, and the remaining target correctly tracks how much sum is still needed. When a leaf node is reached, a path is added only if the remaining sum condition is satisfied. Backtracking guarantees that state does not leak between sibling branches, ensuring correctness.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
result = []
current_path = []
def dfs(node: Optional[TreeNode], remaining_sum: int) -> None:
if not node:
return
current_path.append(node.val)
if (
not node.left
and not node.right
and remaining_sum == node.val
):
result.append(current_path[:])
else:
dfs(node.left, remaining_sum - node.val)
dfs(node.right, remaining_sum - node.val)
current_path.pop()
dfs(root, targetSum)
return result
The implementation begins by creating two structures: result, which stores all valid paths, and current_path, which tracks the active traversal path.
The nested dfs() function performs recursive traversal. If the node is None, recursion stops immediately since there is no path to explore.
When visiting a node, its value is appended to current_path. This ensures the path always represents the current root-to-node sequence.
The leaf node condition is particularly important. We only consider a path valid if:
- The node has no children.
- The remaining sum exactly equals the node's value.
If both conditions are met, we append a copy of the path to result.
Otherwise, recursion continues into both subtrees with an updated remaining target.
Finally, current_path.pop() performs backtracking. This restores the path to its previous state before returning to the parent call.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func pathSum(root *TreeNode, targetSum int) [][]int {
var result [][]int
var currentPath []int
var dfs func(node *TreeNode, remainingSum int)
dfs = func(node *TreeNode, remainingSum int) {
if node == nil {
return
}
currentPath = append(currentPath, node.Val)
if node.Left == nil &&
node.Right == nil &&
remainingSum == node.Val {
pathCopy := make([]int, len(currentPath))
copy(pathCopy, currentPath)
result = append(result, pathCopy)
} else {
dfs(node.Left, remainingSum-node.Val)
dfs(node.Right, remainingSum-node.Val)
}
currentPath = currentPath[:len(currentPath)-1]
}
dfs(root, targetSum)
return result
}
The Go implementation closely mirrors the Python solution but requires explicit slice handling.
Instead of Python list copying with [:], Go requires allocating a new slice using make() and copying elements using copy(). This is necessary because slices are references to underlying arrays. Without creating a copy, later backtracking modifications would affect stored paths.
Go also uses nil checks for missing nodes, which corresponds to Python's None.
Integer overflow is not a concern here because the constraints remain comfortably within Go's integer range.
Worked Examples
Example 1
Input
root = [5,4,8,11,null,13,4,7,2,null,null,5,1]
targetSum = 22
The traversal proceeds as follows:
| Current Node | Current Path | Remaining Sum Before Visit | Remaining Sum After Visit | Action |
|---|---|---|---|---|
| 5 | [5] | 22 | 17 | Continue |
| 4 | [5,4] | 17 | 13 | Continue |
| 11 | [5,4,11] | 13 | 2 | Continue |
| 7 | [5,4,11,7] | 2 | -5 | Leaf, invalid |
| 2 | [5,4,11,2] | 2 | 0 | Valid path |
| 8 | [5,8] | 17 | 9 | Continue |
| 13 | [5,8,13] | 9 | -4 | Leaf, invalid |
| 4 | [5,8,4] | 9 | 5 | Continue |
| 5 | [5,8,4,5] | 5 | 0 | Valid path |
| 1 | [5,8,4,1] | 5 | 4 | Leaf, invalid |
Final result:
[[5,4,11,2],[5,8,4,5]]
Example 2
Input
root = [1,2,3]
targetSum = 5
| Current Node | Current Path | Remaining Sum |
|---|---|---|
| 1 | [1] | 4 |
| 2 | [1,2] | 2 |
| 3 | [1,3] | 1 |
Neither leaf node satisfies the required sum.
Final result:
[]
Example 3
Input
root = [1,2]
targetSum = 0
| Current Node | Current Path | Remaining Sum |
|---|---|---|
| 1 | [1] | -1 |
| 2 | [1,2] | -3 |
No valid path exists.
Final result:
[]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(N) | Every node is visited exactly once |
| Space | O(H) | Recursive call stack and path storage proportional to tree height, excluding output |
The DFS traversal touches each node only once, making the time complexity linear in the number of nodes.
The auxiliary space complexity is O(H) because recursion depth and current_path both depend on tree height. In the worst case of a skewed tree, this becomes O(N). The output list itself is not counted in auxiliary space since it is required by the problem.
Test Cases
# Helper TreeNode class for testing
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
solution = Solution()
# Example 1
root1 = TreeNode(
5,
TreeNode(
4,
TreeNode(
11,
TreeNode(7),
TreeNode(2)
)
),
TreeNode(
8,
TreeNode(13),
TreeNode(
4,
TreeNode(5),
TreeNode(1)
)
)
)
assert solution.pathSum(root1, 22) == [
[5, 4, 11, 2],
[5, 8, 4, 5]
] # multiple valid paths
# Example 2
root2 = TreeNode(1, TreeNode(2), TreeNode(3))
assert solution.pathSum(root2, 5) == [] # no valid path
# Example 3
root3 = TreeNode(1, TreeNode(2))
assert solution.pathSum(root3, 0) == [] # impossible target
# Empty tree
assert solution.pathSum(None, 10) == [] # no nodes
# Single node valid
root4 = TreeNode(5)
assert solution.pathSum(root4, 5) == [[5]] # root equals target
# Single node invalid
root5 = TreeNode(5)
assert solution.pathSum(root5, 10) == [] # root does not match target
# Negative values
root6 = TreeNode(
-2,
None,
TreeNode(-3)
)
assert solution.pathSum(root6, -5) == [
[-2, -3]
] # negative numbers
# Multiple overlapping paths
root7 = TreeNode(
1,
TreeNode(2, TreeNode(3)),
TreeNode(2, None, TreeNode(3))
)
assert solution.pathSum(root7, 6) == [
[1, 2, 3],
[1, 2, 3]
] # duplicate valid paths
# Deep skewed tree
root8 = TreeNode(
1,
TreeNode(
2,
TreeNode(
3,
TreeNode(4)
)
)
)
assert solution.pathSum(root8, 10) == [
[1, 2, 3, 4]
] # skewed tree path
| Test | Why |
|---|---|
| Example 1 | Verifies multiple valid paths |
| Example 2 | Verifies no valid path exists |
| Example 3 | Checks impossible target |
| Empty tree | Ensures None input works |
| Single node valid | Root is also a leaf |
| Single node invalid | Leaf path does not match target |
| Negative values | Confirms negative sums are handled correctly |
| Duplicate paths | Ensures independent path storage |
| Deep skewed tree | Tests recursion depth and single-path traversal |
Edge Cases
Empty Tree
An empty tree (root = None) is a common source of bugs because recursion may attempt to access properties on a null object. Our implementation immediately returns from DFS when encountering None, which naturally produces an empty result list.
Single Node Tree
When the tree contains only one node, the root is also a leaf. A naive implementation might forget to treat this as a complete path. Our leaf condition explicitly checks whether both children are missing, ensuring single-node trees are handled correctly.
Negative Node Values
Because node values may be negative, early pruning is unsafe. For example, a path sum may temporarily exceed the target and later decrease due to negative numbers. Our implementation avoids incorrect pruning entirely and explores all root-to-leaf paths, guaranteeing correctness.
Mutable Path Bugs
A subtle issue in backtracking problems is accidentally storing references to a mutable path instead of copies. If we appended current_path directly, later pop() operations would modify already stored answers. By saving current_path[:] in Python and using copy() in Go, each result remains independent and correct.