Facebook Pixel

3327. Check if DFS Strings Are Palindromes

HardTreeDepth-First SearchArrayHash TableStringHash Function
Leetcode Link

Problem Description

You have a tree with n nodes numbered from 0 to n - 1, where node 0 is the root. The tree structure is given by an array parent where parent[i] indicates the parent of node i. Since node 0 is the root, parent[0] = -1.

Each node i has a character s[i] associated with it.

The problem defines a special DFS traversal that builds a string dfsStr:

  • Starting from any node x, the DFS function:
    1. First recursively visits all children of x in increasing order of their node numbers
    2. After visiting all children, appends the character s[x] to dfsStr

Your task is to determine, for each node i from 0 to n - 1, whether starting a DFS from that node produces a palindrome string.

The output should be a boolean array answer where answer[i] is true if starting DFS from node i produces a palindrome in dfsStr, and false otherwise.

Example of DFS traversal: If we have a tree where node 0 has children 1 and 2, and node 1 has child 3:

  • DFS from node 0 would visit: node 1 → node 3 → append s[3] → append s[1] → node 2 → append s[2] → append s[0]
  • The resulting dfsStr would be: s[3] + s[1] + s[2] + s[0]

The solution uses string hashing to efficiently check palindromes. It first computes the complete DFS string starting from root (node 0) and records the position interval [l, r] for each subtree. Then it uses polynomial rolling hash to compare the forward and reverse hash values of each subtree's string segment to determine if it forms 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 involves a tree structure with nodes (0 to n-1) and edges defined by the parent-child relationships in the parent array.

Is it a tree?

  • Yes: The problem states we have a tree rooted at node 0. The parent array defines a tree structure where each node (except the root) has exactly one parent, and parent[0] = -1 indicates node 0 is the root.

DFS

  • Yes: This leads us directly to DFS as the appropriate algorithm.

Conclusion: The flowchart confirms that DFS (Depth-First Search) is the correct approach for this problem.

Why DFS is the Perfect Fit

The problem explicitly requires a DFS traversal pattern:

  1. Tree Traversal Requirement: We need to traverse each subtree starting from different nodes to build the dfsStr string.

  2. Specific Traversal Order: The problem mandates visiting children in increasing order of their node numbers, then appending the current node's character - this is a post-order DFS traversal.

  3. Subtree Processing: For each node i, we need to process its entire subtree to generate the corresponding substring. DFS naturally handles subtree exploration.

  4. Recursive Nature: The problem description itself defines a recursive DFS function that:

    • Recursively visits all children first
    • Then processes the current node (appends its character)

The solution implements DFS to:

  • Build the complete dfsStr starting from the root
  • Track the position intervals [l, r] for each subtree's contribution to the string
  • Use these intervals with string hashing to efficiently check palindromes
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we don't need to run DFS from every single node separately - that would be inefficient. Instead, we can observe that when we run DFS from any node, we're essentially getting a substring of what we'd get from a complete DFS starting from a higher ancestor.

Think about it this way: when we perform DFS from the root (node 0), we visit every node in the tree and build the complete dfsStr. Now, if we were to start DFS from any other node i, we'd only visit node i and its subtree - this produces a substring that already exists within the complete dfsStr.

This leads to our first breakthrough: we only need to run DFS once from the root, and during this traversal, we can track where each subtree's contribution starts and ends in the final string. For each node i, we record its interval [l, r] representing the positions in dfsStr that would be generated if we started DFS from node i.

The second challenge is checking if each substring is a palindrome. A naive approach would check character by character for each substring, but this is slow. Here's where string hashing comes in - it's a technique that converts strings into numerical values (hashes) for fast comparison.

For palindrome checking, we need a clever trick: a string is a palindrome if its first half matches its second half when reversed. So we:

  1. Compute hash values for the forward string (dfsStr)
  2. Compute hash values for the reversed string (dfsStr[::-1])
  3. For each substring defined by interval [l, r], compare the hash of its first half with the hash of the corresponding position in the reversed string

The beauty of this approach is that we transform an O(n²) problem (running DFS from each node and checking palindromes) into an O(n) solution (one DFS traversal plus O(1) palindrome checks using precomputed hashes).

The polynomial rolling hash with base 13331 and modulo 998244353 ensures very low collision probability while keeping computations efficient.

Learn more about Tree and Depth-First Search patterns.

Solution Approach

