124. Binary Tree Maximum Path Sum
On LeetCode ->Problem¶
Given a non-empty binary tree, find the maximum sum of any simple path. The path may start and end at any nodes and does not need to include the root.
Key trick¶
For each node, compute the maximum downward gain that can be extended by its parent, while separately updating the answer with a path joining both children through that node.
Trap¶
- A path passed to the parent can use only one child, although the completed path may use both.
- Negative child gains must be discarded.
- Initializing the answer to zero breaks all-negative trees.
- Preorder positions do not preserve child indices unless the tree is represented as a complete heap array.
- Recursive DFS may exceed Python's recursion limit on a highly skewed tree.
Why is it interesting?¶
It demonstrates tree DP with two related states: an extendable one-branch path and a completed path that may join two branches.
Python solution¶
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
best = root.val
sums = {None: 0} # child sums: node -> best sum downward
stack = [(root, False)]
while stack:
node, expanded = stack.pop()
if not expanded:
# Process both children before computing this node's downward sum
stack.append((node, True))
if node.left:
stack.append((node.left, False))
if node.right:
stack.append((node.right, False))
continue
# Negative branches can only reduce a path's sum.
left = max(sums[node.left], 0)
right = max(sums[node.right], 0)
# A completed path may connect both branches through this node.
best = max(best, node.val + left + right)
# A parent can extend only one branch without repeating this node.
sums[node] = node.val + max(left, right)
# Child sums are no longer needed.
if node.left:
del sums[node.left]
if node.right:
del sums[node.right]
return best
Time complexity: \(O(n)\).
Space complexity: \(O(h)\), where \(h\) is the tree height.
Comment on my solution¶
Your bottom-up DP idea is correct, but converting the tree to preorder values destroys its topology. The formulas 2 * i + 1 and 2 * i + 2 identify children only in a complete heap-style representation, not in a preorder traversal.
For the failing example, preorder produces:
The index calculation incorrectly treats 15 and 7 as children of 9, although they are children of 20.
The recurrence also needs to:
- Allow the current node without either child.
- Ignore negative child gains.
- Return only one child branch to the parent.
- Use both child branches only when updating the global answer.
# Wrong Answer: 37/97 testcases passed
# [-10,9,20,null,null,15,7]
# output: 34
# expected: 42
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
best = float("-inf")
preorder = []
def dfs(node):
nonlocal best
if not node:
return
# take max value among the leaves as a starting best
if not node.left and not node.right:
best = max(best, node.val)
preorder.append(node.val)
dfs(node.left)
dfs(node.right)
dfs(root)
n = len(preorder)
for i in range(n // 2 - 1, -1, -1):
l = 2*i + 1
r = 2*i + 2
l_val = preorder[l] if l < n else 0
r_val = preorder[r] if r < n else 0
left = preorder[i] + l_val
right = preorder[i] + r_val
combined = preorder[i] + l_val + r_val
preorder[i] = max(left, right)
best = max(best, left, right, combined)
return best
Extra¶
Differences between @dataclass and regular classes¶
- Why using the class defined with
@dataclassis not hashable and the other is?- When is better to define a class in one way over the other?
- Isn't it better anyway to use
id(...)instead of the instance? I mean in term of memory usage?- Should I update my library to define classes without
@dataclass?
Why is the dataclass unhashable?¶
By default, @dataclass generates value-based __eq__. Because its fields are mutable, Python disables __hash__ to prevent dictionary keys from changing after insertion.
A regular class inherits identity-based equality and hashing from object, so distinct instances remain hashable regardless of their fields.
When should each style be used?¶
- Use
@dataclassfor value-like objects where equality should compare fields. - Use
@dataclass(frozen=True)for immutable, hashable values. - Use a regular class for mutable nodes whose identity matters.
- Use
@dataclass(eq=False)when you want dataclass conveniences but identity-based equality and hashing.
For tree and graph nodes, identity usually matters more than structural equality.
Is using id(...) better for memory?¶
Usually not.
- An instance used as a dictionary key stores only a reference to the existing object.
id(node)produces an integer key, which must also be stored and may require a separate integer object.- Object keys are clearer and avoid concerns about ID reuse after an object is destroyed.
Prefer instances as keys when they have identity-based hashing. Use id(...) only when the class cannot be changed or made hashable.
Should the library remove @dataclass?¶
You can keep it, but disable generated equality:
@dataclass(eq=False)
class TreeNode:
val: int
left: Optional["TreeNode"] = None
right: Optional["TreeNode"] = None
next: Optional["TreeNode"] = None
This provides concise initialization and representation while restoring identity-based equality and hashing. It is a good default for mutable linked-list, tree, and graph nodes.
Example¶
from dataclasses import dataclass
# Regular class: identity-based equality and hashing.
class RegularNode:
def __init__(self, value):
self.value = value
a = RegularNode(1)
b = RegularNode(1)
print(a == b) # False: different objects
print(len({a, b})) # 2: both are valid set keys
# Mutable dataclass: value-based equality, but no hashing.
@dataclass
class MutablePoint:
x: int
y: int
p1 = MutablePoint(1, 2)
p2 = MutablePoint(1, 2)
print(p1 == p2) # True: fields are equal
try:
{p1}
except TypeError as error:
print(type(error).__name__) # TypeError
# Hashing would be unsafe because equality can change after mutation.
p1.x = 99
print(p1 == p2) # False
# Frozen dataclass: immutable, value-based, and hashable.
@dataclass(frozen=True)
class FrozenPoint:
x: int
y: int
f1 = FrozenPoint(1, 2)
f2 = FrozenPoint(1, 2)
print(f1 == f2) # True
print(len({f1, f2})) # 1: equal values are the same set key
# Dataclass node: mutable but identity-based.
@dataclass(eq=False)
class TreeNode:
value: int
left: "TreeNode | None" = None
right: "TreeNode | None" = None
n1 = TreeNode(1)
n2 = TreeNode(1)
print(n1 == n2) # False: different objects
print(len({n1, n2})) # 2: both are valid set keys