Facebook Pixel

2246. Longest Path With Different Adjacent Characters

Problem Description

You are given a tree with n nodes numbered from 0 to n - 1, where node 0 is the root. The tree structure is represented by an array parent of size n, where parent[i] indicates the parent of node i. Since node 0 is the root, parent[0] == -1.

Additionally, you have a string s of length n, where s[i] represents the character assigned to node i.

Your task is to find the length of the longest path in the tree such that no two adjacent nodes on the path have the same character assigned to them.

A path in a tree is a sequence of nodes where each adjacent pair of nodes in the sequence has an edge between them. The length of a path is the number of nodes in the path.

For example, if you have a tree where node 0 has character 'a', node 1 (child of 0) has character 'b', and node 2 (child of 1) has character 'a', then the path from node 0 to node 2 would have length 3, and since adjacent nodes have different characters ('a' -> 'b' -> 'a'), this would be a valid path.

The goal is to find the maximum possible length among all valid paths in the tree where adjacent nodes have different characters.

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 explicitly states we have a tree, which is a special type of graph (connected, undirected, and acyclic). The tree is represented using a parent array where nodes have parent-child relationships.

Is it a tree?

  • Yes: The problem clearly states "You are given a tree (i.e. a connected, undirected graph that has no cycles)". We have n nodes numbered from 0 to n-1 with node 0 as the root, and the structure is defined by the parent array.

DFS

  • Yes: Since we're dealing with a tree structure and need to explore paths from root to leaves while tracking the longest valid path, DFS is the appropriate choice.

Conclusion: The flowchart leads us directly to using DFS (Depth-First Search) for this problem.

This makes sense because:

  1. We need to traverse the tree from the root to explore all possible paths
  2. For each node, we need to calculate the longest valid path that passes through it
  3. DFS allows us to recursively explore each subtree and combine results from child nodes
  4. The tree structure guarantees no cycles, making DFS straightforward to implement
  5. We can use DFS to compute the longest path ending at each node and combine paths from different subtrees when characters are different
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we think about finding the longest path in a tree with the constraint that adjacent nodes must have different characters, we need to consider what makes a valid path and how paths can be formed in a tree.

In a tree, any path can be visualized as either going straight down from an ancestor to a descendant, or as a path that goes up from one descendant, passes through a common ancestor, and then goes down to another descendant. The key insight is that every path has a "highest" node - the node closest to the root that the path passes through.

For each node, we can think of it as potentially being this "highest" node in some path. If a node is the highest point in a path, then the path consists of:

  1. A downward path from this node through one of its children (if any)
  2. Another downward path from this node through a different child (if any)

The total length would be the sum of these two longest valid downward paths plus the node itself.

This suggests a bottom-up approach using DFS. For each node, we want to:

  • Calculate the longest valid path that starts at this node and goes downward through each child
  • Keep track of the two longest such paths (since we can combine at most two branches)
  • Update our global answer if combining these paths creates a longer valid path

The constraint about adjacent nodes having different characters simply means that when we extend a path from a node to its child, we can only do so if s[node] != s[child].

By using DFS, we can process the tree from the leaves up to the root. For each node, we:

  1. Recursively get the longest downward path from each child
  2. If the characters are different, we can extend that path by including the current node
  3. Track the longest path we can make by joining paths from different children through the current node
  4. Return the longest single downward path from this node (to be used by its parent)

This way, we consider every possible path in the tree by considering every node as a potential "turning point" or highest node in the path.

Learn more about Tree, Depth-First Search, Graph and Topological Sort patterns.

Solution Approach

The implementation uses tree-shaped dynamic programming with DFS to solve this problem efficiently.

Step 1: Build the Tree Structure

First, we construct an adjacency list g from the parent array. Since parent[i] tells us the parent of node i, we iterate through all nodes (except the root) and add each node to its parent's children list:

g = defaultdict(list)
for i in range(1, len(parent)):
    g[parent[i]].append(i)

Step 2: DFS Traversal

We define a recursive DFS function that processes each node and returns the length of the longest valid path going downward from that node.

For each node i, the function:

  1. Initializes mx = 0 to track the longest downward path from node i
  2. Iterates through all children j in g[i]
  3. Recursively calls dfs(j) to get the longest path from child j
  4. Adds 1 to include the edge from i to j: x = dfs(j) + 1

Step 3: Handle the Character Constraint

