Facebook Pixel

687. Longest Univalue Path

Problem Description

You are given the root of a binary tree. Your task is to find the length of the longest path in the tree where all nodes along the path have the same value. This path can go through any nodes in the tree and doesn't have to pass through the root.

The length of a path is measured by the number of edges between nodes, not the number of nodes. For example, a path with 3 nodes has a length of 2 (two edges connecting the three nodes).

A "univalue path" means all nodes in the path must have identical values. The path can:

  • Start and end at any nodes in the tree
  • Go through parent-child relationships
  • Form a path that goes down from a node to its left subtree and then back up through the same node to its right subtree

For instance, if you have a tree where a parent node with value 5 has two children both with value 5, you could form a path from the left child, through the parent, to the right child. This would be a valid univalue path of length 2.

The solution uses a depth-first search approach where for each node, it calculates:

  1. The longest univalue path extending downward from that node through its left subtree
  2. The longest univalue path extending downward from that node through its right subtree
  3. The potential longest path passing through that node (connecting paths from both subtrees)

The function tracks the maximum path length found anywhere in the tree and returns this as the final answer.

Flowchart Walkthrough

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

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles. Trees are acyclic connected graphs.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure. We have a root node and need to traverse through parent-child relationships.

DFS

  • Following the flowchart, when we have a tree structure, the natural conclusion is to use DFS (Depth-First Search).

Why DFS is appropriate for this problem:

  • We need to explore each path in the tree to find the longest univalue path
  • For each node, we need to check paths extending through its left and right subtrees
  • We need to recursively process subtrees and combine their results
  • The problem requires us to track information as we traverse down the tree and pass information back up (the length of univalue paths)

Conclusion: The flowchart correctly leads us to use DFS for this tree-based problem. The DFS approach allows us to:

  1. Recursively explore each subtree
  2. Calculate the longest univalue path extending downward from each node
  3. Combine results from left and right subtrees to find paths passing through each node
  4. Track the global maximum across all possible paths in the tree
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When looking at this problem, we need to find the longest path where all nodes have the same value. The key insight is that any such path in a tree has a "peak" node - the highest node in the path where the path potentially branches down into both left and right subtrees.

Think about how a univalue path can be formed in a tree. For any node, a path passing through it can extend:

  • Down to its left subtree (if the left child has the same value)
  • Down to its right subtree (if the right child has the same value)
  • Or both, creating a path that goes from left subtree → current node → right subtree

This suggests we need to know, for each node, what's the longest univalue path that extends straight down from it. But here's the catch: when we're calculating the answer (the longest path in the entire tree), we can use both left and right extensions. However, when we're returning a value to use in parent calculations, we can only return one branch - either left or right - because a path must be continuous without branching.

Let's think through an example. If we have a node with value 5, and both its children also have value 5:

  • The left child might have a univalue path of length 2 extending down from it
  • The right child might have a univalue path of length 1 extending down from it
  • The path through this node would be: 2 + 1 + 1 + 1 = 5 edges (left path + edge to left child + edge to right child + right path)
  • But when reporting up to this node's parent, we can only say "the longest path going down from me is max(2+1, 1+1) = 3"

This dual nature - calculating the full path through a node versus what we can extend upward - is why we need to track a global maximum (ans) separately from the return value of our recursive function. The recursive function returns the longest single branch extending down, while we update ans with the longest path that might pass through the current node using both branches.

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

Solution Approach

The solution implements a DFS traversal with a clever tracking mechanism to find the longest univalue path. Let's break down the implementation:

Core DFS Function Design

We design a function dfs(root) that returns the longest univalue path length extending downward from the root node as one endpoint. This is crucial - the function doesn't return the longest path through the node, but rather the longest single branch extending down.

Base Case

If root is None, we return 0 since there's no path from a null node.

Recursive Calls

We recursively call dfs(root.left) and dfs(root.right) to get:

  • l: the longest univalue path extending down from the left child
  • r: the longest univalue path extending down from the right child

Extending Paths Based on Value Matching

Here's where we check if we can extend the paths:

  • If root.left exists and root.left.val == root.val, then we can extend the left path by 1 edge (from root to left child), so l = l + 1
  • Otherwise, we can't extend through the left side, so l = 0
  • Similarly for the right side with r

Updating the Global Maximum

The key insight: while we can only return one branch upward, we can use both branches to form a path through the current node. We update ans = max(ans, l + r). This l + r represents:

  • l edges extending down through the left subtree
  • r edges extending down through the right subtree
  • These connect through the current node to form a complete path

Return Value

The function returns max(l, r) - the longest single branch we can extend upward to the parent. We can't return both branches because that would create a fork, not a valid path.

