Facebook Pixel

1026. Maximum Difference Between Node and Ancestor

Problem Description

You are given the root of a binary tree. Your task is to find the maximum absolute difference between any node and one of its ancestor nodes.

Specifically, you need to find the maximum value v where v = |a.val - b.val|, with the constraint that node a must be an ancestor of node b.

A node a is considered an ancestor of node b if:

  • a is the direct parent of b (any child of a equals b), OR
  • a is an indirect ancestor of b (any child of a is itself an ancestor of b)

In other words, node a must appear somewhere along the path from the root to node b (excluding b itself).

For example, if you have a path from root to leaf like: 10 -> 5 -> 3 -> 1, then:

  • 10 is an ancestor of 5, 3, and 1
  • 5 is an ancestor of 3 and 1
  • 3 is an ancestor of 1

The maximum difference could be between any such ancestor-descendant pair in the entire tree. Your goal is to return this maximum difference value.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves a binary tree, which is a special type of graph with nodes and edges (parent-child relationships).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure where each node has at most two children and there are no cycles.

DFS

  • Since we answered "yes" to the tree question, the flowchart leads us directly to DFS (Depth-First Search).

Why DFS is appropriate for this problem:

  • We need to track information (minimum and maximum values) along the path from root to each node
  • We must maintain the ancestor-descendant relationship, which naturally follows the recursive path traversal in DFS
  • For each node, we need to know all its ancestors' values to compute the maximum difference
  • DFS allows us to carry forward the accumulated information (min/max values seen so far) as we traverse down the tree

Conclusion: The flowchart correctly suggests using DFS for this tree-based problem where we need to maintain and update information along root-to-node paths to find the maximum ancestor-descendant difference.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to find the maximum difference between a node and its ancestor, a key observation is that for any node, the maximum difference with its ancestors will be either:

  • The difference between the node's value and the maximum ancestor value, or
  • The difference between the node's value and the minimum ancestor value

Why? Because the absolute difference |a - b| is maximized when a and b are as far apart as possible. So we only care about the extreme values (min and max) among all ancestors.

This leads to an important insight: instead of keeping track of all ancestor values as we traverse down the tree, we only need to maintain two values:

  • The minimum value seen so far along the path from root
  • The maximum value seen so far along the path from root

As we visit each node during our DFS traversal:

  1. We calculate the difference between the current node's value and both the min and max ancestor values
  2. We update our global answer if we find a larger difference
  3. We update the min and max values to include the current node's value
  4. We pass these updated min/max values to the children

This way, when we reach any node, we have all the information needed to compute the maximum possible difference with any of its ancestors. The beauty of this approach is that it reduces what could be an O(n²) problem (comparing every node with all its ancestors) to an O(n) solution where we visit each node exactly once.

The DFS pattern naturally maintains the ancestor-descendant relationship because the recursive call stack represents the path from root to the current node, and we're passing the accumulated min/max values down this path.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The solution implements a DFS traversal with the key optimization of tracking only the minimum and maximum values encountered along each path from root to node.

Implementation Details:

We define a helper function dfs(root, mi, mx) where:

  • root is the current node being processed
  • mi is the minimum value seen among all ancestors of the current node
  • mx is the maximum value seen among all ancestors of the current node

Algorithm Steps:

  1. Base Case: If the current node is None, we simply return as there's nothing to process.

  2. Calculate Differences: For the current node, we compute:

    • |mi - root.val|: difference between current node and minimum ancestor value
    • |mx - root.val|: difference between current node and maximum ancestor value
    • Update the global answer ans with the maximum of these differences
  3. Update Min/Max for Children: Before recursing to children, we update:

    • mi = min(mi, root.val): include current node's value in the minimum
    • mx = max(mx, root.val): include current node's value in the maximum

    This ensures that when we visit the children, they have access to all ancestor values including the current node.

  4. Recursive Calls: We recursively call dfs on both left and right children with the updated mi and mx values.

  5. Initialization: The traversal starts from the root with dfs(root, root.val, root.val). We initialize both mi and mx to root.val because at the root level, there are no ancestors yet.

