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 ofb
(any child ofa
equalsb
), ORa
is an indirect ancestor ofb
(any child ofa
is itself an ancestor ofb
)
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 of5
,3
, and1
5
is an ancestor of3
and1
3
is an ancestor of1
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.
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:
- We calculate the difference between the current node's value and both the min and max ancestor values
- We update our global answer if we find a larger difference
- We update the min and max values to include the current node's value
- 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 processedmi
is the minimum value seen among all ancestors of the current nodemx
is the maximum value seen among all ancestors of the current node
Algorithm Steps:
-
Base Case: If the current node is
None
, we simply return as there's nothing to process. -
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
-
Update Min/Max for Children: Before recursing to children, we update:
mi = min(mi, root.val)
: include current node's value in the minimummx = 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.
-
Recursive Calls: We recursively call
dfs
on both left and right children with the updatedmi
andmx
values. -
Initialization: The traversal starts from the root with
dfs(root, root.val, root.val)
. We initialize bothmi
andmx
toroot.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 EvaluatorExample 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:
-
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
- Initialize:
-
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
- Current
-
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
- Current
-
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
- Current
-
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
- Node 4:
-
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
- Current
-
Visit node (14)
- Current
mi = 8, mx = 10
(from path 8→10) - Calculate:
|8 - 14| = 6
,|10 - 14| = 4
ans
remains 7
- Current
-
Visit node (13)
- Current
mi = 8, mx = 10
(from path 8→10→14) - Calculate:
|8 - 13| = 5
,|10 - 13| = 3
ans
remains 7
- Current
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, ...)
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!