Using Nonlocal Variable

The solution uses nonlocal ans to maintain the global maximum across all recursive calls. This allows each node to potentially update the answer while still returning the appropriate value for its parent's calculations.

Overall Algorithm Flow

  1. Initialize ans = 0 to track the global maximum
  2. Call dfs(root) to traverse the entire tree
  3. At each node, calculate potential paths through both children
  4. Update the global maximum with the full path through this node
  5. Return the longest single branch for parent calculations
  6. After traversal completes, return ans

This approach ensures we consider all possible univalue paths in the tree while maintaining the correct recursive structure for tree traversal.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

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

Consider this binary tree:

        1
       / \
      4   5
     / \   \
    4   4   5

We want to find the longest univalue path. Let's trace through the DFS:

Step 1: Start DFS from root (value = 1)

  • First, we traverse all the way down to the leaves

Step 2: Process leaf node (leftmost 4)

  • dfs(node_4_leftmost) returns 0 (no children)
  • No path through this node, ans remains 0

Step 3: Process leaf node (middle 4)

  • dfs(node_4_middle) returns 0 (no children)
  • No path through this node, ans remains 0

Step 4: Process node with value 4 (left child of root)

  • l = dfs(left_child) = 0 (from leftmost 4)
  • r = dfs(right_child) = 0 (from middle 4)
  • Both children have value 4, same as parent:
    • l = 0 + 1 = 1 (can extend left by 1 edge)
    • r = 0 + 1 = 1 (can extend right by 1 edge)
  • Update ans = max(0, 1 + 1) = 2 (path from leftmost 4 → parent 4 → middle 4)
  • Return max(1, 1) = 1 to parent

Step 5: Process leaf node (rightmost 5)

  • dfs(node_5_leaf) returns 0 (no children)
  • No path through this node, ans remains 2