Why This Works:

  • By passing down the min and max values, each node knows the range of all its ancestor values
  • The maximum difference for any ancestor-descendant pair must involve either the minimum or maximum ancestor value
  • The nonlocal ans declaration allows us to update the global maximum across all recursive calls
  • The time complexity is O(n) as we visit each node once, and space complexity is O(h) where h is the height of the tree due to the recursion stack

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate how the solution works.

Consider this binary tree:

       8
      / \
     3   10
    / \    \
   1   6   14
      / \   /
     4   7 13

We want to find the maximum difference between any node and its ancestor.

Step-by-step DFS traversal:

  1. Start at root (8)

    • Initialize: mi = 8, mx = 8 (no ancestors yet)
    • No differences to calculate (first node)
    • Pass to children: mi = 8, mx = 8
  2. Visit left child (3)

    • Current mi = 8, mx = 8 (from parent)
    • Calculate: |8 - 3| = 5, |8 - 3| = 5
    • Update ans = 5
    • Update for children: mi = min(8, 3) = 3, mx = max(8, 3) = 8
  3. Visit node (1)

    • Current mi = 3, mx = 8 (accumulated from path 8→3)
    • Calculate: |3 - 1| = 2, |8 - 1| = 7
    • Update ans = 7 (new maximum!)
    • Leaf node, backtrack
  4. Visit node (6)

    • Current mi = 3, mx = 8 (from path 8→3)
    • Calculate: |3 - 6| = 3, |8 - 6| = 2
    • ans remains 7
    • Update for children: mi = 3, mx = 8
  5. Continue with nodes (4) and (7)

    • Node 4: |3 - 4| = 1, |8 - 4| = 4, ans stays 7
    • Node 7: |3 - 7| = 4, |8 - 7| = 1, ans stays 7
  6. Visit right subtree starting at (10)

    • Current mi = 8, mx = 8 (from root)
    • Calculate: |8 - 10| = 2, |8 - 10| = 2
    • Update for children: mi = 8, mx = 10
  7. Visit node (14)

    • Current mi = 8, mx = 10 (from path 8→10)
    • Calculate: |8 - 14| = 6, |10 - 14| = 4
    • ans remains 7
  8. Visit node (13)

    • Current mi = 8, mx = 10 (from path 8→10→14)
    • Calculate: |8 - 13| = 5, |10 - 13| = 3
    • ans remains 7

Result: The maximum difference is 7, found between nodes 8 and 1 (where 8 is an ancestor of 1).