When processing each child j, we check if s[i] != s[j] (parent and child have different characters):

  • If true, the path from i through j is valid
  • We can potentially combine this path with another valid path from i through a different child
  • Update the global answer: ans = max(ans, mx + x) where mx is the longest path seen so far from i, and x is the current path through j
  • Update mx = max(mx, x) to track the longest single downward path from i

Step 4: Path Combination Logic

The key insight is in how we combine paths:

  • mx represents the longest valid downward path from node i seen so far
  • x represents the current valid downward path through child j
  • mx + x represents a complete path that goes:
    • Up from some descendant through a previous child to node i (length mx)
    • Then down from node i through child j to another descendant (length x)

Step 5: Return the Result

The function returns mx, which is the longest single downward path from the current node. This value is used by the parent node in its calculations.

Finally, we add 1 to the answer (ans + 1) because:

  • ans tracks the sum of path lengths (edges) from two branches
  • We need to add 1 for the connecting node itself to get the total number of nodes in the path

The algorithm efficiently considers all possible paths by treating each node as a potential "turning point" where two downward paths meet, ensuring we find the globally longest valid path.

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 small example to illustrate the solution approach.

Consider a tree with 4 nodes and the following structure:

  • parent = [-1, 0, 0, 1]
  • s = "abac"

This gives us the tree:

    0 (a)
   / \
  1(b) 2(a)
  |
  3(c)

Step 1: Build adjacency list

  • g[0] = [1, 2] (node 0 has children 1 and 2)
  • g[1] = [3] (node 1 has child 3)
  • g[2] = [] (node 2 is a leaf)
  • g[3] = [] (node 3 is a leaf)

Step 2: DFS from root (node 0)

Starting dfs(0):

  • Initialize mx = 0, ans = 0

  • Process child 1:

    • Call dfs(1):
      • Initialize mx = 0 for node 1
      • Process child 3:
        • Call dfs(3): returns 0 (leaf node)
        • x = 0 + 1 = 1
        • Check s[1] != s[3]: 'b' != 'c' ✓ (valid)
        • Update ans = max(0, 0 + 1) = 1
        • Update mx = max(0, 1) = 1
      • Return mx = 1
    • Back in dfs(0): x = 1 + 1 = 2
    • Check s[0] != s[1]: 'a' != 'b' ✓ (valid)
    • Update ans = max(1, 0 + 2) = 2
    • Update mx = max(0, 2) = 2
  • Process child 2:

    • Call dfs(2): returns 0 (leaf node)
    • x = 0 + 1 = 1
    • Check s[0] != s[2]: 'a' != 'a' ✗ (invalid - same character)
    • Don't update ans or mx
  • Return mx = 2

Step 3: Final answer

  • ans = 2 (sum of edges in longest path)
  • Return ans + 1 = 3 (number of nodes)

The longest valid path is 3 → 1 → 0, which has 3 nodes with characters 'c' → 'b' → 'a', where all adjacent pairs have different characters.

