3327. Check if DFS Strings Are Palindromes
Problem Description
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
y
ofx
in 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
dfsStr
and calldfs(i)
. - If the resulting string
dfsStr
is a palindrome, then setanswer[i]
totrue
. Otherwise, setanswer[i]
tofalse
.
Return the array answer
.
Intuition
To solve this problem, we can employ a combination of Depth-First Search (DFS) and string hashing. The process begins by constructing the entire dfsStr
for the tree starting from the root node. During the DFS traversal, we also track the interval [l, r]
representing the positions within dfsStr
for each node's subtree.
The primary task is to determine if the string formed by this subtree is a palindrome. Using direct character comparisons for palindromes can be inefficient for large inputs, hence using string hashing provides an efficient approach. By creating hash values for each node's subtree in dfsStr
and its reverse, we can quickly compare these values to check for palindromes.
This approach effectively checks for palindrome properties within subtrees by leveraging properties of hash functions to reduce computational complexity. The hash values for the normal and reversed segments of dfsStr
are compared to establish whether they are identical, indicating a palindrome structure.
Learn more about Tree and Depth-First Search patterns.
Solution Approach
To tackle the problem efficiently, the solution utilizes a combination of Depth-First Search (DFS) and string hashing, which optimizes the palindrome-checking process.
Step-by-Step Approach:
-
Tree Construction:
- Represent the tree using an adjacency list
g
, where each node points to its children.
- Represent the tree using an adjacency list
-
DFS Traversal:
- Implement a recursive
dfs
function starting from node0
(root) to construct thedfsStr
string. - While traversing, record the starting and ending positions
[l, r]
of each node's contributions indfsStr
. These positions are stored in the dictionarypos
.
- Implement a recursive
-
String Hashing:
- Use a
Hashing
class to precompute hash values fordfsStr
and its reverse. This class works by maintaining hash values for substrings, which makes palindrome checks efficient. - We choose a base, typically a large prime such as
13331
, and another large prime998244353
as the modulus.
- Use a
-
Palindrome Check:
- For each node, retrieve its corresponding interval
[l, r]
frompos
. - Calculate the hash value for the segment from
l
tol + k//2
ofdfsStr
and its reverse counterpart using the hash functions. - If both hash values match, it indicates the segment is a palindrome.
- For each node, retrieve its corresponding interval
-
Build the Answer:
- For each node, append
true
to the answer list if the segment is a palindrome, otherwisefalse
.
- For each node, append
This hashing-based technique reduces the time complexity significantly when compared to character-by-character palindrome checks, making it possible to handle larger input sizes efficiently.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach step-by-step.
Example:
Suppose we have a tree with n = 5
nodes and the following parent
array and string s
:
parent = [-1, 0, 0, 1, 1]
s = "abcba"
This tree structure can be visualized as:
0 (a) / \ 1 2 / \ 3 4
- Node
0
is the root and has two children:1
and2
. - Node
1
has two children:3
and4
.
Step 1: Tree Construction
We first represent the tree using an adjacency list g
. Here is how the tree connections are depicted:
g[0] = [1, 2]
g[1] = [3, 4]
g[2] = []
g[3] = []
g[4] = []
Step 2: DFS Traversal
Define the recursive function dfs(x)
to build the dfsStr
:
- Start at the root (
x = 0
). - For each child
y
ofx
in increasing order, recursively calldfs(y)
. - Append the character
s[x]
todfsStr
.
Let's process the tree:
- Starting at
0
: Traverse through children1
and2
. - Starting at
1
: Traverse through children3
and4
. - Visit
3
:dfsStr = "b"
. - Visit
4
:dfsStr = "ba"
. - Node
1
:dfsStr = "bac"
. - Visit
2
:dfsStr = "bacb"
. - Node
0
:dfsStr = "bacba"
.
Step 3: String Hashing
Implement a Hashing
class to calculate and store hash values efficiently for dfsStr = "bacba"
and its reverse. Precompute hashes using a base and modulus to support constant-time comparisons.
Step 4: Palindrome Check
For each node:
- Node
0
: Check the entiredfsStr "bacba"
with its reverse. This matches, indicating a palindrome. - Node
1
: Check subtree segmentdfsStr[0:3] = "bac"
; not a palindrome. - Node
2
: Check subtree segmentdfsStr[3:4] = "b"
; it's a palindrome. - Node
3
: Check subtree segmentdfsStr[0:1] = "b"
; it's a palindrome. - Node
4
: Check subtree segmentdfsStr[1:2] = "a"
; it's a palindrome.
Step 5: Build the Answer
The resultant boolean array answer
is [true, false, true, true, true]
.
Thus, for each node, the segment checked is palindromic, except for node 1
, which is not. This approach leverages efficient hashing to ensure swift palindrome verification.
Solution Implementation
1from typing import List
2
3class Hashing:
4 __slots__ = ["mod", "h", "p"]
5
6 def __init__(self, s: List[str], base: int, mod: int):
7 # Initialize modulo, prefix hash array, and power array
8 self.mod = mod
9 self.h = [0] * (len(s) + 1)
10 self.p = [1] * (len(s) + 1)
11
12 # Precompute prefix hashes and powers of the base
13 for i in range(1, len(s) + 1):
14 self.h[i] = (self.h[i - 1] * base + ord(s[i - 1])) % mod
15 self.p[i] = (self.p[i - 1] * base) % mod
16
17 def query(self, l: int, r: int) -> int:
18 # Calculate hash of the substring s[l..r], using precomputed values
19 return (self.h[r] - self.h[l - 1] * self.p[r - l + 1]) % self.mod
20
21
22class Solution:
23 def findAnswer(self, parent: List[int], s: str) -> List[bool]:
24 # Helper function to perform DFS and record positions
25 def dfs(i: int):
26 l = len(dfs_str) + 1 # Start position for this node's string segment
27 for j in graph[i]:
28 dfs(j) # Recursively process children
29 dfs_str.append(s[i]) # Append character corresponding to current node
30 r = len(dfs_str) # End position for this node's string segment
31 positions[i] = (l, r) # Store the start and end position
32
33 n = len(s) # Number of nodes
34 graph = [[] for _ in range(n)] # Adjacency list of the tree
35
36 # Construct tree from parent list
37 for i in range(1, n):
38 graph[parent[i]].append(i)
39
40 dfs_str = [] # List to record the DFS traversal string
41 positions = {} # Map node to its corresponding string segment positions
42 dfs(0) # Start DFS from the root node (0)
43
44 base, mod = 13331, 998244353 # Base and modulus for hashing
45 h1 = Hashing(dfs_str, base, mod) # Hashing for normal order
46 h2 = Hashing(dfs_str[::-1], base, mod) # Hashing for reversed order
47
48 ans = [] # List to store the boolean results
49 for i in range(n):
50 l, r = positions[i] # Get positions in DFS string
51 k = r - l + 1 # Length of the segment
52 # Calculate hash values for comparing segments from the start and from the end
53 v1 = h1.query(l, l + k // 2 - 1)
54 v2 = h2.query(n - r + 1, n - r + 1 + k // 2 - 1)
55 ans.append(v1 == v2) # Check if the first half is equal to the reverse of the first half
56
57 return ans # Return the list of palindrome checks for each subtree
58
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5// The Hashing class computes hash values for substrings using polynomial rolling hash
6class Hashing {
7 private final long[] p; // Array to store powers of base modulo mod
8 private final long[] h; // Array to store hash values up to each index
9 private final long mod; // The modulus value for hash computation
10
11 // Constructor initializes arrays and computes hash and power values
12 public Hashing(String word, long base, int mod) {
13 int n = word.length();
14 p = new long[n + 1];
15 h = new long[n + 1];
16 p[0] = 1; // Set the first power as 1 (base^0)
17 this.mod = mod; // Store mod value
18 for (int i = 1; i <= n; i++) {
19 p[i] = p[i - 1] * base % mod; // Compute powers of base
20 h[i] = (h[i - 1] * base + word.charAt(i - 1)) % mod; // Compute hash values
21 }
22 }
23
24 // Method to get hash value of a substring from index l to r (1-based index)
25 public long query(int l, int r) {
26 return (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod;
27 }
28}
29
30// The Solution class provides a method to check palindromic subtree strings in a tree-like character array
31class Solution {
32 private char[] s; // Character array to store the string representation of nodes
33 private int[][] pos; // Array to store start and end positions of characters in the DFS traversal
34 private List<Integer>[] g; // Adjacency list to store the tree structure
35 private StringBuilder dfsStr = new StringBuilder(); // Builder to construct sequence of characters in DFS order
36
37 // Method to determine if the substring from each node in the tree forms a palindrome
38 public boolean[] findAnswer(int[] parent, String s) {
39 this.s = s.toCharArray(); // Convert string to char array for processing
40 int n = s.length();
41 g = new List[n]; // Initialize adjacency list
42 pos = new int[n][0]; // Initialize position array for each node
43 Arrays.setAll(g, k -> new ArrayList<>()); // Fill adjacency list with empty lists
44 for (int i = 1; i < n; ++i) {
45 g[parent[i]].add(i); // Populate adjacency list
46 }
47 dfs(0); // Perform DFS starting from root node
48 final int base = 13331; // Base for hashing
49 final int mod = 998244353; // Modulus for hashing
50 Hashing h1 = new Hashing(dfsStr.toString(), base, mod); // Hash object for direct order
51 Hashing h2 = new Hashing(new StringBuilder(dfsStr).reverse().toString(), base, mod); // Hash object for reverse order
52 boolean[] ans = new boolean[n]; // Array to store results for each node
53 for (int i = 0; i < n; ++i) {
54 int l = pos[i][0], r = pos[i][1]; // Get positions of current node
55 int k = r - l + 1; // Compute length of substring
56 long v1 = h1.query(l, l + k / 2 - 1); // Get hash of first half in direct order
57 long v2 = h2.query(n + 1 - r, n + 1 - r + k / 2 - 1); // Get hash of first half in reverse order
58 ans[i] = v1 == v2; // Check if the two halves are equal (palindrome check)
59 }
60 return ans; // Return results
61 }
62
63 // Recursively perform depth-first search to compute positions of characters in the DFS order
64 private void dfs(int i) {
65 int l = dfsStr.length() + 1; // Store starting position for current node
66 for (int j : g[i]) {
67 dfs(j); // Recur for each child
68 }
69 dfsStr.append(s[i]); // Add current node's character to the DFS string
70 int r = dfsStr.length(); // Store ending position for current node
71 pos[i] = new int[] {l, r}; // Record positions in the pos array
72 }
73}
74
1#include <vector>
2#include <string>
3#include <algorithm>
4
5using namespace std;
6
7class Hashing {
8private:
9 vector<long long> p; // Powers of base modulo mod
10 vector<long long> h; // Hash values for prefix substrings
11 long long mod; // Modulo value for hashing
12
13public:
14 // Constructor to initialize the `Hashing` object.
15 Hashing(string word, long long base, int mod) {
16 int n = word.size();
17 p.resize(n + 1); // Initialize vector for powers of base
18 h.resize(n + 1); // Initialize vector for hash values
19 p[0] = 1; // Base case: any number to the power of zero is 1
20 this->mod = mod; // Set the modulo value
21
22 // Calculate powers of the base modulo `mod` and prefix hashes
23 for (int i = 1; i <= n; i++) {
24 p[i] = (p[i - 1] * base) % mod; // Compute base^i % mod
25 // Compute the hash value for [0, i)
26 h[i] = (h[i - 1] * base + (word[i - 1] - 'a')) % mod;
27 }
28 }
29
30 // Function to get the hash value of substring `s[l, r]`
31 long long query(int l, int r) {
32 // Use properties of modular arithmetic to extract hash value of substring
33 return (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod;
34 }
35};
36
37class Solution {
38public:
39 // Function to determine if the path to each node forms a palindrome
40 vector<bool> findAnswer(vector<int>& parent, string s) {
41 int n = s.size();
42 vector<int> g[n]; // Adjacency list for tree
43
44 // Build tree from parent array
45 for (int i = 1; i < n; ++i) {
46 g[parent[i]].push_back(i);
47 }
48
49 string dfsStr; // String to store DFS order
50 vector<pair<int, int>> pos(n); // Keep track of position indices for each node
51
52 // Depth First Search using lambda function
53 auto dfs = [&](auto&& dfs, int i) -> void {
54 int l = dfsStr.size() + 1; // Left index for current node's DFS string
55 for (int j : g[i]) {
56 dfs(dfs, j); // Recursively apply DFS
57 }
58 dfsStr.push_back(s[i]); // Add current character to DFS string
59 int r = dfsStr.size(); // Right index after adding character
60 pos[i] = {l, r}; // Store the position range for current node
61 };
62
63 // Perform DFS starting from root node (0)
64 dfs(dfs, 0);
65
66 const int base = 13331;
67 const int mod = 998244353;
68
69 // Initialize hashing objects for original and reversed DFS strings
70 Hashing h1(dfsStr, base, mod);
71 reverse(dfsStr.begin(), dfsStr.end());
72 Hashing h2(dfsStr, base, mod);
73
74 vector<bool> ans(n); // Result vector indicating if path forms palindrome