Lesson 57 of 70 6 min

MANG Problem #38: Palindrome Pairs (Hard)

Master advanced string logic by using a Trie to find all pairs of words that form a palindrome in O(N * K^2) time.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

1. Problem Statement

Mental Model

Breaking down a complex problem into its most efficient algorithmic primitive.

Given a list of unique words, return all the pairs of the distinct indices (i, j) in the given list, so that the concatenation of the two words words[i] + words[j] is a palindrome.

Input: words = ["abcd","dcba","lls","s","sssll"]
Output: [[0,1],[1,0],[3,2],[2,4]]

2. Approach: Reversed Word Trie

A brute force $O(N^2 \times K)$ solution checking every pair is too slow.

If word1 + word2 is a palindrome, then word1 must be the reverse of word2 (or parts of them must be palindromes themselves).

  1. Build a Trie: Insert the reverse of every word into a Trie. Store the word's index at the leaf.
  2. Search: For every word in the list, search it in the Trie.
    • Case 1: word matches a reverse word exactly (e.g., "abc" and "cba").
    • Case 2: word is longer. We reach a leaf in the Trie. The rest of word must be a palindrome (e.g., "abcdc" and "ba").
    • Case 3: word is shorter. The Trie path continues. The remaining path in the Trie must form a palindrome (e.g., "ab" and "cdcba").

To optimize Case 3, every TrieNode stores a list of indices representing words whose remaining suffix forms a palindrome.

3. Java Implementation

class TrieNode {
    TrieNode[] next = new TrieNode[26];
    int wordIndex = -1;
    List<Integer> palindromePrefixes = new ArrayList<>();
}

public List<List<Integer>> palindromePairs(String[] words) {
    TrieNode root = new TrieNode();
    // 1. Build Trie with reversed words
    for (int i = 0; i < words.length; i++) {
        String rev = new StringBuilder(words[i]).reverse().toString();
        TrieNode curr = root;
        for (int j = 0; j < rev.length(); j++) {
            if (isPalindrome(rev, j, rev.length() - 1)) {
                curr.palindromePrefixes.add(i);
            }
            int idx = rev.charAt(j) - 'a';
            if (curr.next[idx] == null) curr.next[idx] = new TrieNode();
            curr = curr.next[idx];
        }
        curr.wordIndex = i;
        curr.palindromePrefixes.add(i);
    }

    List<List<Integer>> res = new ArrayList<>();
    // 2. Search each word
    for (int i = 0; i < words.length; i++) {
        TrieNode curr = root;
        String w = words[i];
        for (int j = 0; j < w.length(); j++) {
            if (curr.wordIndex != -1 && curr.wordIndex != i && isPalindrome(w, j, w.length() - 1)) {
                res.add(Arrays.asList(i, curr.wordIndex));
            }
            curr = curr.next[w.charAt(j) - 'a'];
            if (curr == null) break;
        }
        if (curr != null && curr.palindromePrefixes.size() > 0) {
            for (int pIdx : curr.palindromePrefixes) {
                if (i != pIdx) res.add(Arrays.asList(i, pIdx));
            }
        }
    }
    return res;
}

private boolean isPalindrome(String s, int left, int right) {
    while (left < right) if (s.charAt(left++) != s.charAt(right--)) return false;
    return true;
}

4. 5-Minute "Video-Style" Walkthrough

  1. The Reverse Trick: If I have "bat" and I want to form a palindrome, I need something ending in "tab". By storing the reverse of words ("tab" becomes "bat"), finding a match becomes a standard prefix search!
  2. The Partial Match: If I have "batman", I can match with "tab" ONLY if the remaining part "man" is a palindrome (it's not).
  3. The Fast Lookup: The palindromePrefixes list in the Trie node is the ultimate optimization. It pre-calculates which words have palindromic remainders, saving us from checking it during the search phase.

5. Verbal Interview Script (Staff Tier)

Interviewer: "Walk me through your optimization strategy for this problem."

You: "When approaching this type of challenge, my primary objective is to identify the underlying Monotonicity or Optimal Substructure that allow us to bypass a naive brute-force search. In my implementation of 'MANG Problem #38: Palindrome Pairs (Hard)', I focused on reducing the time complexity by leveraging a Dynamic Programming state transition. This allows us to handle input sizes that would typically cause a standard O(N^2) approach to fail. Furthermore, I prioritized memory efficiency by optimizing the DP state to use only a 1D array. This ensures that the application remains performant even under heavy garbage collection pressure in a high-concurrency Java environment."

6. Staff-Level Interview Follow-Ups

Once you provide the optimized solution, a senior interviewer at Google or Meta will likely push you further. Here is how to handle the most common follow-ups:

Follow-up 1: "How does this scale to a Distributed System?"

If the input data is too large to fit on a single machine (e.g., billions of records), we would move from a single-node algorithm to a MapReduce or Spark-based approach. We would shard the data based on a consistent hash of the keys and perform local aggregations before a global shuffle and merge phase, similar to the logic used in External Merge Sort.

Follow-up 2: "What are the Concurrency implications?"

In a multi-threaded Java environment, we must ensure that our state (e.g., the DP table or the frequency map) is thread-safe. While we could use synchronized blocks, a higher-performance approach would be to use AtomicVariables or ConcurrentHashMap. For problems involving shared arrays, I would consider a Work-Stealing pattern where each thread processes an independent segment of the data to minimize lock contention.

7. Performance Nuances (The Java Perspective)

  1. Autoboxing Overhead: When using HashMap<Integer, Integer>, Java performs autoboxing which creates thousands of Integer objects on the heap. In a performance-critical system, I would use a primitive-specialized library like fastutil or Trove to use Int2IntMap, significantly reducing GC pauses.
  2. Recursion Depth: As discussed in the code, recursive solutions are elegant but risky for deep inputs. I always ensure the recursion depth is bounded, or I rewrite the logic to be Iterative using an explicit stack on the heap to avoid StackOverflowError.

Key Takeaways

  • Case 1: word matches a reverse word exactly (e.g., "abc" and "cba").
  • Case 2: word is longer. We reach a leaf in the Trie. The rest of word must be a palindrome (e.g., "abcdc" and "ba").
  • Case 3: word is shorter. The Trie path continues. The remaining path in the Trie must form a palindrome (e.g., "ab" and "cdcba").

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.