Step 6: Process node with value 5 (right child of root)

  • l = dfs(left_child) = 0 (no left child)
  • r = dfs(right_child) = 0 (from rightmost 5)
  • Right child has value 5, same as parent:
    • l = 0 (no left child)
    • r = 0 + 1 = 1 (can extend right by 1 edge)
  • Update ans = max(2, 0 + 1) = 2 (doesn't improve our answer)
  • Return max(0, 1) = 1 to parent

Step 7: Process root node (value = 1)

  • l = dfs(left_child) = 1 (from the 4-valued subtree)
  • r = dfs(right_child) = 1 (from the 5-valued subtree)
  • Check children values:
    • Left child (4) ≠ root (1), so l = 0
    • Right child (5) ≠ root (1), so r = 0
  • Update ans = max(2, 0 + 0) = 2
  • Return max(0, 0) = 0

Final Answer: 2

The longest univalue path has length 2, consisting of the path through the three nodes with value 4 (leftmost 4 → parent 4 → middle 4), which has 2 edges.

Key observations from this walkthrough:

  1. The function returns the longest single branch extending down, not the full path through a node
  2. When both children match the parent's value, we can form a path using both branches (updating ans with l + r)
  3. When returning to the parent, we only report the longer single branch (max(l, r))
  4. The global variable ans tracks the maximum path found anywhere in the tree

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 longestUnivaluePath(self, root: Optional[TreeNode]) -> int:
12        """
13        Find the length of the longest path where each node in the path has the same value.
14        The path doesn't need to pass through the root.
15      
16        Args:
17            root: The root of the binary tree
18          
19        Returns:
20            The length of the longest univalue path (number of edges)
21        """
22        # Initialize the global maximum path length
23        self.max_path_length = 0
24      
25        def calculate_univalue_length(node: Optional[TreeNode]) -> int:
26            """
27            Calculate the longest univalue path starting from the current node
28            and going down to one of its children.
29          
30            Args:
31                node: Current node being processed
32              
33            Returns:
34                The longest univalue path length extending from this node to one child
35            """
36            # Base case: if node is None, return 0
37            if node is None:
38                return 0
39          
40            # Recursively calculate the univalue length for left and right subtrees
41            left_length = calculate_univalue_length(node.left)
42            right_length = calculate_univalue_length(node.right)
43          
44            # If left child exists and has the same value, extend the left path
45            # Otherwise, reset to 0
46            left_arrow_length = left_length + 1 if node.left and node.left.val == node.val else 0
47          
48            # If right child exists and has the same value, extend the right path
49            # Otherwise, reset to 0
50            right_arrow_length = right_length + 1 if node.right and node.right.val == node.val else 0
51          
52            # Update the global maximum with the path passing through current node
53            # (connecting left and right paths)
54            self.max_path_length = max(self.max_path_length, left_arrow_length + right_arrow_length)
55          
56            # Return the longer single direction path for parent node to use
57            return max(left_arrow_length, right_arrow_length)
58      
59        # Start the DFS traversal from root
60        calculate_univalue_length(root)
61      
62        # Return the maximum path length found
63        return self.max_path_length
64
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 track the maximum univalue path length
18    private int maxPathLength;
19
20    /**
21     * Finds the length of the longest path where each node has the same value.
22     * The path can go through any node in the tree but doesn't need to pass through the root.
23     * 
24     * @param root The root node of the binary tree
25     * @return The length of the longest univalue path (number of edges)
26     */
27    public int longestUnivaluePath(TreeNode root) {
28        maxPathLength = 0;
29        calculateUnivaluePath(root);
30        return maxPathLength;
31    }
32
33    /**
34     * Helper method that recursively calculates the longest univalue path from each node.
35     * Returns the length of the longest single-direction path (either left or right) 
36     * that can be extended upward to the parent.
37     * 
38     * @param node The current node being processed
39     * @return The length of the longest univalue path that can extend to parent
40     */
41    private int calculateUnivaluePath(TreeNode node) {
42        // Base case: null node contributes 0 to the path
43        if (node == null) {
44            return 0;
45        }
46      
47        // Recursively get the longest univalue path from left and right subtrees
48        int leftPath = calculateUnivaluePath(node.left);
49        int rightPath = calculateUnivaluePath(node.right);
50      
51        // Check if left child has same value as current node
52        // If yes, extend the left path by 1; otherwise, reset to 0
53        int leftExtension = 0;
54        if (node.left != null && node.left.val == node.val) {
55            leftExtension = leftPath + 1;
56        }
57      
58        // Check if right child has same value as current node
59        // If yes, extend the right path by 1; otherwise, reset to 0
60        int rightExtension = 0;
61        if (node.right != null && node.right.val == node.val) {
62            rightExtension = rightPath + 1;
63        }
64      
65        // Update global maximum with the path that goes through current node
66        // (connecting both left and right extensions)
67        maxPathLength = Math.max(maxPathLength, leftExtension + rightExtension);
68      
69        // Return the longer single-direction path that can extend to parent
70        return Math.max(leftExtension, rightExtension);
71    }
72}
73
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 longestUnivaluePath(TreeNode* root) {
15        int maxPathLength = 0;
16      
17        // Helper function to calculate the longest univalue path using DFS
18        // Returns the length of the longest univalue path starting from the current node
19        // and going down through either left or right subtree
20        auto calculatePath = [&](this auto&& calculatePath, TreeNode* node) -> int {
21            // Base case: if node is null, return 0
22            if (!node) {
23                return 0;
24            }
25          
26            // Recursively get the longest path from left and right children
27            int leftPath = calculatePath(node->left);
28            int rightPath = calculatePath(node->right);
29          
30            // Check if left child exists and has the same value as current node
31            // If yes, extend the path by 1; otherwise, reset to 0
32            leftPath = (node->left && node->left->val == node->val) ? leftPath + 1 : 0;
33          
34            // Check if right child exists and has the same value as current node
35            // If yes, extend the path by 1; otherwise, reset to 0
36            rightPath = (node->right && node->right->val == node->val) ? rightPath + 1 : 0;
37          
38            // Update the maximum path length considering the path through current node
39            // The path through current node connects left and right subtrees
40            maxPathLength = max(maxPathLength, leftPath + rightPath);
41          
42            // Return the longer path that can be extended upward to parent
43            // We can only extend one branch (either left or right) to the parent
44            return max(leftPath, rightPath);
45        };
46      
47        // Start DFS traversal from root
48        calculatePath(root);
49      
50        return maxPathLength;
51    }
52};
53
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 length of the longest path between any two nodes in a binary tree
17 * where each node in the path has the same value (univalue path).
18 * The path does not need to pass through the root.
19 * 
20 * @param root - The root node of the binary tree
21 * @returns The length of the longest univalue path
22 */
23function longestUnivaluePath(root: TreeNode | null): number {
24    // Global variable to track the maximum path length found
25    let maxPathLength: number = 0;
26  
27    /**
28     * Depth-first search helper function that returns the longest univalue path
29     * starting from the current node and going down to one of its children.
30     * 
31     * @param currentNode - The current node being processed
32     * @returns The length of the longest univalue path going down from this node
33     */
34    const calculateUnivaluePath = (currentNode: TreeNode | null): number => {
35        // Base case: if node is null, return 0
36        if (!currentNode) {
37            return 0;
38        }
39      
40        // Recursively calculate the longest univalue paths from left and right children
41        let leftPathLength: number = calculateUnivaluePath(currentNode.left);
42        let rightPathLength: number = calculateUnivaluePath(currentNode.right);
43      
44        // If left child exists and has the same value, extend the left path by 1
45        // Otherwise, reset the left path to 0
46        leftPathLength = currentNode.left && currentNode.left.val === currentNode.val 
47            ? leftPathLength + 1 
48            : 0;
49      
50        // If right child exists and has the same value, extend the right path by 1
51        // Otherwise, reset the right path to 0
52        rightPathLength = currentNode.right && currentNode.right.val === currentNode.val 
53            ? rightPathLength + 1 
54            : 0;
55      
56        // Update the global maximum with the path that goes through current node
57        // (connecting left and right paths)
58        maxPathLength = Math.max(maxPathLength, leftPathLength + rightPathLength);
59      
60        // Return the longer single path (either left or right) for parent node to use
61        return Math.max(leftPathLength, rightPathLength);
62    };
63  
64    // Start the DFS traversal from the root
65    calculateUnivaluePath(root);
66  
67    // Return the maximum path length found
68    return maxPathLength;
69}
70

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:

  • Checking if the node is null
  • Recursive calls to left and right children
  • Comparing values with children nodes
  • Updating the maximum path length
  • Returning the maximum of left and right paths