The algorithm correctly identified this by:

  1. Finding the path from node 1 down to node 3 (length 1)
  2. Extending it through node 1 to node 0 (total length 2)
  3. Converting from edge count (2) to node count (3)

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4class Solution:
5    def longestPath(self, parent: List[int], s: str) -> int:
6        # Build adjacency list representation of the tree
7        children = defaultdict(list)
8        for node_id in range(1, len(parent)):
9            parent_node = parent[node_id]
10            children[parent_node].append(node_id)
11      
12        # Variable to track the maximum path length found
13        self.max_path_length = 0
14      
15        def dfs(current_node: int) -> int:
16            """
17            Depth-first search to find the longest path from current_node downward.
18          
19            Returns the longest valid path length starting from current_node
20            and going down to any descendant.
21            """
22            # Track the longest valid path from this node downward
23            longest_chain = 0
24          
25            # Explore all children of the current node
26            for child_node in children[current_node]:
27                # Get the longest path from the child node
28                child_chain_length = dfs(child_node) + 1
29              
30                # Only consider this path if characters are different
31                if s[current_node] != s[child_node]:
32                    # Update global maximum by considering path through current node
33                    # connecting two branches (longest_chain + child_chain_length)
34                    self.max_path_length = max(self.max_path_length, 
35                                               longest_chain + child_chain_length)
36                    # Update the longest single chain from current node
37                    longest_chain = max(longest_chain, child_chain_length)
38          
39            return longest_chain
40      
41        # Start DFS from the root node (node 0)
42        dfs(0)
43      
44        # Return the result (add 1 to convert from edge count to node count)
45        return self.max_path_length + 1
46
1class Solution {
2    // Adjacency list to represent the tree structure
3    private List<Integer>[] adjacencyList;
4    // Input string containing characters for each node
5    private String nodeCharacters;
6    // Maximum path length found so far (stored as length - 1)
7    private int maxPathLength;
8
9    public int longestPath(int[] parent, String s) {
10        int nodeCount = parent.length;
11      
12        // Initialize adjacency list for tree representation
13        adjacencyList = new List[nodeCount];
14        this.nodeCharacters = s;
15      
16        // Create empty list for each node
17        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
18      
19        // Build the tree by adding children to their parent nodes
20        // Start from 1 since node 0 is the root (has no parent)
21        for (int childNode = 1; childNode < nodeCount; ++childNode) {
22            adjacencyList[parent[childNode]].add(childNode);
23        }
24      
25        // Perform DFS traversal starting from root node (0)
26        dfs(0);
27      
28        // Return the actual path length (stored value + 1)
29        return maxPathLength + 1;
30    }
31
32    /**
33     * Performs depth-first search to find the longest path
34     * @param currentNode The current node being processed
35     * @return The longest valid path length going down from current node
36     */
37    private int dfs(int currentNode) {
38        // Track the longest valid path going down from current node
39        int longestDownPath = 0;
40      
41        // Explore all children of the current node
42        for (int childNode : adjacencyList[currentNode]) {
43            // Recursively get the longest path from child node and add 1 for the edge
44            int pathFromChild = dfs(childNode) + 1;
45          
46            // Only consider this path if parent and child have different characters
47            if (nodeCharacters.charAt(currentNode) != nodeCharacters.charAt(childNode)) {
48                // Update global maximum by combining current longest path with new path
49                // This forms a path through the current node
50                maxPathLength = Math.max(maxPathLength, longestDownPath + pathFromChild);
51              
52                // Update the longest single path going down from current node
53                longestDownPath = Math.max(longestDownPath, pathFromChild);
54            }
55        }
56      
57        // Return the longest valid path going down from this node
58        return longestDownPath;
59    }
60}
61
1class Solution {
2public:
3    int longestPath(vector<int>& parent, string s) {
4        int n = parent.size();
5      
6        // Build adjacency list representation of the tree
7        // g[i] contains all children of node i
8        vector<vector<int>> graph(n);
9        for (int i = 1; i < n; ++i) {
10            graph[parent[i]].push_back(i);
11        }
12      
13        // Variable to store the maximum path length found
14        int maxPathLength = 0;
15      
16        // DFS function to find the longest path starting from node i
17        // Returns the length of the longest valid path going down from node i
18        function<int(int)> dfs = [&](int currentNode) -> int {
19            // Track the longest valid path from current node to any descendant
20            int longestDownPath = 0;
21          
22            // Explore all children of the current node
23            for (int childNode : graph[currentNode]) {
24                // Recursively get the longest path from child node
25                int pathFromChild = dfs(childNode) + 1;
26              
27                // Only consider this path if characters are different
28                if (s[currentNode] != s[childNode]) {
29                    // Update global maximum by considering path through current node
30                    // connecting two subtrees (previous longest + current path)
31                    maxPathLength = max(maxPathLength, longestDownPath + pathFromChild);
32                  
33                    // Update the longest downward path from current node
34                    longestDownPath = max(longestDownPath, pathFromChild);
35                }
36            }
37          
38            return longestDownPath;
39        };
40      
41        // Start DFS from root node (node 0)
42        dfs(0);
43      
44        // Add 1 because we count edges, but path length is number of nodes
45        return maxPathLength + 1;
46    }
47};
48
1/**
2 * Finds the longest path in a tree where adjacent nodes have different characters
3 * @param parent - Array where parent[i] is the parent of node i (parent[0] = -1 for root)
4 * @param s - String where s[i] is the character at node i
5 * @returns The length of the longest valid path
6 */
7function longestPath(parent: number[], s: string): number {
8    const nodeCount: number = parent.length;
9  
10    // Build adjacency list representation of the tree
11    // graph[i] contains all children of node i
12    const graph: number[][] = Array.from({ length: nodeCount }, () => []);
13  
14    // Populate the graph with parent-child relationships
15    // Start from 1 since node 0 is the root with no parent
16    for (let i = 1; i < nodeCount; ++i) {
17        graph[parent[i]].push(i);
18    }
19  
20    // Track the maximum path length found so far
21    let maxPathLength: number = 0;
22  
23    /**
24     * Performs depth-first search to find the longest path from each node
25     * @param currentNode - The current node being processed
26     * @returns The longest path length going down from the current node
27     */
28    const dfs = (currentNode: number): number => {
29        // Track the longest valid path from current node to any descendant
30        let longestDownPath: number = 0;
31      
32        // Explore all children of the current node
33        for (const childNode of graph[currentNode]) {
34            // Recursively get the longest path from the child node
35            const pathFromChild: number = dfs(childNode) + 1;
36          
37            // Only consider this path if characters are different
38            if (s[currentNode] !== s[childNode]) {
39                // Update global maximum by combining two longest paths through current node
40                maxPathLength = Math.max(maxPathLength, longestDownPath + pathFromChild);
41              
42                // Update the longest single path going down from current node
43                longestDownPath = Math.max(longestDownPath, pathFromChild);
44            }
45        }
46      
47        return longestDownPath;
48    };
49  
50    // Start DFS from the root node (node 0)
51    dfs(0);
52  
53    // Add 1 to account for the path length (edges + 1 = nodes)
54    return maxPathLength + 1;
55}
56

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once during the DFS traversal. For each node i, we iterate through all its children in g[i]. Since this is a tree structure with n nodes and n-1 edges, the total number of parent-child relationships processed across all nodes is n-1. Therefore, the overall time complexity is O(n), where n is the number of nodes in the tree.

