Count Paths That Can Form a Palindrome in a Tree
HardPrompt
You are given a tree (i.e. a connected, undirected graph that has no cycles) rooted at node 0 consisting of n nodes numbered from 0 to n - 1. The tree is represented by a 0-indexed array parent of size n, where parent[i] is the parent of node i. Since node 0 is the root, parent[0] == -1.
You are also given a string s of length n, where s[i] is the character assigned to the edge between i and parent[i]. s[0] can be ignored.
Return the number of pairs of nodes (u, v) such that u < v and the characters assigned to edges on the path from u to v can be rearranged to form a palindrome.
A string is a palindrome when it reads the same backwards as forwards.
Example 1:

Input: parent = [-1,0,0,1,1,2], s = "acaabc"
Output: 8
Explanation: The valid pairs are:
- All the pairs (0,1), (0,2), (1,3), (1,4) and (2,5) result in one character which is always a palindrome.
- The pair (2,3) result in the string "aca" which is a palindrome.
- The pair (1,5) result in the string "cac" which is a palindrome.
- The pair (3,5) result in the string "acac" which can be rearranged into the palindrome "acca".Example 2:
Input: parent = [-1,0,0,0,0], s = "aaaaa"
Output: 10
Explanation: Any pair of nodes (u,v) where u < v is valid.
Constraints:
n == parent.length == s.length1 <= n <= 1050 <= parent[i] <= n - 1for alli >= 1parent[0] == -1parentrepresents a valid tree.sconsists of only lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This is a straightforward brute-force approach that directly simulates the problem statement. It iterates through all possible pairs of nodes (u, v), finds the path between them, counts the frequencies of characters along that path, and checks if these characters can form a palindrome. A string can be rearranged into a palindrome if at most one of its characters appears an odd number of times.
Algorithm
- Build Graph: Construct an adjacency list representation of the tree from the
parentarray. - Iterate All Pairs: Use nested loops to iterate through every possible pair of nodes
(u, v)such thatu < v. - Find Path: For each pair, find the path between
uandv. This can be done by finding their Lowest Common Ancestor (LCA). The path consists of nodes fromuto the LCA and nodes from the LCA tov. - Count Character Frequencies: Traverse the path and count the occurrences of each character on the edges.
- Check Palindrome Condition: After counting, check if at most one character has an odd frequency. If so, the path can form a palindrome.
- Count Valid Pairs: Increment a counter for each valid pair found.
- Return Total Count: After checking all pairs, return the total count.
Walkthrough
The core of this method is to check every single pair of nodes (u, v). For each pair, we must first determine the path connecting them. In a tree, this path is unique and goes from u up to the Lowest Common Ancestor (LCA) of u and v, and then down to v. After identifying the path, we collect all characters on the edges of this path, count their frequencies, and verify the palindrome property.
Here's a sketch of the implementation:
- First, we convert the
parentarray into a more usable adjacency list representation, where each entry also stores the character associated with the edge. - We then have two nested loops,
for u from 0 to n-1andfor v from u+1 to n-1. - Inside the loops, for a given
(u, v), we find the path. A simple way to do this without a complex LCA algorithm is to trace the parents ofuup to the root, storing them in a set. Then, trace the parents ofvup until we find a node that is in the set; this node is the LCA. The full path is then reconstructed. - We iterate over the edges in the path, count character frequencies using an array of size 26.
- Finally, we check the frequency array to see how many characters have odd counts. If the number of characters with odd counts is 0 or 1, we increment our total result.
// This is a conceptual snippet and would be part of a larger class structure.// A full implementation would require building the graph and helper methods for path finding. private long countPathsThatCanFormPalindrome(int n, int[] parent, String s) { // 1. Build adjacency list (not shown for brevity) List<List<Pair<Integer, Character>>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (int i = 1; i < n; i++) { adj.get(parent[i]).add(new Pair<>(i, s.charAt(i))); adj.get(i).add(new Pair<>(parent[i], s.charAt(i))); } long count = 0; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { // 2. Find path from i to j (e.g., using BFS/DFS) List<Character> pathChars = findPathChars(i, j, n, adj); // 3. Check if path can form a palindrome if (canFormPalindrome(pathChars)) { count++; } } } return count;} private boolean canFormPalindrome(List<Character> chars) { int[] freq = new int[26]; for (char c : chars) { freq[c - 'a']++; } int oddCounts = 0; for (int f : freq) { if (f % 2 != 0) { oddCounts++; } } return oddCounts <= 1;} // findPathChars would be a helper function that finds the path and returns the characters.// This is non-trivial and adds to the complexity.Complexity
Time
O(N² * H), where N is the number of nodes and H is the height of the tree. There are O(N²) pairs. For each pair, finding the path and counting characters takes O(H) time. In the worst case (a skewed tree), H can be O(N), leading to O(N³).
Space
O(N) to store the tree structure. Each path-finding operation might take up to O(H) space, where H is the height of the tree.
Trade-offs
Pros
Simple to understand and implement.
Directly follows the problem description.
Cons
Extremely inefficient due to nested loops and repeated path traversals.
Finding the path and LCA for every pair is computationally expensive.
Will result in a 'Time Limit Exceeded' error on large inputs.
This approach improves upon the first one by using a clever bitmasking trick. Instead of recounting character frequencies for each path, we precompute a 'path mask' for every node. This mask represents the parity of character counts on the path from the root to that node. The mask for the path between any two nodes u and v can then be found in O(1) by XORing their individual root-to-node path masks. This avoids the expensive path-finding step inside the main loop, reducing the complexity for each pair check from O(H) to O(1).
Algorithm
- Bitmask Insight: Realize that a path can form a palindrome if the bitmask of its character parities has at most one bit set. The
i-th bit is 1 if thei-th letter appears an odd number of times, 0 otherwise. - Path Mask Calculation: The mask for a path between
uandvismask(u) ^ mask(v), wheremask(x)is the mask for the path from the root to nodex. - Precompute Masks: Perform a single tree traversal (DFS or BFS) from the root to compute
mask(i)for every nodei. - Iterate All Pairs: Use nested loops to iterate through all pairs
(u, v)withu < v. - Check Condition: For each pair, calculate the path mask
p = mask(u) ^ mask(v). Check ifpis 0 or a power of two. A numberp > 0is a power of two if(p & (p - 1)) == 0. - Count and Return: Increment a counter for each valid pair and return the total.
Walkthrough
The key idea is to represent character counts as bitmasks. Since we only care about whether a character count is odd or even, a 26-bit integer suffices. The k-th bit is set if the character 'a' + k appears an odd number of times on a path.
The mask for the path from u to v can be calculated as mask_from_root(u) ^ mask_from_root(v). This works because edges on the common path from the root to lca(u,v) are included in both masks, and the XOR operation cancels them out, leaving only the edges on the unique parts of the paths from the root to u and v.
The algorithm is as follows:
- Build the adjacency list for the tree.
- Perform a single DFS or BFS traversal starting from the root (node 0) to compute the path mask for every node. Let's say
masks[i]stores the mask for the path from the root to nodei.masks[0]is 0. For any other nodeiwith parentp,masks[i] = masks[p] ^ (1 << (s[i] - 'a')). - After precomputation, iterate through all pairs
(u, v)withu < v. - For each pair, calculate
pathMask = masks[u] ^ masks[v]. - A path can form a palindrome if its mask is zero (all even counts) or a power of two (one odd count). We check this condition:
pathMask == 0 || (pathMask > 0 && (pathMask & (pathMask - 1)) == 0). - If the condition is met, we increment our result counter.
class Solution { public long countPalindromePaths(List<Integer> parent, String s) { int n = parent.size(); List<List<Pair<Integer, Integer>>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (int i = 1; i < n; i++) { adj.get(parent.get(i)).add(new Pair<>(i, 1 << (s.charAt(i) - 'a'))); } int[] masks = new int[n]; // Precompute masks from root to each node Queue<Integer> q = new LinkedList<>(); q.offer(0); masks[0] = 0; while (!q.isEmpty()) { int u = q.poll(); for (Pair<Integer, Integer> edge : adj.get(u)) { int v = edge.getKey(); int charMask = edge.getValue(); masks[v] = masks[u] ^ charMask; q.offer(v); } } long count = 0; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { int pathMask = masks[i] ^ masks[j]; if ((pathMask & (pathMask - 1)) == 0) { count++; } } } return count; }}// Note: In LeetCode, parent is a List<Integer>, not an array.// Pair class would be a simple helper class or use an array int[2].Complexity
Time
O(N²). Precomputing all path masks takes O(N). The nested loops to check all pairs take O(N²), with each check being O(1).
Space
O(N) for the adjacency list, the queue for BFS, and the `masks` array.
Trade-offs
Pros
Much faster than the first approach.
Introduces the key insight of using bitmasks for path properties.
Cons
The O(N²) complexity is still too slow for the given constraints (N up to 10^5).
Will result in a 'Time Limit Exceeded' error on large test cases.
This optimal approach completely avoids the O(N²) pair iteration. It leverages the path mask insight and focuses on counting. After computing all root-to-node path masks in a single O(N) traversal, we count how many times each mask appears. The problem then transforms into a combinatorial one: given the counts of different masks, how many pairs (u, v) satisfy the condition mask(u) ^ mask(v) = 0 or mask(u) ^ mask(v) = 2^k? This can be solved efficiently by iterating through the unique masks and their frequencies.
Algorithm
- Path Masks: Use the same bitmask representation as in the previous approach. The goal is to count pairs
(u, v)wheremask(u) ^ mask(v)is 0 or a power of 2. - Compute All Masks: Traverse the tree once with DFS or BFS to compute the root-to-node path mask for every node.
- Count Mask Frequencies: Store the frequency of each unique path mask in a HashMap, e.g.,
freqMap[mask] = count. - Calculate Pairs from Frequencies:
- Initialize
totalPairs = 0. - Iterate through each
maskand itscountin thefreqMap. - Case 1 (Zero difference): For paths where
mask(u) == mask(v), the path mask is 0. The number of such pairs for a given mask iscount * (count - 1) / 2. Add this tototalPairs. - Case 2 (One bit difference): For paths where
mask(u) ^ mask(v)is a power of two, iterate through each bitkfrom 0 to 25. LettargetMask = mask ^ (1 << k). IffreqMapcontainstargetMask, we can formcount * freqMap.get(targetMask)pairs. To avoid double-counting (e.g., counting(m, m')and later(m', m)), we only process pairs wheremask < targetMask.
- Initialize
- Return Result: The final
totalPairsis the answer.
Walkthrough
This method is the most efficient and is necessary to pass the given constraints. It builds upon the path mask idea from Approach 2 but uses a more advanced counting technique.
Algorithm Steps:
-
Build Adjacency List: As before, create an adjacency list from the
parentarray to represent the tree. Store the character's bitmask value with each edge. -
Compute All Path Masks: Traverse the tree using DFS (or BFS) from the root. During the traversal, compute the path mask for each node, which is the XOR sum of edge character masks from the root to the current node. Store these masks in an array or list.
-
Count Frequencies: Create a
HashMap<Integer, Integer>to store the frequency of each path mask. Iterate through the computed masks and populate this map. -
Calculate Total Pairs:
- Initialize a
longvariableans = 0. - Iterate over each entry (
mask,count) in the frequency map. - Paths with mask 0: These correspond to pairs of nodes
(u, v)that have the same root-to-node path mask (mask(u) == mask(v)). For a given mask that appearscounttimes, we can formcount * (count - 1) / 2such pairs. Add this toans. - Paths with one bit set: These correspond to pairs
(u, v)wheremask(u)andmask(v)differ by exactly one bit. For eachmaskfrom our frequency map, we check 26 possibilities. For each bitjfrom 0 to 25, we form atargetMask = mask ^ (1 << j). If thistargetMaskexists in our frequency map, we can formcount * freqMap.get(targetMask)new valid pairs. To avoid counting each pair twice (once formaskand once fortargetMask), we only add pairs whenmask < targetMask.
- Initialize a
This counting method ensures that every valid pair is counted exactly once.
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; class Solution { public long countPalindromePaths(List<Integer> parent, String s) { int n = parent.size(); List<List<int[]>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) { adj.add(new ArrayList<>()); } for (int i = 1; i < n; i++) { adj.get(parent.get(i)).add(new int[]{i, s.charAt(i) - 'a'}); } Map<Integer, Integer> freq = new HashMap<>(); dfs(0, 0, adj, freq); long ans = 0; for (Map.Entry<Integer, Integer> entry : freq.entrySet()) { int mask = entry.getKey(); long count = entry.getValue(); // Case 1: Paths with mask 0 (u and v have same path mask from root) ans += count * (count - 1) / 2; // Case 2: Paths with one bit set (u and v masks differ by one bit) for (int i = 0; i < 26; i++) { int targetMask = mask ^ (1 << i); if (mask < targetMask && freq.containsKey(targetMask)) { ans += count * freq.get(targetMask); } } } return ans; } private void dfs(int u, int currentMask, List<List<int[]>> adj, Map<Integer, Integer> freq) { freq.put(currentMask, freq.getOrDefault(currentMask, 0) + 1); for (int[] edge : adj.get(u)) { int v = edge[0]; int charIndex = edge[1]; dfs(v, currentMask ^ (1 << charIndex), adj, freq); } }}Complexity
Time
O(N). Building the graph takes O(N). The DFS to compute masks and populate the frequency map takes O(N). The final counting step iterates through unique masks (at most N) and for each, checks 26 other masks. This gives a complexity of O(N + M * 26), where M is the number of unique masks (M <= N), which simplifies to O(N).
Space
O(N) to store the adjacency list, recursion stack for DFS, and the frequency map. The number of unique masks in the map is at most N.
Trade-offs
Pros
Highly efficient with linear time complexity.
Solves the problem within the time limits for large inputs.
Elegant solution combining graph traversal, bit manipulation, and combinatorics.
Cons
The logic for counting from frequencies can be slightly tricky to get right, especially avoiding double counting.
Solutions
Solution
class Solution {private List<int[]>[] g;private Map<Integer, Integer> cnt = new HashMap<>();private long ans;public long countPalindromePaths(List<Integer> parent, String s) { int n = parent.size(); g = new List[n]; cnt.put(0, 1); Arrays.setAll(g, k->new ArrayList<>()); for (int i = 1; i < n; ++i) { int p = parent.get(i); g[p].add(new int[]{i, 1 << (s.charAt(i) - 'a')}); } dfs(0, 0); return ans; }private void dfs(int i, int xor) { for (int[] e : g[i]) { int j = e[0], v = e[1]; int x = xor^v; ans += cnt.getOrDefault(x, 0); for (int k = 0; k < 26; ++k) { ans += cnt.getOrDefault(x ^ (1 << k), 0); } cnt.merge(x, 1, Integer : : sum); dfs(j, x); } }}class Solution {public: long long countPalindromePaths(vector<int> &parent, string s) { int n = parent.size(); vector<vector<pair<int, int>>> g(n); unordered_map<int, int> cnt; cnt[0] = 1; for (int i = 1; i < n; ++i) { int p = parent[i]; g[p].emplace_back(i, 1 << (s[i] - 'a')); } long long ans = 0; function<void(int, int)> dfs = [&](int i, int xo) { for (auto [j, v] : g[i]) { int x = xo ^ v; ans += cnt[x]; for (int k = 0; k < 26; ++k) { ans += cnt[x ^ (1 << k)]; } ++cnt[x]; dfs(j, x); } }; dfs(0, 0); return ans; }};class Solution: def countPalindromePaths(self, parent: List[int], s: str) -> int: def dfs(i: int, xor: int): nonlocal ans for j, v in g[i]: x = xor ^ v ans += cnt[x] for k in range(26): ans += cnt[x ^ (1 << k)] cnt[x] += 1 dfs(j, x) n = len(parent) g = defaultdict(list) for i in range(1, n): p = parent[i] g[p]. append((i, 1 << (ord(s[i]) - ord('a')))) ans = 0 cnt = Counter({0: 1}) dfs(0, 0) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.