549. Binary Tree Longest Consecutive Sequence II


Problem Description

This problem provides us with the root of a binary tree and asks us to find the length of the longest consecutive path in that tree. The consecutive path we are looking for can be either increasing or decreasing in terms of node values, and every two consecutive nodes in this path should have values that differ by exactly one. To clarify, paths such as [1,2,3,4] (increasing) and [4,3,2,1] (decreasing) are valid. However, a path like [1,2,4,3] is invalid as the values do not differ by one. Additionally, it's important to note that the path does not need to follow parent-child relationships and could include a 'bounce', going from child to parent to another child (child-parent-child pattern).

Flowchart Walkthrough

To find the appropriate algorithm for solving LeetCode 549. Binary Tree Longest Consecutive Sequence II, let's walk through the decision process using the Flowchart. Here's a detailed analysis based on the graph nodes and edges:

  1. Is it a graph?

    • Yes: The binary tree is a type of graph.
  2. Is it a tree?

    • Yes: A binary tree is specifically a tree structure.
  3. DFS (Depth-First Search)

    • Since we've determined it is a tree, the next step suggests using a Depth-First Search (DFS) pattern.

The problem involves finding the longest consecutive sequence that can increase or decrease, requiring traversal where each path needs to be checked for its consecutive nature. DFS is particularly well-suited for such tree problems because it efficiently explores all possible paths and tracks conditions (like consecutive sequences) deeply before backtracking.

Conclusion: The flowchart guides us to utilize the Depth-First Search algorithm to solve the problem due to its efficiency in handling tree structures and ability to manage path-dependent conditions.

Intuition

The intuition behind this solution lies in using a recursive depth-first search (DFS) algorithm to traverse the tree and compute the increasing and decreasing consecutive paths. For every node, there can be four kinds of consecutive paths:

  1. Paths that only include this node.
  2. Paths that start from this node and extend to any node in the left subtree.
  3. Paths that start from this node and extend to any node in the right subtree.
  4. Paths that go through this node, meaning they start from the left subtree, include this node, and go to the right subtree.

To track these paths, for each node, we calculate two values: incr and decr, which represent the lengths of the longest increasing and decreasing paths ending at this node, respectively.

When we visit a node, we check its children. If either child's value is one more than the current node's value, we increment incr; if it's one less, we increment decr. We take these two values from both children and use them to update the incr and decr values of the current node.

The magic happens by considering not only the deepest path from this node to each child but also the possibility of continuing the path by "bouncing" from one child to the other through the current node, which effectively can increase the overall length of the consecutive path.

As we compute the incr and decr values for each node, we keep track of the global maximum length (ans) found so far. To get the maximum consecutive path that can pass through a node, we add the incr and decr values of this node but subtract 1 because the node itself is counted twice (once in each path). After the DFS traverses the whole tree, ans will store the length of the longest consecutive path.

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

Solution Approach