Space Complexity: O(n)

The space complexity consists of several components:

  • The adjacency list g stores all parent-child relationships. In a tree with n nodes, there are n-1 edges, so the adjacency list uses O(n) space.
  • The recursive DFS call stack can go as deep as the height of the tree. In the worst case (a skewed tree), the height could be O(n), requiring O(n) space for the call stack.
  • Other variables (ans, mx, loop variables) use O(1) space.

Therefore, the overall space complexity is O(n), where n is the number of nodes in the tree.

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

Common Pitfalls

Pitfall 1: Forgetting to Handle Isolated Nodes

Issue: When a node has no valid children (either no children at all or all children have the same character), the code might not properly account for the node itself as a path of length 1.

Example: Consider a tree where the root has character 'a' and all its children also have character 'a'. The longest path should still be 1 (just the root or any single node).

Solution: The current implementation handles this correctly by returning longest_chain (which remains 0 when no valid children exist) and adding 1 at the final return. However, developers often forget this edge case when implementing from scratch.

Pitfall 2: Incorrect Path Combination Logic

Issue: A common mistake is to combine the two longest paths from children without checking if both paths are valid (i.e., both children have different characters from the parent).

Incorrect approach:

# Wrong: combining any two longest paths
first_longest = 0
second_longest = 0
for child in children[node]:
    child_length = dfs(child) + 1
    if child_length > first_longest:
        second_longest = first_longest
        first_longest = child_length
    elif child_length > second_longest:
        second_longest = child_length
# This ignores the character constraint!

Solution: Always check the character constraint before considering a path:

for child_node in children[current_node]:
    child_chain_length = dfs(child_node) + 1
    # Only process if characters are different
    if s[current_node] != s[child_node]:
        self.max_path_length = max(self.max_path_length, 
                                   longest_chain + child_chain_length)
        longest_chain = max(longest_chain, child_chain_length)

Pitfall 3: Confusing Edge Count with Node Count

Issue: The DFS function tracks the number of edges in paths, but the problem asks for the number of nodes. Forgetting to add 1 at the end will give an off-by-one error.

Example: A path with 3 nodes (A -> B -> C) has 2 edges. If the DFS returns 2, the final answer should be 3.

Solution: Always remember to add 1 when converting from edge count to node count:

return self.max_path_length + 1  # Convert edges to nodes

Pitfall 4: Not Initializing the Global Maximum Properly

Issue: If no valid paths exist (all adjacent nodes have the same character), the global maximum might remain 0, leading to returning 1 when it should return 1 anyway (for any single node).

Solution: The current implementation handles this correctly, but ensure that:

  • The global maximum starts at 0
  • The final return adds 1, ensuring at least 1 is returned (representing a single node)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More