title: Binary Tree Maximum Path Sum
slug: binary-tree-maximum-path-sum
difficulty: hard
leetcode_id: 124
leetcode_url: https://leetcode.com/problems/binary-tree-maximum-path-sum/
categories:
  - trees
  - dynamic-programming
  - recursion
patterns:
  - dfs
  - dynamic-programming

function_signature: "def max_path_sum(root: TreeNode) -> int:"

test_cases:
  visible:
    - input: { root: [1, 2, 3] }
      expected: 6
    - input: { root: [-10, 9, 20, null, null, 15, 7] }
      expected: 42
  hidden:
    - input: { root: [1] }
      expected: 1
    - input: { root: [-3] }
      expected: -3
    - input: { root: [2, -1] }
      expected: 2
    - input: { root: [-2, -1] }
      expected: -1
    - input: { root: [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, null, 1] }
      expected: 48
    - input: { root: [1, -2, -3, 1, 3, -2, null, -1] }
      expected: 3

description: |
  A **path** in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence **at most once**. Note that the path does not need to pass through the root.

  The **path sum** of a path is the sum of the node's values in the path.

  Given the `root` of a binary tree, return *the maximum **path sum** of any **non-empty** path*.

constraints: |
  - The number of nodes in the tree is in the range `[1, 3 * 10^4]`
  - `-1000 <= Node.val <= 1000`

examples:
  - input: "root = [1,2,3]"
    output: "6"
    explanation: "The optimal path is 2 -> 1 -> 3 with a path sum of 2 + 1 + 3 = 6."
  - input: "root = [-10,9,20,null,null,15,7]"
    output: "42"
    explanation: "The optimal path is 15 -> 20 -> 7 with a path sum of 15 + 20 + 7 = 42."

explanation:
  intuition: |
    Imagine each node in the tree as a potential **turning point** or **apex** of a path. A path can go through a node in three ways:

    1. Enter from the left subtree, stop at the node
    2. Enter from the right subtree, stop at the node
    3. Enter from the left, pass through the node, and exit through the right (forming an "inverted V" shape)

    The key insight is that **every valid path has exactly one apex node** — the topmost node where the path changes direction or ends. At this apex, the path can include contributions from both the left and right subtrees.

    Think of it like finding the best hiking trail in a mountain range: at each peak (node), you consider the best trail coming up from the left valley, the best trail from the right valley, and whether combining them through this peak creates the overall best route.

    The challenge is that we need to track two things simultaneously:
    - The **best complete path** found anywhere in the tree (can turn at any node)
    - The **best "arm"** extending from each node upward (for its parent to potentially use)

  approach: |
    We solve this using **DFS with Post-order Traversal**, computing the maximum gain from each subtree while tracking the global maximum path sum.

    **Step 1: Define the recursive return value**

    - The helper function returns the **maximum gain** that can be obtained by extending a path from the current node upward to its parent
    - This means the path can only go in one direction (either left or right child, not both)
    - We return `0` if including any child would decrease the sum (negative contribution)

    &nbsp;

    **Step 2: Compute gains from children**

    - Recursively get the maximum gain from the left subtree: `left_gain = max(0, dfs(node.left))`
    - Recursively get the maximum gain from the right subtree: `right_gain = max(0, dfs(node.right))`
    - Taking `max(0, ...)` means we only include a subtree if it adds positive value

    &nbsp;

    **Step 3: Update the global maximum**

    - At each node, compute the path sum if this node is the apex: `node.val + left_gain + right_gain`
    - This path goes left-child → node → right-child (the full "V" shape)
    - Update the global maximum if this path is better than any previously found

    &nbsp;

    **Step 4: Return the maximum single-direction gain**

    - Return `node.val + max(left_gain, right_gain)` to the parent
    - We can only extend in one direction since the parent will connect from above
    - This value represents the best "arm" starting at this node and going downward

    &nbsp;

    **Step 5: Handle the base case**

    - If the node is `None`, return `0` (no contribution to the path)

  common_pitfalls:
    - title: Confusing the Two Path Types
      description: |
        There are two distinct concepts in this problem:

        1. **Complete path** — can turn at any node, used to update the global maximum
        2. **Extendable arm** — can only go one direction, returned to the parent

        A common mistake is returning `node.val + left_gain + right_gain` to the parent. This would be invalid because the parent can't connect to a path that already branches both ways — paths don't fork.
      wrong_approach: "Return the complete path sum to parent"
      correct_approach: "Return only the best single-direction gain to parent"

    - title: Forgetting Negative Values
      description: |
        Node values can be negative (`-1000 <= Node.val <= 1000`). This means:

        - The maximum path might consist of a single node
        - We should not force including children if they decrease the sum
        - Taking `max(0, child_gain)` lets us "prune" negative contributions

        For example, in a tree `[-3]`, the answer is `-3`, not `0`. But if a child returns `-5`, we take `max(0, -5) = 0` to exclude it.
      wrong_approach: "Always include child contributions"
      correct_approach: "Use max(0, child_gain) to optionally exclude negative paths"

    - title: Not Initializing Global Maximum Correctly
      description: |
        Since all node values could be negative, initializing `max_sum = 0` is wrong.

        With `root = [-3, -2, -1]`, the answer should be `-1` (the single node with the least negative value), not `0`.

        Initialize with negative infinity or the root's value.
      wrong_approach: "Initialize max_sum = 0"
      correct_approach: "Initialize max_sum = float('-inf')"

  key_takeaways:
    - "**Post-order DFS pattern**: Process children before the parent — essential when the parent's computation depends on children's results"
    - "**Two-value tracking**: Maintain a global maximum for the answer while returning a different value (single-direction gain) for recursion"
    - "**Optional inclusion**: Use `max(0, value)` to optionally exclude negative contributions without special-casing"
    - "**Path apex insight**: Every tree path has exactly one 'top' node where direction changes — iterate over all possible apex positions"

  time_complexity: "O(n). We visit each node exactly once during the DFS traversal."
  space_complexity: "O(h) where h is the height of the tree. The recursion stack can go as deep as the tree height, which is O(log n) for balanced trees and O(n) for skewed trees."