The solution uses a recursive depth-first search (DFS) to explore the binary tree and calculate the length of the longest consecutive path. Here's a step-by-step walkthrough of the implementation:

  1. Recursive Function Definition: A function dfs is defined, which takes a node of the tree as an argument and returns a tuple [incr, decr]. incr holds the maximum length of an increasing consecutive path ending at that node, and decr is the same for decreasing paths.

  2. Base Case: When dfs encounters a None (indicating the node being visited is non-existent or a leaf's child), it returns [0, 0] as there are no consecutive paths beyond this point.

  3. State Variables: The solution introduces a nonlocal variable ans to track the maximum length found during traversal.

  4. Child Nodes Analysis: Each call to dfs considers the current node's left and right child nodes. For each child, the function computes i1/d1 and i2/d2 which are tuples returned by the recursive dfs call on the left and right children, respectively.

  5. Update Increments/Decrements:

    • It then checks the value of the left child (if it exists), updating incr or decr based on whether the left child's value is one less or one more than the current node's value, respectively.
    • Similarly, checks are performed on the right child, updating incr and decr by comparing the values of the right child and the current node.
  6. Global Maximum Update: After calculating the incr and decr for both the left and right children, ans is updated by taking the sum of the current node's incr and decr minus one — as the current node is counted in both incr and decr and should only be counted once.

  7. Return Values: Finally, the function returns a tuple [incr, decr] for the current node, which signifies the maximum lengths of consecutive paths ending at this node (both increasing and decreasing).

  8. Result: After dfs is called on the root, ans contains the length of the longest consecutive path in the tree, which is then returned as the result of the longestConsecutive function.

The algorithm effectively scans the entire tree only once, ensuring an efficient solution with a time complexity of O(n), where n is the number of nodes in the tree. By considering each node and its potential paths (both increasing and decreasing), as well as potential "bounces" between children, we ensure no possible paths are missed.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through an example to illustrate the solution approach. Consider the following binary tree:

      3
     / \
    2   4
   /     \
  1       5

Here's how the recursive DFS would process this tree:

  1. Starting at the Root (Node 3): The initial call to DFS is made on the root (node 3). At this point, ans is initialized to 0.

  2. Recursive Calls: The dfs function will make recursive calls to the left (node 2) and right (node 4) children:

    • Left Child (Node 2):
      • The left child has the value 2, which is less by 1 than its parent (node 3), so for node 3, decr = 2.
      • It recursively calls dfs on node 2's left child (node 1).
      • Node 1 is also less by 1 than node 2, so now for node 2, decr = 2.
      • Node 1 has no children, so the recursive call would return [0, 0].
      • The decr from node 2 is now combined with node 3's decr to update ans, if necessary, to ans = decr_2 + decr_3 - 1.
    • Right Child (Node 4):
      • The right child has the value 4, which is more by 1 than its parent (node 3), so for node 3, incr = 2.
      • It recursively calls dfs on node 4's right child (node 5).
      • Node 5 is more by 1 than node 4, so for node 4, incr = 2.
      • The incr from node 4 is now combined with node 3's incr to update ans, if necessary, to ans = incr_4 + incr_3 - 1.
  3. Update Global Maximum (ans): Since both left and right children of node 3 form consecutive sequences, we calculate the maximum sum of decr and incr.

    • From left child's decr (2 from node 2) and right child's incr (2 from node 4), we get 2 + 2 - 1 = 3 for node 3, which means including node 3, there is a path of length 3 that goes from node 5 to node 3 to node 2.
  4. Finalizing the Result: After the full traversal,

    • We have computed incr and decr for all nodes.
    • We've also computed ans at each node, which is the maximum value obtained by adding incr and decr and subtracting 1 (to account for the current node being included in both sequences).
    • Since the tree was recursively traversed, ans holds the maximum length of the longest consecutive path after taking all nodes into consideration.

Based on our example, the longest consecutive path is 3 (which is the path [2, 3, 4] or [4, 3, 2]), and this is output by the longestConsecutive function as the DFS is executed from the root of the tree to all its children and their subsequent descendants.

Solution Implementation

1# Definition for a binary tree node.
2class TreeNode:
3    def __init__(self, val=0, left=None, right=None):
4        self.val = val
5        self.left = left
6        self.right = right
7
8
9class Solution:
10    def longestConsecutive(self, root: TreeNode) -> int:
11        # Recursive depth-first search to find the longest consecutive path
12        def dfs(node):
13            if node is None:
14                return [0, 0]  # Base case: return 0's for the length of increasing and decreasing sequences
15
16            nonlocal max_length  # Use nonlocal keyword to modify the non-local max_length variable
17            incr_length = decr_length = 1  # Initialize lengths of increasing and decreasing paths
18          
19            # Perform DFS on the left and right children
20            left_incr, left_decr = dfs(node.left)
21            right_incr, right_decr = dfs(node.right)
22          
23            # Check for consecutive increments or decrements on the left child
24            if node.left:
25                if node.left.val + 1 == node.val:
26                    incr_length = left_incr + 1
27                elif node.left.val - 1 == node.val:
28                    decr_length = left_decr + 1
29          
30            # Check for consecutive increments or decrements on the right child
31            if node.right:
32                if node.right.val + 1 == node.val:
33                    incr_length = max(incr_length, right_incr + 1)
34                elif node.right.val - 1 == node.val:
35                    decr_length = max(decr_length, right_decr + 1)
36          
37            # Update the max_length considering both increasing and decreasing paths
38            max_length = max(max_length, incr_length + decr_length - 1)
39            return [incr_length, decr_length]
40
41        max_length = 0  # Initialize the maximum length of consecutive sequence
42        dfs(root)  # Start DFS from the root
43        return max_length  # Return the maximum length found
44
1class Solution {
2    private int longestLength;
3
4    // Function to start the longest consecutive sequence process
5    public int longestConsecutive(TreeNode root) {
6        longestLength = 0;
7        dfs(root);
8        return longestLength;
9    }
10
11    // Perform a Depth First Search on the tree
12    private int[] dfs(TreeNode node) {
13        if (node == null) {
14            return new int[] {0, 0};
15        }
16      
17        int incrementing = 1; // Length of incrementing sequence ending at this node
18        int decrementing = 1; // Length of decrementing sequence ending at this node
19
20        // Recurse left
21        int[] leftSubtree = dfs(node.left);
22        // Recurse right
23        int[] rightSubtree = dfs(node.right);
24      
25        // Check left child
26        if (node.left != null) {
27            if (node.left.val + 1 == node.val) {
28                incrementing = leftSubtree[0] + 1;
29            }
30            if (node.left.val - 1 == node.val) {
31                decrementing = leftSubtree[1] + 1;
32            }
33        }
34      
35        // Check right child
36        if (node.right != null) {
37            if (node.right.val + 1 == node.val) {
38                incrementing = Math.max(incrementing, rightSubtree[0] + 1);
39            }
40            if (node.right.val - 1 == node.val) {
41                decrementing = Math.max(decrementing, rightSubtree[1] + 1);
42            }
43        }
44      
45        // Update longestLength if the current sequence is the longest
46        // -1 is to not double count the current node in both incrementing and decrementing sequences
47        longestLength = Math.max(longestLength, incrementing + decrementing - 1);
48      
49        // Return the length of the longest incrementing and decrementing sequence ending at this node
50        return new int[] {incrementing, decrementing};
51    }
52}
53
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 longestStreak;
15
16    // Function that starts the process and returns the longest consecutive sequence length
17    int longestConsecutive(TreeNode* root) {
18        longestStreak = 0;
19        dfs(root);
20        return longestStreak;
21    }
22
23    // Helper function to perform DFS and calculate the consecutive sequence length
24    vector<int> dfs(TreeNode* root) {
25        // Base case: if the node is null, return {0, 0} as there is no sequence
26        if (!root) return {0, 0};
27
28        // Initialize the length of the increasing and decreasing sequences to 1 (the root itself)
29        int increaseLength = 1, decreaseLength = 1;
30
31        // Recursively call dfs for the left and right subtrees
32        vector<int> leftSequence = dfs(root->left);
33        vector<int> rightSequence = dfs(root->right);
34
35        // Process left child
36        if (root->left) {
37            // Check if it's consecutively increasing
38            if (root->left->val + 1 == root->val) increaseLength = leftSequence[0] + 1;
39            // Check if it's consecutively decreasing
40            if (root->left->val - 1 == root->val) decreaseLength = leftSequence[1] + 1;
41        }
42
43        // Process right child
44        if (root->right) {
45            // Check if it's consecutively increasing
46            if (root->right->val + 1 == root->val) increaseLength = max(increaseLength, rightSequence[0] + 1);
47            // Check if it's consecutively decreasing
48            if (root->right->val - 1 == root->val) decreaseLength = max(decreaseLength, rightSequence[1] + 1);
49        }
50
51        // Update the longest streak result by taking the maximum sum of increasing and
52        // decreasing lengths from the current node minus 1 (to avoid double-counting the node itself)
53        longestStreak = max(longestStreak, increaseLength + decreaseLength - 1);
54
55        // Return a pair of the longest increasing and decreasing sequences starting from the current node
56        return {increaseLength, decreaseLength};
57    }
58};
59
1// Definition for a binary tree node.
2class TreeNode {
3    val: number;
4    left: TreeNode | null;
5    right: TreeNode | null;
6
7    constructor(val: number = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
8        this.val = val;
9        this.left = left;
10        this.right = right;
11    }
12}
13
14let longestStreak: number;
15
16// Function that starts the process and returns the longest consecutive sequence length
17function longestConsecutive(root: TreeNode | null): number {
18    longestStreak = 0;
19    dfs(root);
20    return longestStreak;
21}
22
23// Helper function to perform DFS and calculate the consecutive sequence length
24function dfs(root: TreeNode | null): number[] {
25    // Base case: if the node is null, return [0, 0] as there is no sequence
26    if (!root) return [0, 0];
27
28    // Initialize the length of the increasing and decreasing sequences to 1 (the root itself)
29    let increaseLength: number = 1, decreaseLength: number = 1;
30
31    // Recursively call dfs for the left and right subtrees
32    const leftSequence: number[] = dfs(root.left);
33    const rightSequence: number[] = dfs(root.right);
34
35    // Process left child
36    if (root.left) {
37        // Check if it's consecutively increasing
38        if (root.left.val + 1 === root.val) {
39            increaseLength = leftSequence[0] + 1;
40        }
41        // Check if it's consecutively decreasing
42        if (root.left.val - 1 === root.val) {
43            decreaseLength = leftSequence[1] + 1;
44        }
45    }
46
47    // Process right child
48    if (root.right) {
49        // Check if it's consecutively increasing
50        if (root.right.val + 1 === root.val) {
51            increaseLength = Math.max(increaseLength, rightSequence[0] + 1);
52        }
53        // Check if it's consecutively decreasing
54        if (root.right.val - 1 === root.val) {
55            decreaseLength = Math.max(decreaseLength, rightSequence[1] + 1);
56        }
57    }
58
59    // Update the longest streak result by taking the maximum sum of increasing and
60    // decreasing lengths from the current node minus 1 (to avoid double-counting the node)
61    longestStreak = Math.max(longestStreak, increaseLength + decreaseLength - 1);
62
63    // Return a pair of the longest increasing and decreasing sequences starting from the current node
64    return [increaseLength, decreaseLength];
65}
66

Time and Space Complexity

Time Complexity

The provided code executes a depth-first search (DFS) on a binary tree. For every node, it calculates the longest consecutive sequence that can be formed both increasing and decreasing. The decision to increment or decrement the consecutive count, or to reset it, is done at every node. This leads to each node being visited exactly once.

Therefore, the time complexity of the DFS is O(N), where N is the number of nodes in the tree. This is because the function processes each node a single time without revisiting anything.

Space Complexity

The space complexity of the algorithm includes the space used by the recursive call stack during the DFS traversal as well as the space used for storing the variables in each recursive call.

In the worst case, the height of the binary tree may be O(N) (in case of a skewed tree where each node has only one child), which would imply O(N) recursive calls stack space would be used. For balanced trees, the average case height would be O(log N) leading to an average case space complexity of O(log N).

Hence, the space complexity is O(N) in the worst case, but in the average case for a balanced tree, it would be O(log N).

The ans variable used to store the maximum length of the consecutive sequence doesn't significantly affect the space complexity as it is a single integer value.

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

How many times is a tree node visited in a depth first search?


Recommended Readings

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