The key insight illustrated here is how we only track the minimum (3) and maximum (8) values along each path, rather than storing all ancestors. When we reached node 1, we could immediately calculate the maximum possible difference using these two values, finding our answer of 7.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import Optional
9
10class Solution:
11    def maxAncestorDiff(self, root: Optional[TreeNode]) -> int:
12        """
13        Find the maximum absolute difference between any node and its ancestor.
14      
15        Args:
16            root: The root of the binary tree
17          
18        Returns:
19            The maximum absolute difference between any node value and its ancestor value
20        """
21      
22        def dfs(node: Optional[TreeNode], min_val: int, max_val: int) -> None:
23            """
24            Traverse the tree while tracking min and max ancestor values.
25          
26            Args:
27                node: Current node being processed
28                min_val: Minimum value seen from root to current node's parent
29                max_val: Maximum value seen from root to current node's parent
30            """
31            # Base case: reached a null node
32            if node is None:
33                return
34          
35            # Update the maximum difference using current node and ancestor min/max
36            nonlocal max_difference
37            max_difference = max(
38                max_difference, 
39                abs(min_val - node.val),  # Difference with minimum ancestor
40                abs(max_val - node.val)   # Difference with maximum ancestor
41            )
42          
43            # Update min and max values including current node
44            current_min = min(min_val, node.val)
45            current_max = max(max_val, node.val)
46          
47            # Recursively process left and right subtrees
48            dfs(node.left, current_min, current_max)
49            dfs(node.right, current_min, current_max)
50      
51        # Initialize the maximum difference
52        max_difference = 0
53      
54        # Start DFS from root with root's value as both min and max
55        dfs(root, root.val, root.val)
56      
57        return max_difference
58
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    // Global variable to store the maximum ancestor difference
18    private int maxDifference;
19
20    /**
21     * Finds the maximum absolute difference between any node and its ancestors in a binary tree.
22     * 
23     * @param root The root of the binary tree
24     * @return The maximum absolute difference between any node value and its ancestor value
25     */
26    public int maxAncestorDiff(TreeNode root) {
27        // Initialize maxDifference to 0
28        maxDifference = 0;
29      
30        // Start DFS traversal with root value as both minimum and maximum
31        dfs(root, root.val, root.val);
32      
33        return maxDifference;
34    }
35
36    /**
37     * Performs depth-first search to find maximum ancestor difference.
38     * Tracks the minimum and maximum values encountered from root to current node.
39     * 
40     * @param node Current node being processed
41     * @param minAncestor Minimum value among all ancestors (including current path)
42     * @param maxAncestor Maximum value among all ancestors (including current path)
43     */
44    private void dfs(TreeNode node, int minAncestor, int maxAncestor) {
45        // Base case: if node is null, return
46        if (node == null) {
47            return;
48        }
49      
50        // Calculate the maximum difference between current node and the min/max ancestors
51        int currentDifference = Math.max(
52            Math.abs(minAncestor - node.val), 
53            Math.abs(maxAncestor - node.val)
54        );
55      
56        // Update the global maximum difference if current difference is larger
57        maxDifference = Math.max(maxDifference, currentDifference);
58      
59        // Update minimum and maximum values for the path including current node
60        minAncestor = Math.min(minAncestor, node.val);
61        maxAncestor = Math.max(maxAncestor, node.val);
62      
63        // Recursively process left subtree with updated min and max values
64        dfs(node.left, minAncestor, maxAncestor);
65      
66        // Recursively process right subtree with updated min and max values
67        dfs(node.right, minAncestor, maxAncestor);
68    }
69}
70
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    int maxAncestorDiff(TreeNode* root) {
15        int maxDifference = 0;
16      
17        // Define a recursive DFS function to traverse the tree
18        // Parameters: current node, minimum value seen so far, maximum value seen so far
19        function<void(TreeNode*, int, int)> dfs = [&](TreeNode* node, int minValue, int maxValue) {
20            // Base case: if node is null, return
21            if (!node) {
22                return;
23            }
24          
25            // Calculate the maximum difference between current node and ancestors
26            // Update the global maximum difference if needed
27            maxDifference = max({maxDifference, 
28                                abs(minValue - node->val), 
29                                abs(maxValue - node->val)});
30          
31            // Update the minimum and maximum values for the path
32            minValue = min(minValue, node->val);
33            maxValue = max(maxValue, node->val);
34          
35            // Recursively traverse left and right subtrees with updated min/max values
36            dfs(node->left, minValue, maxValue);
37            dfs(node->right, minValue, maxValue);
38        };
39      
40        // Start DFS from root with root's value as both initial min and max
41        dfs(root, root->val, root->val);
42      
43        return maxDifference;
44    }
45};
46
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Finds the maximum absolute difference between any ancestor and descendant node values in a binary tree
17 * @param root - The root node of the binary tree
18 * @returns The maximum absolute difference between any ancestor-descendant pair
19 */
20function maxAncestorDiff(root: TreeNode | null): number {
21    // Initialize the maximum difference found so far
22    let maxDifference: number = 0;
23  
24    /**
25     * Performs depth-first search to find the maximum ancestor-descendant difference
26     * @param node - Current node being processed
27     * @param minAncestor - Minimum value among all ancestors of the current node
28     * @param maxAncestor - Maximum value among all ancestors of the current node
29     */
30    const depthFirstSearch = (node: TreeNode | null, minAncestor: number, maxAncestor: number): void => {
31        // Base case: if node is null, return
32        if (!node) {
33            return;
34        }
35      
36        // Calculate the maximum difference between current node and its ancestors
37        // Update the global maximum if a larger difference is found
38        maxDifference = Math.max(
39            maxDifference, 
40            Math.abs(node.val - minAncestor), 
41            Math.abs(node.val - maxAncestor)
42        );
43      
44        // Update the minimum and maximum ancestor values to include the current node
45        const newMinAncestor: number = Math.min(minAncestor, node.val);
46        const newMaxAncestor: number = Math.max(maxAncestor, node.val);
47      
48        // Recursively process left and right subtrees with updated ancestor values
49        depthFirstSearch(node.left, newMinAncestor, newMaxAncestor);
50        depthFirstSearch(node.right, newMinAncestor, newMaxAncestor);
51    };
52  
53    // Start DFS from root with root's value as both min and max ancestor
54    depthFirstSearch(root, root!.val, root!.val);
55  
56    return maxDifference;
57}
58

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the algorithm performs constant time operations: calculating the maximum difference with abs(mi - root.val) and abs(mx - root.val), updating the minimum and maximum values, and making recursive calls. Since we visit all n nodes exactly once and perform O(1) work at each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity is determined by the recursive call stack used during the DFS traversal. In the worst case, the binary tree is completely unbalanced (e.g., a skewed tree where each node has only one child), resulting in a recursion depth equal to the number of nodes n. Therefore, the call stack can grow up to O(n) in the worst case. In the best case of a perfectly balanced tree, the space complexity would be O(log n), but we consider the worst-case scenario, giving us O(n) space complexity.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Including the Current Node in Difference Calculation