Since we visit each of the n nodes exactly once and perform O(1) operations 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 tree could be 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. This would require O(n) space on the call stack.

In the best case of a perfectly balanced tree, the recursion depth would be O(log n), but we consider the worst-case scenario for space complexity analysis.

Additionally, the algorithm uses only a constant amount of extra space for variables (ans, l, r), which doesn't affect the overall space complexity.

Therefore, the space complexity is O(n).

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

Common Pitfalls

Pitfall 1: Confusing Path Length with Node Count

A frequent mistake is returning or calculating the number of nodes instead of the number of edges. Remember that path length is defined as the number of edges, not nodes. A path with 3 nodes has a length of 2.

Incorrect approach:

# Wrong: Counting nodes instead of edges
if node.left and node.left.val == node.val:
    left_arrow_length = left_length + 1  # This counts nodes
# Then later using this directly as path length

Correct approach: The solution correctly handles this by adding 1 for each edge when extending paths, and the final left_arrow_length + right_arrow_length correctly represents the total number of edges.

Pitfall 2: Incorrectly Handling the Return Value

Many implementations mistakenly return the maximum path through a node instead of the maximum single-direction path from that node.

Incorrect approach:

def calculate_univalue_length(node):
    # ... calculation logic ...
    # Wrong: Returning the full path through the node
    return left_arrow_length + right_arrow_length

Correct approach:

def calculate_univalue_length(node):
    # ... calculation logic ...
    # Update global max with full path through node
    self.max_path_length = max(self.max_path_length, left_arrow_length + right_arrow_length)
    # But return only the longest single direction for parent to use
    return max(left_arrow_length, right_arrow_length)

Pitfall 3: Not Resetting Path Length When Values Don't Match

A critical error is forgetting to reset the path length to 0 when a child node has a different value than its parent.

Incorrect approach:

# Wrong: Always extending the path regardless of value
left_arrow_length = left_length + 1 if node.left else 0
# This doesn't check if values match!

Correct approach:

# Correct: Only extend if child exists AND values match
left_arrow_length = left_length + 1 if node.left and node.left.val == node.val else 0

Pitfall 4: Using Global Variables Incorrectly

Using a regular variable instead of instance variable or nonlocal can cause scoping issues.

Incorrect approach:

def longestUnivaluePath(self, root):
    max_path_length = 0  # Local variable
  
    def calculate_univalue_length(node):
        # This will cause UnboundLocalError
        max_path_length = max(max_path_length, left_arrow_length + right_arrow_length)

Correct approaches:

# Option 1: Using instance variable (as shown in the solution)
self.max_path_length = 0

# Option 2: Using nonlocal
def longestUnivaluePath(self, root):
    max_path_length = 0
  
    def calculate_univalue_length(node):
        nonlocal max_path_length
        max_path_length = max(max_path_length, left_arrow_length + right_arrow_length)

Pitfall 5: Edge Case of Single Node or Empty Tree

Forgetting to handle edge cases where the tree is empty or contains only one node.

The solution correctly handles these cases:

  • Empty tree (root is None): The base case returns 0, and self.max_path_length remains 0
  • Single node: Both left and right lengths are 0, resulting in a maximum path of 0 (which is correct since a single node has no edges)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More