Facebook Pixel

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:

  1. We're working with a tree data structure
  2. We need to explore paths between nodes (from any node u to any node v)
  3. We need to track information (character frequencies via XOR) as we traverse from the root down to each node
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. xor_u ^ xor_v = 0 (all characters appear even times)
  2. 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 where g[parent] contains tuples of (child, [bitmask](/problems/bitmask_intro)). The bitmask represents 1 << (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:

  1. Previous nodes with the same XOR value (path between them has all even counts)
  2. 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 Evaluator

Example 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 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:

  1. (0,1): path has "a" → palindrome
  2. (0,2): path has "b" → palindrome
  3. (1,3): path has "b" → palindrome
  4. (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 most n-1 edges (tree structure), so O(n)
  • Counter cnt: in the worst case, each path from root could have a unique XOR value, storing up to n distinct values, so O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More