A common mistake is to update the min/max values before calculating the difference with the current node. This would incorrectly allow a node to be compared with itself.

Incorrect Implementation:

def dfs(node, min_val, max_val):
    if node is None:
        return
  
    # WRONG: Updating min/max before calculating differences
    current_min = min(min_val, node.val)
    current_max = max(max_val, node.val)
  
    # This might compare node.val with itself if node.val is the new min or max
    max_difference = max(
        max_difference,
        abs(current_min - node.val),  # Could be 0 if node.val is current_min
        abs(current_max - node.val)   # Could be 0 if node.val is current_max
    )

Correct Implementation:

def dfs(node, min_val, max_val):
    if node is None:
        return
  
    # CORRECT: Calculate differences first with ancestor min/max
    max_difference = max(
        max_difference,
        abs(min_val - node.val),  # min_val is strictly from ancestors
        abs(max_val - node.val)   # max_val is strictly from ancestors
    )
  
    # Then update min/max for children
    current_min = min(min_val, node.val)
    current_max = max(max_val, node.val)
  
    dfs(node.left, current_min, current_max)
    dfs(node.right, current_min, current_max)

2. Forgetting to Handle the Root Node Properly

Another pitfall is initializing the min/max values incorrectly at the root level. Some might initialize with extreme values like float('inf') and float('-inf').

Incorrect Initialization:

# WRONG: Using extreme values
dfs(root, float('inf'), float('-inf'))

This would create invalid comparisons at the root since there are no actual ancestors yet.

Correct Initialization:

# CORRECT: Start with root's value
dfs(root, root.val, root.val)

At the root level, we use root.val for both min and max because:

  • The root has no ancestors initially
  • When we process root's children, they will correctly see root.val as their only ancestor value
  • This ensures the first real comparison happens between root and its children

3. Using Global Variable Without Proper Declaration

Forgetting the nonlocal declaration when using a variable from the outer scope will cause an error.

Incorrect (causes UnboundLocalError):

def maxAncestorDiff(self, root):
    max_difference = 0
  
    def dfs(node, min_val, max_val):
        if node is None:
            return
      
        # ERROR: Trying to modify outer variable without nonlocal
        max_difference = max(max_difference, ...)  # UnboundLocalError

Correct:

def maxAncestorDiff(self, root):
    max_difference = 0
  
    def dfs(node, min_val, max_val):
        if node is None:
            return
      
        nonlocal max_difference  # Properly declare we're using outer variable
        max_difference = max(max_difference, ...)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More