The implementation consists of three main components: DFS traversal, string hashing, and palindrome verification.

Step 1: Build the Tree Structure

First, we construct an adjacency list representation of the tree from the parent array:

g = [[] for _ in range(n)]
for i in range(1, n):
    g[parent[i]].append(i)

This creates a list where g[i] contains all children of node i.

Step 2: DFS Traversal to Build dfsStr

We perform a single DFS from the root to build the complete dfsStr and record position intervals:

def dfs(i: int):
    l = len(dfsStr) + 1  # Start position (1-indexed)
    for j in g[i]:       # Visit children in order
        dfs(j)
    dfsStr.append(s[i])  # Append current node's character
    r = len(dfsStr)      # End position
    pos[i] = (l, r)      # Store interval for node i

The DFS follows the problem's specification:

  • Visit all children recursively in increasing order (already sorted in adjacency list)
  • After visiting children, append the current node's character
  • Track the interval [l, r] where node i's subtree contributes to dfsStr

Step 3: String Hashing Setup

The Hashing class implements polynomial rolling hash:

self.h[i] = (self.h[i - 1] * base + ord(s[i - 1])) % mod
self.p[i] = (self.p[i - 1] * base) % mod
  • h[i] stores the hash value of prefix s[0:i]
  • p[i] stores base^i % mod for efficient hash computation
  • The query(l, r) method returns the hash of substring s[l-1:r] using the formula: (h[r] - h[l-1] * p[r-l+1]) % mod

We create two hash objects:

  • h1 for the forward string dfsStr
  • h2 for the reversed string dfsStr[::-1]

Step 4: Palindrome Checking

For each node i, we check if its substring is a palindrome:

for i in range(n):
    l, r = pos[i]
    k = r - l + 1  # Length of substring
    v1 = h1.query(l, l + k // 2 - 1)  # First half hash
    v2 = h2.query(n - r + 1, n - r + 1 + k // 2 - 1)  # Corresponding reversed half
    ans.append(v1 == v2)

The palindrome check works by:

  1. Getting the substring length k
  2. Computing hash of the first half from the forward string
  3. Computing hash of the corresponding position in the reversed string
  4. If both hashes match, the substring is a palindrome

The key insight is that for a palindrome, reading the first half forward should match reading the second half backward. By using the reversed string's hash, we can check this condition in O(1) time.

Time Complexity: O(n) - one DFS traversal plus O(n) hash computations Space Complexity: O(n) - for storing the tree, dfsStr, and hash arrays

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.

Given:

  • n = 4 nodes (0, 1, 2, 3)
  • parent = [-1, 0, 0, 1] (tree structure: 0 is root, 1 and 2 are children of 0, 3 is child of 1)
  • s = "abba" (node 0 has 'a', node 1 has 'b', node 2 has 'b', node 3 has 'a')

Step 1: Build Tree Structure

Adjacency list:
g[0] = [1, 2]  (node 0's children)
g[1] = [3]     (node 1's children)
g[2] = []      (node 2 has no children)
g[3] = []      (node 3 has no children)

Step 2: DFS Traversal from Root Starting DFS from node 0:

  1. Visit node 1 (child of 0)
    • Visit node 3 (child of 1)
      • No children, append s[3]='a' → dfsStr = "a", pos[3] = (1,1)
    • Children done, append s[1]='b' → dfsStr = "ab", pos[1] = (1,2)
  2. Visit node 2 (child of 0)
    • No children, append s[2]='b' → dfsStr = "abb", pos[2] = (3,3)
  3. Children done, append s[0]='a' → dfsStr = "abba", pos[0] = (1,4)

Final dfsStr = "abba"

Position intervals:

  • pos[0] = (1,4) → substring "abba"
  • pos[1] = (1,2) → substring "ab"
  • pos[2] = (3,3) → substring "b"
  • pos[3] = (1,1) → substring "a"

Step 3: String Hashing

  • Forward hash h1 on "abba"
  • Reverse hash h2 on "abba" reversed = "abba"

Step 4: Palindrome Checking

For node 0: interval (1,4), length k=4

  • First half (positions 1-2): "ab"
  • Check against reversed positions: comparing "ab" with "ba" reversed
  • Hash values match → "abba" is palindrome → answer[0] = true

For node 1: interval (1,2), length k=2

  • First half (position 1): "a"
  • Check against reversed position: comparing "a" with "b"
  • Hash values don't match → "ab" is not palindrome → answer[1] = false

For node 2: interval (3,3), length k=1

  • Single character "b" is always palindrome → answer[2] = true

For node 3: interval (1,1), length k=1

  • Single character "a" is always palindrome → answer[3] = true

Final Answer: [true, false, true, true]

This example demonstrates how we build the complete DFS string once, track position intervals for each node's subtree, and use hashing to efficiently check palindromes without repeatedly running DFS from each node.

Solution Implementation

1from typing import List
2
3
4class Hashing:
5    """Rolling hash implementation for string hashing."""
6    __slots__ = ["mod", "hash_values", "powers"]
7
8    def __init__(self, string: List[str], base: int, mod: int):
9        """
10        Initialize the hashing object with precomputed hash values and powers.
11      
12        Args:
13            string: List of characters to hash
14            base: Base for polynomial rolling hash
15            mod: Modulo value to prevent overflow
16        """
17        self.mod = mod
18        self.hash_values = [0] * (len(string) + 1)
19        self.powers = [1] * (len(string) + 1)
20      
21        # Precompute hash values and powers of base
22        for i in range(1, len(string) + 1):
23            self.hash_values[i] = (self.hash_values[i - 1] * base + ord(string[i - 1])) % mod
24            self.powers[i] = (self.powers[i - 1] * base) % mod
25
26    def query(self, left: int, right: int) -> int:
27        """
28        Get the hash value of substring from position left to right (1-indexed).
29      
30        Args:
31            left: Starting position (1-indexed)
32            right: Ending position (1-indexed)
33          
34        Returns:
35            Hash value of the substring
36        """
37        return (self.hash_values[right] - self.hash_values[left - 1] * self.powers[right - left + 1]) % self.mod
38
39
40class Solution:
41    def findAnswer(self, parent: List[int], s: str) -> List[bool]:
42        """
43        Check if each node's subtree forms a palindrome when traversed in DFS order.
44      
45        Args:
46            parent: Parent array where parent[i] is the parent of node i
47            s: String where s[i] is the character at node i
48          
49        Returns:
50            List of booleans indicating if each node's subtree is a palindrome
51        """
52        def dfs(node: int) -> None:
53            """
54            Perform DFS traversal and build the DFS string while recording node positions.
55          
56            Args:
57                node: Current node being visited
58            """
59            # Record the starting position for this node's subtree
60            left_pos = len(dfs_string) + 1
61          
62            # Recursively visit all children
63            for child in adjacency_list[node]:
64                dfs(child)
65          
66            # Add current node's character to DFS string
67            dfs_string.append(s[node])
68          
69            # Record the ending position for this node's subtree
70            right_pos = len(dfs_string)
71            node_positions[node] = (left_pos, right_pos)
72
73        # Build adjacency list representation of the tree
74        n = len(s)
75        adjacency_list = [[] for _ in range(n)]
76        for i in range(1, n):
77            adjacency_list[parent[i]].append(i)
78      
79        # Perform DFS to build the string and record positions
80        dfs_string = []
81        node_positions = {}
82        dfs(0)
83      
84        # Initialize hashing parameters
85        BASE = 13331
86        MOD = 998244353
87      
88        # Create hash objects for forward and reverse strings
89        forward_hash = Hashing(dfs_string, BASE, MOD)
90        reverse_hash = Hashing(dfs_string[::-1], BASE, MOD)
91      
92        # Check if each node's subtree forms a palindrome
93        result = []
94        for i in range(n):
95            left, right = node_positions[i]
96            subtree_length = right - left + 1
97          
98            # Compare first half with reversed second half
99            first_half_hash = forward_hash.query(left, left + subtree_length // 2 - 1)
100            second_half_reversed_hash = reverse_hash.query(n - right + 1, n - right + 1 + subtree_length // 2 - 1)
101          
102            # Check if the hashes match (indicating a palindrome)
103            result.append(first_half_hash == second_half_reversed_hash)
104      
105        return result
106
1/**
2 * Rolling hash utility class for efficient substring hash computation
3 */
4class Hashing {
5    private final long[] powers;      // powers[i] = base^i mod mod
6    private final long[] prefixHash;  // prefixHash[i] = hash of s[0..i-1]
7    private final long mod;
8  
9    /**
10     * Initialize the hashing structure for a given string
11     * @param word The input string to be hashed
12     * @param base The base for polynomial rolling hash
13     * @param mod The modulo value to prevent overflow
14     */
15    public Hashing(String word, long base, int mod) {
16        int n = word.length();
17        powers = new long[n + 1];
18        prefixHash = new long[n + 1];
19        powers[0] = 1;
20        this.mod = mod;
21      
22        // Precompute powers of base and prefix hashes
23        for (int i = 1; i <= n; i++) {
24            powers[i] = powers[i - 1] * base % mod;
25            prefixHash[i] = (prefixHash[i - 1] * base + word.charAt(i - 1)) % mod;
26        }
27    }
28  
29    /**
30     * Query the hash value of substring from index l-1 to r-1 (1-indexed input)
31     * @param l Starting position (1-indexed)
32     * @param r Ending position (1-indexed)
33     * @return Hash value of the substring
34     */
35    public long query(int l, int r) {
36        // Calculate hash of substring using prefix difference
37        // Add mod before final mod to handle negative values
38        return (prefixHash[r] - prefixHash[l - 1] * powers[r - l + 1] % mod + mod) % mod;
39    }
40}
41
42/**
43 * Solution class to determine if each node's subtree forms a palindrome
44 */
45class Solution {
46    private char[] nodeCharacters;           // Characters at each node
47    private int[][] nodePositions;            // Start and end positions in DFS string for each node
48    private List<Integer>[] adjacencyList;   // Tree adjacency list (children of each node)
49    private StringBuilder dfsTraversal = new StringBuilder();  // DFS traversal string
50  
51    /**
52     * Find whether each node's subtree forms a palindrome string
53     * @param parent Array where parent[i] is the parent of node i
54     * @param s String where s[i] is the character at node i
55     * @return Boolean array where result[i] indicates if node i's subtree is palindrome
56     */
57    public boolean[] findAnswer(int[] parent, String s) {
58        this.nodeCharacters = s.toCharArray();
59        int n = s.length();
60      
61        // Initialize adjacency list for tree representation
62        adjacencyList = new List[n];
63        nodePositions = new int[n][0];
64        Arrays.setAll(adjacencyList, k -> new ArrayList<>());
65      
66        // Build tree structure from parent array
67        for (int i = 1; i < n; ++i) {
68            adjacencyList[parent[i]].add(i);
69        }
70      
71        // Perform DFS to build the traversal string
72        dfs(0);
73      
74        // Hash configuration
75        final int BASE = 13331;
76        final int MOD = 998244353;
77      
78        // Create hash structures for forward and reverse strings
79        Hashing forwardHash = new Hashing(dfsTraversal.toString(), BASE, MOD);
80        Hashing reverseHash = new Hashing(new StringBuilder(dfsTraversal).reverse().toString(), BASE, MOD);
81      
82        // Check palindrome for each node's subtree
83        boolean[] result = new boolean[n];
84        for (int i = 0; i < n; ++i) {
85            int left = nodePositions[i][0];
86            int right = nodePositions[i][1];
87            int subtreeLength = right - left + 1;
88          
89            // Compare first half with reversed second half
90            long firstHalfHash = forwardHash.query(left, left + subtreeLength / 2 - 1);
91            long secondHalfReversedHash = reverseHash.query(n + 1 - right, n + 1 - right + subtreeLength / 2 - 1);
92          
93            result[i] = (firstHalfHash == secondHalfReversedHash);
94        }
95      
96        return result;
97    }
98  
99    /**
100     * Depth-first search to build the DFS traversal string
101     * Children are visited first, then the current node's character is appended
102     * @param nodeIndex Current node being visited
103     */
104    private void dfs(int nodeIndex) {
105        // Record starting position (1-indexed) for this node's subtree
106        int startPosition = dfsTraversal.length() + 1;
107      
108        // Recursively visit all children
109        for (int child : adjacencyList[nodeIndex]) {
110            dfs(child);
111        }
112      
113        // Append current node's character
114        dfsTraversal.append(nodeCharacters[nodeIndex]);
115      
116        // Record ending position for this node's subtree
117        int endPosition = dfsTraversal.length();
118        nodePositions[nodeIndex] = new int[] {startPosition, endPosition};
119    }
120}
121
1class Hashing {
2private:
3    vector<long long> power;      // power[i] stores base^i mod modulo
4    vector<long long> hash;       // hash[i] stores hash value of prefix [0, i-1]
5    long long modulo;              // modulo for hash computation
6
7public:
8    /**
9     * Constructor to build polynomial rolling hash for a string
10     * @param word: input string to hash
11     * @param base: base for polynomial hash
12     * @param mod: modulo value for hash computation
13     */
14    Hashing(string word, long long base, int mod) {
15        int n = word.size();
16        power.resize(n + 1);
17        hash.resize(n + 1);
18        power[0] = 1;
19        this->modulo = mod;
20      
21        // Precompute powers of base and prefix hashes
22        for (int i = 1; i <= n; i++) {
23            power[i] = (power[i - 1] * base) % mod;
24            hash[i] = (hash[i - 1] * base + word[i - 1] - 'a') % mod;
25        }
26    }
27
28    /**
29     * Query hash value for substring [l-1, r-1] (1-indexed input)
30     * @param l: left boundary (1-indexed)
31     * @param r: right boundary (1-indexed)
32     * @return: hash value of substring
33     */
34    long long query(int l, int r) {
35        // Extract hash of substring using prefix hash difference
36        return (hash[r] - hash[l - 1] * power[r - l + 1] % modulo + modulo) % modulo;
37    }
38};
39
40class Solution {
41public:
42    /**
43     * Find if each node's subtree forms a palindrome when traversed in DFS order
44     * @param parent: parent[i] is the parent of node i (parent[0] = -1 for root)
45     * @param s: string where s[i] is the character at node i
46     * @return: vector where ans[i] indicates if subtree rooted at i forms palindrome
47     */
48    vector<bool> findAnswer(vector<int>& parent, string s) {
49        int n = s.size();
50      
51        // Build adjacency list representation of tree
52        vector<int> adjacencyList[n];
53        for (int i = 1; i < n; ++i) {
54            adjacencyList[parent[i]].push_back(i);
55        }
56      
57        string dfsString;                    // String formed by DFS traversal
58        vector<pair<int, int>> position(n);  // position[i] = {left, right} boundaries of node i's subtree in dfsString
59      
60        // DFS to build the traversal string (children first, then current node)
61        auto dfs = [&](this auto&& dfs, int node) -> void {
62            int left = dfsString.size() + 1;  // 1-indexed position
63          
64            // Recursively visit all children
65            for (int child : adjacencyList[node]) {
66                dfs(child);
67            }
68          
69            // Append current node's character
70            dfsString.push_back(s[node]);
71            int right = dfsString.size();     // 1-indexed position
72          
73            position[node] = {left, right};
74        };
75      
76        // Start DFS from root (node 0)
77        dfs(0);
78
79        // Hash configuration
80        const int BASE = 13331;
81        const int MODULO = 998244353;
82      
83        // Create hash for original DFS string
84        Hashing forwardHash(dfsString, BASE, MODULO);
85      
86        // Create hash for reversed DFS string
87        reverse(dfsString.begin(), dfsString.end());
88        Hashing reverseHash(dfsString, BASE, MODULO);
89      
90        // Check palindrome for each node's subtree
91        vector<bool> answer(n);
92        for (int i = 0; i < n; ++i) {
93            auto [left, right] = position[i];
94            int length = right - left + 1;
95          
96            // Compare first half with reversed second half
97            long long firstHalfHash = forwardHash.query(left, left + length / 2 - 1);
98            long long secondHalfReversedHash = reverseHash.query(n - right + 1, n - right + 1 + length / 2 - 1);
99          
100            answer[i] = (firstHalfHash == secondHalfReversedHash);
101        }
102      
103        return answer;
104    }
105};
106
1// Global variables and types
2let power: number[] = [];      // power[i] stores base^i mod modulo
3let hash: number[] = [];       // hash[i] stores hash value of prefix [0, i-1]
4let modulo: number = 0;        // modulo for hash computation
5
6/**
7 * Build polynomial rolling hash for a string
8 * @param word - input string to hash
9 * @param base - base for polynomial hash
10 * @param mod - modulo value for hash computation
11 */
12function initializeHashing(word: string, base: number, mod: number): void {
13    const n = word.length;
14    power = new Array(n + 1).fill(0);
15    hash = new Array(n + 1).fill(0);
16    power[0] = 1;
17    modulo = mod;
18  
19    // Precompute powers of base and prefix hashes
20    for (let i = 1; i <= n; i++) {
21        power[i] = (power[i - 1] * base) % mod;
22        hash[i] = (hash[i - 1] * base + word.charCodeAt(i - 1) - 'a'.charCodeAt(0)) % mod;
23    }
24}
25
26/**
27 * Query hash value for substring [l-1, r-1] (1-indexed input)
28 * @param l - left boundary (1-indexed)
29 * @param r - right boundary (1-indexed)
30 * @returns hash value of substring
31 */
32function queryHash(l: number, r: number): number {
33    // Extract hash of substring using prefix hash difference
34    return (hash[r] - hash[l - 1] * power[r - l + 1] % modulo + modulo) % modulo;
35}
36
37/**
38 * Find if each node's subtree forms a palindrome when traversed in DFS order
39 * @param parent - parent[i] is the parent of node i (parent[0] = -1 for root)
40 * @param s - string where s[i] is the character at node i
41 * @returns array where ans[i] indicates if subtree rooted at i forms palindrome
42 */
43function findAnswer(parent: number[], s: string): boolean[] {
44    const n = s.length;
45  
46    // Build adjacency list representation of tree
47    const adjacencyList: number[][] = Array.from({ length: n }, () => []);
48    for (let i = 1; i < n; i++) {
49        adjacencyList[parent[i]].push(i);
50    }
51  
52    let dfsString = "";                                    // String formed by DFS traversal
53    const position: [number, number][] = new Array(n);     // position[i] = [left, right] boundaries of node i's subtree in dfsString
54  
55    // DFS to build the traversal string (children first, then current node)
56    const dfs = (node: number): void => {
57        const left = dfsString.length + 1;  // 1-indexed position
58      
59        // Recursively visit all children
60        for (const child of adjacencyList[node]) {
61            dfs(child);
62        }
63      
64        // Append current node's character
65        dfsString += s[node];
66        const right = dfsString.length;     // 1-indexed position
67      
68        position[node] = [left, right];
69    };
70  
71    // Start DFS from root (node 0)
72    dfs(0);
73  
74    // Hash configuration
75    const BASE = 13331;
76    const MODULO = 998244353;
77  
78    // Create hash for original DFS string
79    initializeHashing(dfsString, BASE, MODULO);
80    const forwardPower = [...power];
81    const forwardHash = [...hash];
82  
83    // Create hash for reversed DFS string
84    const reversedDfsString = dfsString.split('').reverse().join('');
85    initializeHashing(reversedDfsString, BASE, MODULO);
86    const reversePower = [...power];
87    const reverseHash = [...hash];
88  
89    // Check palindrome for each node's subtree
90    const answer: boolean[] = new Array(n);
91    for (let i = 0; i < n; i++) {
92        const [left, right] = position[i];
93        const length = right - left + 1;
94      
95        // Compare first half with reversed second half
96        // Restore forward hash arrays for query
97        power = forwardPower;
98        hash = forwardHash;
99        const firstHalfHash = queryHash(left, left + Math.floor(length / 2) - 1);
100      
101        // Restore reverse hash arrays for query
102        power = reversePower;
103        hash = reverseHash;
104        const secondHalfReversedHash = queryHash(n - right + 1, n - right + 1 + Math.floor(length / 2) - 1);
105      
106        answer[i] = (firstHalfHash === secondHalfReversedHash);
107    }
108  
109    return answer;
110}
111

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of several main components:

  • Building the adjacency list g from the parent array takes O(n) time
  • The DFS traversal visits each node exactly once, appending each character to dfsStr, which takes O(n) time total
  • Initializing the two Hashing objects (h1 and h2) each takes O(n) time to compute the prefix hashes and powers
  • The final loop iterates through all n nodes, and for each node:
    • Retrieves the position bounds in O(1)
    • Performs two hash queries, each in O(1) time
    • Compares the hash values in O(1)

Therefore, the overall time complexity is O(n) + O(n) + O(n) + O(n) + O(n) = O(n).

Space Complexity: O(n)

The space usage includes:

  • The adjacency list g storing the tree structure: O(n)
  • The dfsStr list containing all characters from the DFS traversal: O(n)
  • The pos dictionary storing position pairs for each node: O(n)
  • Two Hashing objects, each maintaining arrays h and p of size n+1: O(n) each
  • The ans list storing boolean results: O(n)
  • The recursion stack for DFS in the worst case (skewed tree): O(n)

The total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Hash Collision Leading to False Positives

The Pitfall: The current implementation uses a single hash function with one base and modulo value. While polynomial rolling hash is efficient, it can produce collisions where different strings yield the same hash value, causing the algorithm to incorrectly identify non-palindromes as palindromes.

Example Scenario: Two different substrings might hash to the same value by chance, especially with a single modulo operation. This is particularly problematic in competitive programming or when dealing with adversarial inputs.

Solution: Use double hashing with two different prime moduli to significantly reduce collision probability:

class DoubleHashing:
    def __init__(self, string: List[str], base: int):
        self.mod1 = 10**9 + 7
        self.mod2 = 998244353
        n = len(string)
      
        self.h1 = [0] * (n + 1)
        self.h2 = [0] * (n + 1)
        self.p1 = [1] * (n + 1)
        self.p2 = [1] * (n + 1)
      
        for i in range(1, n + 1):
            self.h1[i] = (self.h1[i - 1] * base + ord(string[i - 1])) % self.mod1
            self.h2[i] = (self.h2[i - 1] * base + ord(string[i - 1])) % self.mod2
            self.p1[i] = (self.p1[i - 1] * base) % self.mod1
            self.p2[i] = (self.p2[i - 1] * base) % self.mod2
  
    def query(self, left: int, right: int) -> tuple:
        hash1 = (self.h1[right] - self.h1[left - 1] * self.p1[right - left + 1]) % self.mod1
        hash2 = (self.h2[right] - self.h2[left - 1] * self.p2[right - left + 1]) % self.mod2
        return (hash1, hash2)

Then modify the palindrome check to compare both hash values:

v1 = forward_hash.query(left, left + subtree_length // 2 - 1)
v2 = reverse_hash.query(n - right + 1, n - right + 1 + subtree_length // 2 - 1)
result.append(v1[0] == v2[0] and v1[1] == v2[1])

2. Integer Overflow in Hash Computation

The Pitfall: Even with modulo operations, intermediate calculations like self.h[i - 1] * base can overflow before the modulo is applied, especially in languages with fixed integer sizes or when using very large base values.

Solution: Ensure all intermediate operations stay within bounds by applying modulo more frequently:

def __init__(self, string: List[str], base: int, mod: int):
    self.mod = mod
    self.hash_values = [0] * (len(string) + 1)
    self.powers = [1] * (len(string) + 1)
  
    for i in range(1, len(string) + 1):
        # Apply modulo after each operation to prevent overflow
        self.hash_values[i] = ((self.hash_values[i - 1] % mod) * (base % mod)) % mod
        self.hash_values[i] = (self.hash_values[i] + ord(string[i - 1])) % mod
        self.powers[i] = ((self.powers[i - 1] % mod) * (base % mod)) % mod

3. Off-by-One Errors in Index Calculation

The Pitfall: The mixing of 0-indexed arrays with 1-indexed position tracking can easily lead to off-by-one errors. The current code uses 1-indexed positions for the hash queries while Python uses 0-indexed arrays, creating confusion.

Solution: Add clear documentation and validation:

def query(self, left: int, right: int) -> int:
    """
    Get hash of substring [left, right] where positions are 1-indexed.
    """
    assert 1 <= left <= right <= len(self.hash_values) - 1, f"Invalid range: [{left}, {right}]"
  
    # Calculate with clear variable names
    substring_length = right - left + 1
    hash_value = (self.hash_values[right] - 
                  self.hash_values[left - 1] * self.powers[substring_length]) % self.mod
  
    # Handle negative modulo results
    return (hash_value + self.mod) % self.mod

4. Incorrect Handling of Odd-Length Palindromes

The Pitfall: The current implementation only compares the first half with the reversed second half using integer division (k // 2). For odd-length strings, this ignores the middle character, which could lead to incorrect results.

Solution: Handle odd and even length cases explicitly:

for i in range(n):
    left, right = node_positions[i]
    subtree_length = right - left + 1
  
    if subtree_length == 1:
        # Single character is always a palindrome
        result.append(True)
    else:
        # For even length: compare two halves
        # For odd length: compare two halves excluding middle
        half_length = subtree_length // 2
      
        first_half = forward_hash.query(left, left + half_length - 1)
      
        # Adjust for odd length by potentially skipping middle character
        if subtree_length % 2 == 0:
            second_half_start = n - right + 1
        else:
            second_half_start = n - right + 2  # Skip middle character in reverse
      
        second_half = reverse_hash.query(second_half_start, 
                                        second_half_start + half_length - 1)
      
        result.append(first_half == second_half)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

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

Load More