Palindrome Pairs

HARD

Description

You are given a 0-indexed array of unique strings words.

A palindrome pair is a pair of integers (i, j) such that:

  • 0 <= i, j < words.length,
  • i != j, and
  • words[i] + words[j] (the concatenation of the two strings) is a palindrome.

Return an array of all the palindrome pairs of words.

You must write an algorithm with O(sum of words[i].length) runtime complexity.

 

Example 1:

Input: words = ["abcd","dcba","lls","s","sssll"]
Output: [[0,1],[1,0],[3,2],[2,4]]
Explanation: The palindromes are ["abcddcba","dcbaabcd","slls","llssssll"]

Example 2:

Input: words = ["bat","tab","cat"]
Output: [[0,1],[1,0]]
Explanation: The palindromes are ["battab","tabbat"]

Example 3:

Input: words = ["a",""]
Output: [[0,1],[1,0]]
Explanation: The palindromes are ["a","a"]

 

Constraints:

  • 1 <= words.length <= 5000
  • 0 <= words[i].length <= 300
  • words[i] consists of lowercase English letters.

Approaches

Checkout 4 different approaches to solve Palindrome Pairs. Click on different approaches to view the approach and algorithm in detail.

Using Hash Map

We can optimize the brute-force approach by avoiding the inner loop that iterates through all other words. Instead, for each word w1, we can deduce what its partner word w2 must look like. If w1 + w2 is a palindrome, w2 must be related to the reverse of w1. We can pre-process the words into a HashMap for quick lookups.

There are three main scenarios for a pair (w1, w2):

  1. len(w1) == len(w2): w2 must be the reverse of w1.
  2. len(w1) > len(w2): w1 must be of the form P + S, where S is the reverse of w2 and P is a palindrome.
  3. len(w1) < len(w2): w2 must be of the form S + P, where S is the reverse of w1 and P is a palindrome.

We can iterate through each word and all its possible splits into a prefix and a suffix. If one part is a palindrome, we look for the reverse of the other part in the map.

Algorithm

  • Create a HashMap to store each word and its index. This allows for O(1) average time lookups.
  • Handle the edge case of an empty string. If "" exists in words, find its index. Then, iterate through all other words; if a word is a palindrome, it can form a pair with the empty string in both orders.
  • Iterate through each word w at index i in the words array.
  • For each word w, iterate through all possible split points j from 0 to w.length().
  • Split w into prefix = w.substring(0, j) and suffix = w.substring(j).
  • Case 1: Check if the prefix is a palindrome. If it is, reverse the suffix to get reversedSuffix. Look for reversedSuffix in the HashMap. If it exists at an index k different from i, then (k, i) is a palindrome pair (words[k] + words[i] forms a palindrome).
  • Case 2: Check if the suffix is a palindrome. If it is, reverse the prefix to get reversedPrefix. Look for reversedPrefix in the HashMap. If it exists at an index k different from i, then (i, k) is a palindrome pair. Note: When j=0, the prefix is empty, and this case becomes checking for the reverse of the whole word. To avoid duplicates when the suffix is empty (j = w.length()), we can add a condition suffix.length() > 0 or use a Set for the results.

This approach improves upon brute force by intelligently searching for the second word in a pair. By pre-populating a HashMap with all words and their indices, we can quickly check for the existence of a required word (e.g., the reverse of a suffix). The main logic involves iterating through each word and, for each word, iterating through all possible split points. This creates a prefix and a suffix. We check if either part is a palindrome. If the prefix is a palindrome, we need a word that is the reverse of the suffix to place before our current word. If the suffix is a palindrome, we need a word that is the reverse of the prefix to place after our current word.

class Solution {
    public List<List<Integer>> palindromePairs(String[] words) {
        List<List<Integer>> result = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < words.length; i++) {
            map.put(words[i], i);
        }

        for (int i = 0; i < words.length; i++) {
            String word = words[i];
            for (int j = 0; j <= word.length(); j++) {
                String prefix = word.substring(0, j);
                String suffix = word.substring(j);

                // Case 1: prefix is a palindrome, look for reversed suffix
                if (isPalindrome(prefix)) {
                    String reversedSuffix = new StringBuilder(suffix).reverse().toString();
                    if (map.containsKey(reversedSuffix) && map.get(reversedSuffix) != i) {
                        result.add(Arrays.asList(map.get(reversedSuffix), i));
                    }
                }

                // Case 2: suffix is a palindrome, look for reversed prefix
                // suffix.length() != 0 to avoid duplicates when word is a palindrome
                // (e.g. "aba", prefix="aba", suffix=""). This is handled by Case 1 when j=0.
                if (isPalindrome(suffix) && suffix.length() != 0) {
                    String reversedPrefix = new StringBuilder(prefix).reverse().toString();
                    if (map.containsKey(reversedPrefix) && map.get(reversedPrefix) != i) {
                        result.add(Arrays.asList(i, map.get(reversedPrefix)));
                    }
                }
            }
        }
        return result;
    }

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

Complexity Analysis

Time Complexity: O(N * K^2), where N is the number of words and K is the maximum length. For each of the N words, we iterate through K+1 split points. Inside the loop, substring creation, reversal, and palindrome checking can take up to O(K) time.Space Complexity: O(N * K), for storing the words in the HashMap.

Pros and Cons

Pros:
  • Significantly faster than the brute-force approach for N > K.
  • Avoids the O(N^2) pair iteration.
Cons:
  • The time complexity of O(N * K^2) might still be too slow for the given constraints.
  • Requires careful handling of edge cases like empty strings and duplicate pairs.

Code Solutions

Checking out 3 solutions in different languages for Palindrome Pairs. Click on different languages to view the code.

using System.Collections.Generic ; using System.Linq ; public class Solution { public IList < IList < int >> PalindromePairs ( string [] words ) { var results = new List < IList < int >>(); var reverseDict = words . Select (( w , i ) => new { Word = w , Index = i }). ToDictionary ( w => new string ( w . Word . Reverse (). ToArray ()), w => w . Index ); for ( var i = 0 ; i < words . Length ; ++ i ) { var word = words [ i ]; for ( var j = 0 ; j <= word . Length ; ++ j ) { if ( j > 0 && IsPalindrome ( word , 0 , j - 1 )) { var suffix = word . Substring ( j ); int pairIndex ; if ( reverseDict . TryGetValue ( suffix , out pairIndex ) && i != pairIndex ) { results . Add ( new [] { pairIndex , i }); } } if ( IsPalindrome ( word , j , word . Length - 1 )) { var prefix = word . Substring ( 0 , j ); int pairIndex ; if ( reverseDict . TryGetValue ( prefix , out pairIndex ) && i != pairIndex ) { results . Add ( new [] { i , pairIndex }); } } } } return results ; } private bool IsPalindrome ( string word , int startIndex , int endIndex ) { var i = startIndex ; var j = endIndex ; while ( i < j ) { if ( word [ i ] != word [ j ]) return false ; ++ i ; -- j ; } return true ; } }

Video Solution

Watch the video walkthrough for Palindrome Pairs



Data Structures:

ArrayHash TableStringTrie

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.