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 from0
ton-1
with node0
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:
- We need to traverse the tree from the root to explore all possible paths
- For each node, we need to calculate the longest valid path that passes through it
- DFS allows us to recursively explore each subtree and combine results from child nodes
- The tree structure guarantees no cycles, making DFS straightforward to implement
- We can use DFS to compute the longest path ending at each node and combine paths from different subtrees when characters are different
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:
- A downward path from this node through one of its children (if any)
- 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:
- Recursively get the longest downward path from each child
- If the characters are different, we can extend that path by including the current node
- Track the longest path we can make by joining paths from different children through the current node
- 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:
- Initializes
mx = 0
to track the longest downward path from nodei
- Iterates through all children
j
ing[i]
- Recursively calls
dfs(j)
to get the longest path from childj
- Adds 1 to include the edge from
i
toj
: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
throughj
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)
wheremx
is the longest path seen so far fromi
, andx
is the current path throughj
- Update
mx = max(mx, x)
to track the longest single downward path fromi
Step 4: Path Combination Logic
The key insight is in how we combine paths:
mx
represents the longest valid downward path from nodei
seen so farx
represents the current valid downward path through childj
mx + x
represents a complete path that goes:- Up from some descendant through a previous child to node
i
(lengthmx
) - Then down from node
i
through childj
to another descendant (lengthx
)
- Up from some descendant through a previous child to node
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 EvaluatorExample 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
- Call
- Return
mx = 1
- Initialize
- 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
- Call
-
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
ormx
- Call
-
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:
- Finding the path from node 1 down to node 3 (length 1)
- Extending it through node 1 to node 0 (total length 2)
- 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 withn
nodes, there aren-1
edges, so the adjacency list usesO(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)
, requiringO(n)
space for the call stack. - Other variables (
ans
,mx
, loop variables) useO(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)
What are the most two important steps in writing a depth first search function? (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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Don’t Miss This!