2791. Count Paths That Can Form a Palindrome in a Tree
Problem Description
You are given a rooted 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
where parent[i]
indicates the parent of node i
. Since node 0
is the root, parent[0] = -1
.
Each edge in the tree has a character assigned to it. These characters are given in a string s
of length n
, where s[i]
represents the character on the edge connecting node i
to its parent parent[i]
. The character s[0]
can be ignored since the root has no parent.
Your task is to count the number of node pairs (u, v)
where u < v
such that the characters on the path between nodes u
and v
can be rearranged to form a palindrome.
A key insight is that a string can be rearranged into a palindrome if and only if at most one character appears an odd number of times. For example:
"aab"
can be rearranged to"aba"
(palindrome)"abc"
cannot be rearranged into any palindrome"aabbcc"
can be rearranged to"abccba"
(palindrome)
The solution uses XOR operations and bit manipulation to track character frequencies efficiently. Each character is represented by a bit position (0-25 for 'a'-'z'). The XOR of all characters on a path gives us a bitmask where each set bit represents a character appearing an odd number of times. A path can form a palindrome if the XOR result has at most one bit set (i.e., it's either 0 or a power of 2).
The algorithm performs a DFS traversal from the root, maintaining XOR values for paths from the root to each node. It counts valid palindromic paths by checking if two paths' XOR values differ by at most one bit, which would mean the path between those nodes can form a palindrome.
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).
Is it a tree?
- Yes: The problem clearly mentions "You are given a tree (i.e. a connected, undirected graph that has no cycles)". The tree is rooted at node 0 and represented by a parent array.
DFS
- Conclusion: Based on the flowchart, when we have a tree structure, DFS (Depth-First Search) is the recommended approach.
The flowchart leads us directly to DFS because:
- We're working with a tree data structure
- We need to explore paths between nodes (from any node
u
to any nodev
) - We need to track information (character frequencies via XOR) as we traverse from the root down to each node
- DFS allows us to maintain state as we traverse down the tree and efficiently compute path information
The DFS approach is particularly suitable here because:
- We can maintain the XOR value of characters from the root to the current node as we traverse
- We can use the XOR properties to quickly determine if a path between two nodes can form a palindrome
- The tree structure guarantees there's exactly one path between any two nodes, making DFS traversal straightforward
Conclusion: The flowchart correctly identifies that for a tree-based problem involving path analysis, DFS is the optimal pattern to use.
Intuition
The key insight starts with understanding when a string can be rearranged into a palindrome. A string forms a palindrome when characters can be paired up symmetrically, with at most one character in the middle. This means at most one character can appear an odd number of times.
Instead of actually finding paths between every pair of nodes and checking character frequencies (which would be inefficient), we can use a clever observation: in a tree, the path from node u
to node v
can be decomposed as:
- Path from root to
u
- Path from root to
v
- Minus the overlapping path from root to their lowest common ancestor (LCA)
This is where XOR becomes brilliant. XOR has the property that a ^ a = 0
, meaning any character that appears an even number of times will cancel out. So if we XOR all characters on a path, we get a bitmask where each set bit represents a character appearing an odd number of times.
For each character 'a' to 'z', we assign a bit position (0 to 25). When we encounter a character, we XOR with 1 << (character - 'a')
. This flips the corresponding bit, effectively tracking odd/even occurrences.
During DFS traversal from the root, we maintain the XOR value from root to current node. When we're at node v
with XOR value xor_v
, we want to find how many previously visited nodes u
can form a palindromic path with v
.
The path from u
to v
can form a palindrome if:
xor_u ^ xor_v = 0
(all characters appear even times)xor_u ^ xor_v = 2^k
for some k (exactly one character appears odd times)
Rearranging: we need xor_u = xor_v
or xor_u = xor_v ^ (1 << k)
for some k.
So at each node, we count how many previous nodes have XOR values that match these conditions. We use a frequency map to track XOR values we've seen, allowing O(1) lookups. This transforms a potentially O(n²) problem into an O(n) solution with smart preprocessing and XOR properties.
Learn more about Tree, Depth-First Search, Dynamic Programming and Bitmask patterns.
Solution Approach
The implementation uses DFS with XOR-based state tracking to efficiently count palindromic paths:
Data Structures:
g
: A graph adjacency list whereg[parent]
contains tuples of(child, [bitmask](/problems/bitmask_intro))
. The bitmask represents1 << (character - 'a')
for the edge character.cnt
: A counter/frequency map tracking how many times each XOR value has been seen during traversal.ans
: Accumulates the total count of valid palindromic paths.
Building the Graph:
g = defaultdict(list)
for i in range(1, n):
p = parent[i]
g[p].append((i, 1 << (ord(s[i]) - ord('a'))))
For each node i
(except root), we add an edge from its parent to itself with a bitmask representing the edge character.
DFS Traversal:
def dfs(i: int, xor: int):
for j, v in g[i]:
x = xor ^ v # XOR value from root to child j
The function maintains xor
as the cumulative XOR from root to current node. For each child j
, we compute its XOR value x
by XORing the parent's value with the edge bitmask v
.
Counting Valid Paths:
ans += cnt[x] # Paths with all even character counts
for k in range(26):
ans += cnt[x ^ (1 << k)] # Paths with exactly one odd character
For the current node with XOR value x
, we count:
- Previous nodes with the same XOR value (path between them has all even counts)
- Previous nodes whose XOR differs by exactly one bit (path has exactly one odd character)
Recording Current Node:
cnt[x] += 1 dfs(j, x)
After counting paths ending at this node, we record its XOR value in cnt
for future nodes to reference, then recursively process its subtree.
Initialization:
cnt = Counter({0: 1}) dfs(0, 0)
We initialize cnt[0] = 1
to handle paths starting from the root (which has XOR value 0). The DFS starts from root node 0 with initial XOR value 0.
The algorithm runs in O(n Ă 26) = O(n) time, where the factor of 26 comes from checking all possible single-bit differences for palindrome conditions. Space complexity is O(n) for storing the graph and XOR frequency map.
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.
Example Tree:
parent = [-1, 0, 0, 1] s = "abab"
This gives us the following tree structure:
0 (root) / \ a/ \b 1 2 / b/ 3
Step 1: Build the Graph
g = { 0: [(1, 1<<0), (2, 1<<1)], # 0 -> 1 with 'a' (bit 0), 0 -> 2 with 'b' (bit 1) 1: [(3, 1<<1)] # 1 -> 3 with 'b' (bit 1) }
- Edge 0â1 has character 'a', represented as bitmask
1<<0 = 1
- Edge 0â2 has character 'b', represented as bitmask
1<<1 = 2
- Edge 1â3 has character 'b', represented as bitmask
1<<1 = 2
Step 2: DFS Traversal
Initialize: cnt = {0: 1}
, ans = 0
Visit node 0 (root):
-
Current XOR = 0
-
Process child 1:
x = 0 ^ 1 = 1
(XOR for path rootâ1 is 1, meaning 'a' appears odd times)- Count valid paths ending at node 1:
cnt[1] = 0
(no previous node with XOR=1)- Check single-bit differences:
cnt[1 ^ 1<<0] = cnt[0] = 1
â (path 0â1 has only 'a')cnt[1 ^ 1<<1] = cnt[3] = 0
- ... (other bits give 0)
ans += 1
(total: 1)
- Record:
cnt[1] = 1
- Recursively visit node 1:
- Process child 3:
x = 1 ^ 2 = 3
(XOR for path rootâ3 is 3, bits 0 and 1 set)- Count valid paths:
cnt[3] = 0
cnt[3 ^ 1<<0] = cnt[2] = 0
cnt[3 ^ 1<<1] = cnt[1] = 1
â (path 1â3 has only 'b')- ... (other bits give 0)
ans += 1
(total: 2)- Record:
cnt[3] = 1
- Process child 3:
-
Process child 2:
x = 0 ^ 2 = 2
(XOR for path rootâ2 is 2, meaning 'b' appears odd times)- Count valid paths:
cnt[2] = 0
cnt[2 ^ 1<<0] = cnt[3] = 1
â (path 2â3 has "ab", can form "aba")cnt[2 ^ 1<<1] = cnt[0] = 1
â (path 0â2 has only 'b')- ... (other bits give 0)
ans += 2
(total: 4)- Record:
cnt[2] = 1
Final Result: 4 valid pairs
The valid pairs are:
- (0,1): path has "a" â palindrome
- (0,2): path has "b" â palindrome
- (1,3): path has "b" â palindrome
- (2,3): path has "ab" â can rearrange to "aba"
The algorithm correctly identifies all pairs where the path characters can form a palindrome by tracking XOR values and checking for at most one odd character count.
Solution Implementation
1from typing import List
2from collections import defaultdict, Counter
3
4class Solution:
5 def countPalindromePaths(self, parent: List[int], s: str) -> int:
6 # Build adjacency list for the tree
7 # Each edge stores (child_node, bitmask_of_character)
8 tree = defaultdict(list)
9 n = len(parent)
10
11 # Build tree from parent array (node 0 is root)
12 for child in range(1, n):
13 parent_node = parent[child]
14 # Convert character to bitmask (1 << position_in_alphabet)
15 char_bitmask = 1 << (ord(s[child]) - ord('a'))
16 tree[parent_node].append((child, char_bitmask))
17
18 # Initialize result counter
19 palindrome_path_count = 0
20
21 # Track XOR values from root to each node
22 # XOR tracks parity of character frequencies
23 xor_frequency = Counter({0: 1}) # Start with empty path
24
25 def dfs(node: int, path_xor: int) -> None:
26 """
27 DFS traversal to count palindromic paths.
28
29 Args:
30 node: Current node in traversal
31 path_xor: XOR of all characters from root to current node
32 """
33 nonlocal palindrome_path_count
34
35 # Explore all children of current node
36 for child, char_mask in tree[node]:
37 # Update XOR for path from root to child
38 child_path_xor = path_xor ^ char_mask
39
40 # Count palindromic paths ending at current child:
41
42 # Case 1: Path with all even character frequencies
43 # (XOR equals to some previous XOR value)
44 palindrome_path_count += xor_frequency[child_path_xor]
45
46 # Case 2: Path with exactly one odd character frequency
47 # (XOR differs by exactly one bit from previous XOR values)
48 for bit_position in range(26): # Check each letter a-z
49 target_xor = child_path_xor ^ (1 << bit_position)
50 palindrome_path_count += xor_frequency[target_xor]
51
52 # Add current path XOR to frequency map
53 xor_frequency[child_path_xor] += 1
54
55 # Recursively process subtree rooted at child
56 dfs(child, child_path_xor)
57
58 # Start DFS from root (node 0) with empty XOR
59 dfs(0, 0)
60
61 return palindrome_path_count
62
1class Solution {
2 // Graph representation: adjacency list where each edge stores [childNode, characterBitmask]
3 private List<int[]>[] graph;
4
5 // Map to count occurrences of each XOR value (bitmask state)
6 private Map<Integer, Integer> xorCount = new HashMap<>();
7
8 // Final answer: count of palindromic paths
9 private long answer;
10
11 /**
12 * Counts the number of paths in a tree that form palindromes.
13 * A path forms a palindrome if at most one character appears an odd number of times.
14 *
15 * @param parent List where parent[i] is the parent of node i (parent[0] = -1 for root)
16 * @param s String where s.charAt(i) is the character associated with node i
17 * @return Number of palindromic paths in the tree
18 */
19 public long countPalindromePaths(List<Integer> parent, String s) {
20 int nodeCount = parent.size();
21
22 // Initialize the graph as an array of lists
23 graph = new List[nodeCount];
24 Arrays.setAll(graph, index -> new ArrayList<>());
25
26 // Initialize XOR count with root having XOR value 0
27 xorCount.put(0, 1);
28
29 // Build the tree structure (skip root at index 0)
30 for (int childNode = 1; childNode < nodeCount; ++childNode) {
31 int parentNode = parent.get(childNode);
32 // Store child node and its character as a bitmask (1 << position)
33 int characterBitmask = 1 << (s.charAt(childNode) - 'a');
34 graph[parentNode].add(new int[] {childNode, characterBitmask});
35 }
36
37 // Start DFS traversal from root with initial XOR value 0
38 dfs(0, 0);
39
40 return answer;
41 }
42
43 /**
44 * Performs depth-first search to count palindromic paths.
45 * Uses XOR to track character frequency parity along paths.
46 *
47 * @param currentNode Current node in the traversal
48 * @param currentXor XOR value representing character frequency parity from root to current node
49 */
50 private void dfs(int currentNode, int currentXor) {
51 // Traverse all children of the current node
52 for (int[] edge : graph[currentNode]) {
53 int childNode = edge[0];
54 int characterBitmask = edge[1];
55
56 // Calculate new XOR value by toggling the bit for current character
57 int newXor = currentXor ^ characterBitmask;
58
59 // Count paths that form palindromes ending at this child node:
60
61 // Case 1: Perfect palindrome (all characters appear even times)
62 // Add count of previous nodes with same XOR value
63 answer += xorCount.getOrDefault(newXor, 0);
64
65 // Case 2: Palindrome with one odd character
66 // Check all 26 possible characters that could be the single odd one
67 for (int bitPosition = 0; bitPosition < 26; ++bitPosition) {
68 // Toggle each bit to check if flipping it gives a matching XOR
69 int targetXor = newXor ^ (1 << bitPosition);
70 answer += xorCount.getOrDefault(targetXor, 0);
71 }
72
73 // Update count for current XOR value
74 xorCount.merge(newXor, 1, Integer::sum);
75
76 // Continue DFS traversal to child node
77 dfs(childNode, newXor);
78 }
79 }
80}
81
1class Solution {
2public:
3 long long countPalindromePaths(vector<int>& parent, string s) {
4 int n = parent.size();
5
6 // Build adjacency list representation of the tree
7 // Each edge stores: (child_node, character_bitmask)
8 vector<vector<pair<int, int>>> adjacencyList(n);
9
10 // Map to count occurrences of each XOR bitmask value
11 // Represents paths from root to current node
12 unordered_map<int, int> bitmaskCount;
13 bitmaskCount[0] = 1; // Initialize with empty path from root
14
15 // Build the tree structure (skip root at index 0)
16 for (int i = 1; i < n; ++i) {
17 int parentNode = parent[i];
18 int characterBit = 1 << (s[i] - 'a'); // Convert character to bit position
19 adjacencyList[parentNode].emplace_back(i, characterBit);
20 }
21
22 long long totalPalindromePaths = 0;
23
24 // DFS function to traverse tree and count palindromic paths
25 // currentNode: current node being visited
26 // currentXor: XOR of all characters from root to current node
27 function<void(int, int)> dfs = [&](int currentNode, int currentXor) {
28 // Process each child of the current node
29 for (auto [childNode, characterBitmask] : adjacencyList[currentNode]) {
30 // Calculate XOR for path from root to child
31 int childXor = currentXor ^ characterBitmask;
32
33 // Count paths that form palindromes with current path
34 // Case 1: Exact match (all characters appear even times)
35 totalPalindromePaths += bitmaskCount[childXor];
36
37 // Case 2: One character appears odd times (flip each bit)
38 for (int bitPosition = 0; bitPosition < 26; ++bitPosition) {
39 int targetXor = childXor ^ (1 << bitPosition);
40 totalPalindromePaths += bitmaskCount[targetXor];
41 }
42
43 // Add current path's XOR to the map for future comparisons
44 ++bitmaskCount[childXor];
45
46 // Recursively process the subtree
47 dfs(childNode, childXor);
48 }
49 };
50
51 // Start DFS from root with XOR value 0
52 dfs(0, 0);
53
54 return totalPalindromePaths;
55 }
56};
57
1/**
2 * Counts the number of palindrome paths in a tree where each node has a character
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 number of valid palindrome paths between nodes
6 */
7function countPalindromePaths(parent: number[], s: string): number {
8 const nodeCount = parent.length;
9
10 // Build adjacency list for the tree
11 // Each edge stores [childNode, characterBitmask]
12 const adjacencyList: [number, number][][] = Array.from({ length: nodeCount }, () => []);
13
14 // Construct tree edges (skip root at index 0)
15 for (let i = 1; i < nodeCount; ++i) {
16 // Create bitmask for character at node i (bit position based on 'a' = 0)
17 const characterBitmask = 1 << (s.charCodeAt(i) - 97);
18 adjacencyList[parent[i]].push([i, characterBitmask]);
19 }
20
21 // Map to store XOR values and their frequencies
22 // XOR represents the parity of character occurrences on path from root
23 const xorFrequencyMap: Map<number, number> = new Map();
24 xorFrequencyMap.set(0, 1); // Initialize with empty path
25
26 let palindromePathCount = 0;
27
28 /**
29 * DFS traversal to count palindrome paths
30 * @param currentNode - Current node being visited
31 * @param currentXor - XOR of all characters from root to current node
32 */
33 const dfs = (currentNode: number, currentXor: number): void => {
34 // Traverse all children of current node
35 for (const [childNode, characterBitmask] of adjacencyList[currentNode]) {
36 // Calculate new XOR value including child's character
37 const newXor = currentXor ^ characterBitmask;
38
39 // Count paths that form palindromes with current path
40 // Case 1: Even occurrences of all characters (XOR = 0)
41 palindromePathCount += xorFrequencyMap.get(newXor) || 0;
42
43 // Case 2: At most one character with odd occurrence
44 // Check all 26 possible characters that could have odd count
45 for (let bitPosition = 0; bitPosition < 26; ++bitPosition) {
46 const xorWithSingleOddChar = newXor ^ (1 << bitPosition);
47 palindromePathCount += xorFrequencyMap.get(xorWithSingleOddChar) || 0;
48 }
49
50 // Update frequency map with current path's XOR
51 xorFrequencyMap.set(newXor, (xorFrequencyMap.get(newXor) || 0) + 1);
52
53 // Continue DFS to child nodes
54 dfs(childNode, newXor);
55 }
56 };
57
58 // Start DFS from root with initial XOR value of 0
59 dfs(0, 0);
60
61 return palindromePathCount;
62}
63
Time and Space Complexity
Time Complexity: O(n Ă 26) = O(n)
The algorithm performs a DFS traversal of the tree starting from node 0. For each node visited:
- We iterate through all its children (each node is visited exactly once)
- For each child, we check the current XOR value against the counter:
O(1)
- We check 26 possible single-bit differences (for each letter a-z):
O(26)
- We update the counter:
O(1)
- We recursively call DFS on the child
Since each of the n
nodes is visited exactly once, and for each node we perform O(26)
operations, the total time complexity is O(n Ă 26) = O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- Graph adjacency list
g
: stores at mostn-1
edges (tree structure), soO(n)
- Counter
cnt
: in the worst case, each path from root could have a unique XOR value, storing up ton
distinct values, soO(n)
- Recursion stack depth: in the worst case (skewed tree), the depth could be
O(n)
- XOR values and other variables:
O(1)
The total space complexity is O(n) + O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Initialize XOR Frequency Map with {0: 1}
The Pitfall:
A common mistake is initializing the XOR frequency counter as empty (Counter()
) instead of Counter({0: 1})
. This causes incorrect counting of paths that start from the root node.
Why It Happens:
When we reach a node with XOR value x
and want to count palindromic paths, we look for previous nodes with matching XOR values. For paths starting from the root, we need to match with XOR value 0 (the root's XOR). Without {0: 1}
initialization, these paths won't be counted.
Incorrect Code:
xor_frequency = Counter() # Missing {0: 1}
Correct Solution:
xor_frequency = Counter({0: 1}) # Properly handle root paths
2. Updating XOR Frequency Before Counting
The Pitfall: Adding the current node's XOR value to the frequency map before counting palindromic paths leads to counting invalid self-loops.
Why It Happens:
If we update xor_frequency[child_path_xor] += 1
before counting, we might count a path from a node to itself (which isn't valid since we need u < v
with distinct nodes).
Incorrect Code:
for child, char_mask in tree[node]: child_path_xor = path_xor ^ char_mask xor_frequency[child_path_xor] += 1 # Updated too early! palindrome_path_count += xor_frequency[child_path_xor] # ... rest of counting logic
Correct Solution:
for child, char_mask in tree[node]:
child_path_xor = path_xor ^ char_mask
# Count first
palindrome_path_count += xor_frequency[child_path_xor]
for bit_position in range(26):
palindrome_path_count += xor_frequency[child_path_xor ^ (1 << bit_position)]
# Update after counting
xor_frequency[child_path_xor] += 1
3. Incorrectly Handling the Root Node's Character
The Pitfall:
Including s[0]
in the XOR calculation or trying to process it as an edge character. The problem states that s[0]
should be ignored since the root has no parent.
Why It Happens:
It's easy to overlook that s[i]
represents the character on the edge from node i
to its parent, not the character "at" node i
.
Incorrect Code:
# Wrong: Starting loop from 0
for i in range(0, n):
p = parent[i]
if p != -1: # Still wrong logic
g[p].append((i, 1 << (ord(s[i]) - ord('a'))))
Correct Solution:
# Start from 1, skipping the root
for i in range(1, n):
p = parent[i]
g[p].append((i, 1 << (ord(s[i]) - ord('a'))))
4. Not Considering All 26 Possible Single-Bit Differences
The Pitfall: Only checking a subset of possible characters when looking for paths with exactly one odd character frequency, or using the wrong range.
Why It Happens: Developers might optimize prematurely by only checking characters that appear in the string, but this misses valid palindromic paths.
Incorrect Code:
# Only checking characters that we've seen
unique_chars = set(s)
for char in unique_chars:
bit_pos = ord(char) - ord('a')
palindrome_path_count += xor_frequency[child_path_xor ^ (1 << bit_pos)]
Correct Solution:
# Check all 26 possible positions
for bit_position in range(26):
target_xor = child_path_xor ^ (1 << bit_position)
palindrome_path_count += xor_frequency[target_xor]
This ensures we find all valid palindromic paths, even when the differing character hasn't been seen yet in our traversal.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Donât Miss This!