Check if DFS Strings Are Palindromes
HardPrompt
You are given a tree rooted at node 0, consisting of n nodes numbered from 0 to n - 1. The tree is represented by an 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 node i.
Consider an empty string dfsStr, and define a recursive function dfs(int x) that takes a node x as a parameter and performs the following steps in order:
- Iterate over each child
yofxin increasing order of their numbers, and calldfs(y). - Add the character
s[x]to the end of the stringdfsStr.
Note that dfsStr is shared across all recursive calls of dfs.
You need to find a boolean array answer of size n, where for each index i from 0 to n - 1, you do the following:
- Empty the string
dfsStrand calldfs(i). - If the resulting string
dfsStris a palindrome, then setanswer[i]totrue. Otherwise, setanswer[i]tofalse.
Return the array answer.
Example 1:
Input: parent = [-1,0,0,1,1,2], s = "aababa"
Output: [true,true,false,true,true,true]
Explanation:
- Calling
dfs(0)results in the stringdfsStr = "abaaba", which is a palindrome. - Calling
dfs(1)results in the stringdfsStr = "aba", which is a palindrome. - Calling
dfs(2)results in the stringdfsStr = "ab", which is not a palindrome. - Calling
dfs(3)results in the stringdfsStr = "a", which is a palindrome. - Calling
dfs(4)results in the stringdfsStr = "b", which is a palindrome. - Calling
dfs(5)results in the stringdfsStr = "a", which is a palindrome.
Example 2:
Input: parent = [-1,0,0,0,0], s = "aabcb"
Output: [true,true,true,true,true]
Explanation:
Every call on dfs(x) results in a palindrome string.
Constraints:
n == parent.length == s.length1 <= n <= 1050 <= parent[i] <= n - 1for alli >= 1.parent[0] == -1parentrepresents a valid tree.sconsists only of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
The most straightforward approach is to directly simulate the process described in the problem. For each node i in the tree, we can perform the specified DFS traversal starting from i to generate the corresponding dfsStr. Once the string is generated, we can check if it's a palindrome. We repeat this for all n nodes.
Algorithm
- Create an adjacency list
adjfrom theparentarray to represent the tree. Since children are added based on their node indexi, they will be naturally sorted. - Initialize a boolean array
answerof sizen. - Iterate through each node
ifrom0ton-1. - For each
i:- Create an empty
StringBuilderto store thedfsStr. - Call a recursive helper function, say
buildDfsStr(i, adj, s, dfsStr), to construct the string. - The
buildDfsStrfunction performs a post-order traversal on the subtree rooted at the given node. It first makes recursive calls for all children (in increasing order of their indices) and then appends the character of the current node to theStringBuilder. - After the string is built, convert it to a
String. - Check if the resulting string is a palindrome. This can be done with a two-pointer approach, comparing characters from the start and end moving inwards.
- Store the result of the palindrome check in
answer[i].
- Create an empty
- Finally, return the
answerarray.
Walkthrough
First, we need a convenient representation of the tree. We can convert the given parent array into an adjacency list, where adj[u] stores a list of all children of node u. The problem specifies that children should be visited in increasing order of their numbers, and building the adjacency list by iterating from i=1 to n-1 naturally achieves this.
Next, we loop through every node i from 0 to n-1. In each iteration, we initiate the process of building dfsStr for the subtree rooted at i. A recursive DFS function is used for this. This function takes the current node u, the adjacency list, the character string s, and a StringBuilder as arguments. It recursively calls itself for each child of u and then appends s.charAt(u) to the StringBuilder. This is a classic post-order traversal.
After the recursive calls starting from i are complete, the StringBuilder will contain the full dfsStr. We then check if this string is a palindrome. A simple and efficient way to do this is using a two-pointer technique. One pointer starts at the beginning of the string, and the other at the end. We move them towards the center, checking if the characters at their positions are always equal. If we find a mismatch, it's not a palindrome. If the pointers meet or cross without any mismatches, it is a palindrome.
The boolean result of this check is stored in answer[i], and after iterating through all nodes, the answer array is returned.
import java.util.ArrayList;import java.util.List; class Solution { public boolean[] checkDfsPalindromes(int[] parent, String s) { int n = parent.length; List<List<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[i]).add(i); } boolean[] answer = new boolean[n]; for (int i = 0; i < n; i++) { StringBuilder dfsStr = new StringBuilder(); generateDfsString(i, adj, s, dfsStr); answer[i] = isPalindrome(dfsStr.toString()); } return answer; } private void generateDfsString(int u, List<List<Integer>> adj, String s, StringBuilder sb) { for (int v : adj.get(u)) { generateDfsString(v, adj, s, sb); } sb.append(s.charAt(u)); } private boolean isPalindrome(String str) { int left = 0; int right = str.length() - 1; while (left < right) { if (str.charAt(left) != str.charAt(right)) { return false; } left++; right--; } return true; }}Complexity
Time
O(N^2). The main loop runs N times. Inside the loop, we perform a DFS on the subtree of node `i`. Let `size(i)` be the size of the subtree. The DFS and palindrome check both take O(size(i)) time. The total time is the sum of `size(i)` for all `i`. In the worst case of a line graph (e.g., 0-1-2-...-n-1), the sum is N + (N-1) + ... + 1, which is O(N^2).
Space
O(N). The space is dominated by the adjacency list (O(N)), the recursion stack depth (which can be O(N) in a skewed tree), and the `dfsStr` which can have a length up to N.
Trade-offs
Pros
Simple to understand and implement as it directly follows the problem statement.
Correct for small inputs.
Cons
The time complexity of O(N^2) is too slow for the given constraints (N up to 10^5) and will result in a Time Limit Exceeded error on larger test cases.
The brute-force approach is slow because it repeatedly generates and compares strings. A much more efficient method is to use polynomial rolling hash to represent the strings. Hashing allows us to compute a unique (with high probability) numerical signature for each string and its reverse. We can then check for palindromes by simply comparing these numerical hash values, which is an O(1) operation.
The key insight is that the hash of dfsStr(u) and its reverse can be computed efficiently for all nodes u in a single post-order traversal of the tree. The hash for a parent node can be constructed from the pre-computed hashes of its children.
Algorithm
- Select two different pairs of prime base and large prime modulus, e.g.,
(P1, M1)and(P2, M2), to minimize hash collisions. - Precompute the powers of
P1andP2modulo their respective moduli up tonand store them in arrays for O(1) access. - Build the adjacency list representation of the tree from the
parentarray. - Create arrays to store the length (
len), forward hashes (h1,h2), and reverse hashes (rh1,rh2) for thedfsStrof each node's subtree. - Perform a single post-order traversal of the entire tree, starting from the root (node 0). A recursive DFS function
postOrder(u)is suitable for this. - Inside
postOrder(u), after the recursive calls for all children ofuhave returned:- Compute
len[u], the size of the subtree atu, by summing the lengths of its children's subtrees and adding 1 foruitself. - Compute the forward hashes
h1[u]andh2[u]. This is done by combining the forward hashes of its children (in increasing order of child index) and then appending the characters[u]. - Compute the reverse hashes
rh1[u]andrh2[u]. This is done by starting with the characters[u]and then combining the reverse hashes of its children (in decreasing order of child index). - A string is a palindrome if its forward hash equals its reverse hash. Check if
h1[u] == rh1[u]andh2[u] == rh2[u]. Setanswer[u]to the result of this check.
- Compute
- After the traversal is complete, return the
answerarray.
Walkthrough
This approach avoids the O(N^2) complexity by calculating hashes for dfsStr(u) and reverse(dfsStr(u)) for every node u in one pass. Let's denote the forward hash as h[u] and the reverse hash as rh[u]. A string is a palindrome if h[u] == rh[u].
We use a post-order traversal (DFS) because to compute the hashes for a node u, we first need the hashes and lengths for all of its children. The traversal computes the following for each node u:
len[u]: The length ofdfsStr(u), which is simply the size of the subtree rooted atu.h[u]: The hash ofdfsStr(u) = dfsStr(c1) + ... + dfsStr(ck) + s[u]. This is computed by combining the hashes of its children's stringsh[c1], ..., h[ck]in order, and then incorporatings[u].rh[u]: The hash ofreverse(dfsStr(u)) = s[u] + reverse(dfsStr(ck)) + ... + reverse(dfsStr(c1)). This is computed by starting withs[u]and then combining the reverse hashes of its childrenrh[ck], ..., rh[c1]in reverse order.
The hash of a concatenated string A + B is (hash(A) + hash(B) * p^length(A)) % M. We use this formula to combine the children's hashes. To make the solution robust against collisions, we use two independent hash functions with different bases and moduli.
import java.util.ArrayList;import java.util.List; class Solution { private List<List<Integer>> adj; private String s; private boolean[] answer; private long[] p1_pow, p2_pow; private long[] h1, h2, rh1, rh2; private int[] len; private final long P1 = 31, M1 = 1_000_000_007; private final long P2 = 37, M2 = 1_000_000_009; public boolean[] checkDfsPalindromes(int[] parent, String s) { int n = parent.length; this.s = s; this.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(i); } this.answer = new boolean[n]; this.p1_pow = new long[n + 1]; this.p2_pow = new long[n + 1]; this.h1 = new long[n]; this.h2 = new long[n]; this.rh1 = new long[n]; this.rh2 = new long[n]; this.len = new int[n]; p1_pow[0] = 1; p2_pow[0] = 1; for (int i = 1; i <= n; i++) { p1_pow[i] = (p1_pow[i - 1] * P1) % M1; p2_pow[i] = (p2_pow[i - 1] * P2) % M2; } postOrder(0); return answer; } private void postOrder(int u) { for (int v : adj.get(u)) { postOrder(v); } // Compute forward hash long current_h1 = 0, current_h2 = 0; int current_len = 0; for (int v : adj.get(u)) { current_h1 = (current_h1 + (h1[v] * p1_pow[current_len]) % M1) % M1; current_h2 = (current_h2 + (h2[v] * p2_pow[current_len]) % M2) % M2; current_len += len[v]; } long char_val = s.charAt(u) - 'a' + 1; h1[u] = (current_h1 + (char_val * p1_pow[current_len]) % M1) % M1; h2[u] = (current_h2 + (char_val * p2_pow[current_len]) % M2) % M2; // Compute reverse hash long current_rh1 = char_val, current_rh2 = char_val; int current_rev_len = 1; List<Integer> children = adj.get(u); for (int i = children.size() - 1; i >= 0; i--) { int v = children.get(i); current_rh1 = (current_rh1 + (rh1[v] * p1_pow[current_rev_len]) % M1) % M1; current_rh2 = (current_rh2 + (rh2[v] * p2_pow[current_rev_len]) % M2) % M2; current_rev_len += len[v]; } rh1[u] = current_rh1; rh2[u] = current_rh2; len[u] = current_len + 1; answer[u] = (h1[u] == rh1[u] && h2[u] == rh2[u]); }}Complexity
Time
O(N). We perform a single DFS traversal, visiting each node and edge exactly once. The precomputation of powers also takes O(N). The work done at each node during the traversal is proportional to its number of children. The sum of children over all nodes is the number of edges, N-1. Thus, the total time is linear.
Space
O(N). We need O(N) space for the adjacency list, the recursion stack (O(H) where H is tree height, O(N) in worst case), and several arrays of size N to store lengths and hash values.
Trade-offs
Pros
Optimal time complexity of O(N), which is very efficient and passes for large constraints.
Solves the problem in a single pass over the tree.
Cons
The implementation is more complex than the brute-force approach.
It relies on polynomial rolling hash, which has a theoretical, albeit extremely small, probability of collision. Using two hash functions makes this probability negligible for practical purposes.
Solutions
Solution
class Hashing { private final long [] p ; private final long [] h ; private final long mod ; public Hashing ( String word , long base , int mod ) { int n = word . length (); p = new long [ n + 1 ]; h = new long [ n + 1 ]; p [ 0 ] = 1 ; this . mod = mod ; for ( int i = 1 ; i <= n ; i ++) { p [ i ] = p [ i - 1 ] * base % mod ; h [ i ] = ( h [ i - 1 ] * base + word . charAt ( i - 1 )) % mod ; } } public long query ( int l , int r ) { return ( h [ r ] - h [ l - 1 ] * p [ r - l + 1 ] % mod + mod ) % mod ; } } class Solution { private char [] s ; private int [][] pos ; private List < Integer >[] g ; private StringBuilder dfsStr = new StringBuilder (); public boolean [] findAnswer ( int [] parent , String s ) { this . s = s . toCharArray (); int n = s . length (); g = new List [ n ]; pos = new int [ n ][ 0 ]; Arrays . setAll ( g , k -> new ArrayList <>()); for ( int i = 1 ; i < n ; ++ i ) { g [ parent [ i ]]. add ( i ); } dfs ( 0 ); final int base = 13331 ; final int mod = 998244353 ; Hashing h1 = new Hashing ( dfsStr . toString (), base , mod ); Hashing h2 = new Hashing ( new StringBuilder ( dfsStr ). reverse (). toString (), base , mod ); boolean [] ans = new boolean [ n ]; for ( int i = 0 ; i < n ; ++ i ) { int l = pos [ i ][ 0 ], r = pos [ i ][ 1 ]; int k = r - l + 1 ; long v1 = h1 . query ( l , l + k / 2 - 1 ); long v2 = h2 . query ( n + 1 - r , n + 1 - r + k / 2 - 1 ); ans [ i ] = v1 == v2 ; } return ans ; } private void dfs ( int i ) { int l = dfsStr . length () + 1 ; for ( int j : g [ i ]) { dfs ( j ); } dfsStr . append ( s [ i ]); int r = dfsStr . length (); pos [ i ] = new int [] { l , r }; } }class Hashing { private: vector < long long > p ; vector < long long > h ; long long mod ; public: Hashing ( string word , long long base , int mod ) { int n = word . size (); p . resize ( n + 1 ); h . resize ( n + 1 ); p [ 0 ] = 1 ; this -> mod = mod ; for ( int i = 1 ; i <= n ; i ++ ) { p [ i ] = ( p [ i - 1 ] * base ) % mod ; h [ i ] = ( h [ i - 1 ] * base + word [ i - 1 ] - 'a' ) % mod ; } } long long query ( int l , int r ) { return ( h [ r ] - h [ l - 1 ] * p [ r - l + 1 ] % mod + mod ) % mod ; } }; class Solution { public: vector < bool > findAnswer ( vector < int >& parent , string s ) { int n = s . size (); vector < int > g [ n ]; for ( int i = 1 ; i < n ; ++ i ) { g [ parent [ i ]]. push_back ( i ); } string dfsStr ; vector < pair < int , int >> pos ( n ); auto dfs = [ & ]( auto && dfs , int i ) -> void { int l = dfsStr . size () + 1 ; for ( int j : g [ i ]) { dfs ( dfs , j ); } dfsStr . push_back ( s [ i ]); int r = dfsStr . size (); pos [ i ] = { l , r }; }; dfs ( dfs , 0 ); const int base = 13331 ; const int mod = 998244353 ; Hashing h1 ( dfsStr , base , mod ); reverse ( dfsStr . begin (), dfsStr . end ()); Hashing h2 ( dfsStr , base , mod ); vector < bool > ans ( n ); for ( int i = 0 ; i < n ; ++ i ) { auto [ l , r ] = pos [ i ]; int k = r - l + 1 ; long long v1 = h1 . query ( l , l + k / 2 - 1 ); long long v2 = h2 . query ( n - r + 1 , n - r + 1 + k / 2 - 1 ); ans [ i ] = v1 == v2 ; } return ans ; } };class Hashing : __slots__ = [ "mod" , "h" , "p" ] def __init__ ( self , s : List [ str ], base : int , mod : int ): self . mod = mod self . h = [ 0 ] * ( len ( s ) + 1 ) self . p = [ 1 ] * ( len ( s ) + 1 ) for i in range ( 1 , len ( s ) + 1 ): self . h [ i ] = ( self . h [ i - 1 ] * base + ord ( s [ i - 1 ])) % mod self . p [ i ] = ( self . p [ i - 1 ] * base ) % mod def query ( self , l : int , r : int ) -> int : return ( self . h [ r ] - self . h [ l - 1 ] * self . p [ r - l + 1 ]) % self . mod class Solution : def findAnswer ( self , parent : List [ int ], s : str ) -> List [ bool ]: def dfs ( i : int ): l = len ( dfsStr ) + 1 for j in g [ i ]: dfs ( j ) dfsStr . append ( s [ i ]) r = len ( dfsStr ) pos [ i ] = ( l , r ) n = len ( s ) g = [[] for _ in range ( n )] for i in range ( 1 , n ): g [ parent [ i ]]. append ( i ) dfsStr = [] pos = {} dfs ( 0 ) base , mod = 13331 , 998244353 h1 = Hashing ( dfsStr , base , mod ) h2 = Hashing ( dfsStr [:: - 1 ], base , mod ) ans = [] for i in range ( n ): l , r = pos [ i ] k = r - l + 1 v1 = h1 . query ( l , l + k // 2 - 1 ) v2 = h2 . query ( n - r + 1 , n - r + 1 + k // 2 - 1 ) ans . append ( v1 == v2 ) 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.