solutions:
  - approach_name: DFS with Post-order Traversal
    is_optimal: true
    code: |
      class TreeNode:
          def __init__(self, val=0, left=None, right=None):
              self.val = val
              self.left = left
              self.right = right

      def max_path_sum(root: TreeNode) -> int:
          # Track the global maximum path sum
          max_sum = float('-inf')

          def dfs(node: TreeNode) -> int:
              nonlocal max_sum

              # Base case: null nodes contribute nothing
              if not node:
                  return 0

              # Get max gain from left and right subtrees
              # Use max(0, ...) to ignore negative contributions
              left_gain = max(0, dfs(node.left))
              right_gain = max(0, dfs(node.right))

              # Path sum if current node is the apex (turning point)
              # This path goes: left subtree -> node -> right subtree
              path_through_node = node.val + left_gain + right_gain

              # Update global maximum if this path is better
              max_sum = max(max_sum, path_through_node)

              # Return max gain if we extend path to parent
              # Can only go one direction (left OR right, not both)
              return node.val + max(left_gain, right_gain)

          dfs(root)
          return max_sum
    explanation: |
      **Time Complexity:** O(n) — Each node is visited exactly once.

      **Space Complexity:** O(h) — Recursion stack depth equals tree height.

      The DFS function serves a dual purpose: it updates the global maximum considering the current node as an apex, while returning the maximum single-direction gain for the parent to use. This elegant design handles all path configurations in a single traversal.

  - approach_name: DFS with Tuple Return
    is_optimal: false
    code: |
      class TreeNode:
          def __init__(self, val=0, left=None, right=None):
              self.val = val
              self.left = left
              self.right = right

      def max_path_sum(root: TreeNode) -> int:
          def dfs(node: TreeNode) -> tuple[int, int]:
              """
              Returns (max_path_in_subtree, max_extendable_arm)
              - max_path_in_subtree: best complete path found in this subtree
              - max_extendable_arm: best path starting at node going downward
              """
              if not node:
                  # No path possible, arm contributes nothing
                  return float('-inf'), 0

              # Recurse on children
              left_max_path, left_arm = dfs(node.left)
              right_max_path, right_arm = dfs(node.right)

              # Best arm extending from this node (can skip children if negative)
              best_arm = node.val + max(0, left_arm, right_arm)

              # Best path with this node as apex
              path_through_node = node.val + max(0, left_arm) + max(0, right_arm)

              # Best path in entire subtree rooted here
              best_path = max(left_max_path, right_max_path, path_through_node)

              return best_path, max(0, best_arm)

          max_path, _ = dfs(root)
          return max_path

    explanation: |
      **Time Complexity:** O(n) — Each node is visited exactly once.

      **Space Complexity:** O(h) — Recursion stack depth equals tree height.

      This variant avoids using a global variable by returning two values: the best complete path found in the subtree and the best extendable arm. It's functionally equivalent to the optimal solution but makes the dual-tracking more explicit. Some prefer this for avoiding nonlocal variables.
