Palindrome Pairs takes the "remember what you've seen in a hash map" trick and stretches it over strings. The challenge is not the lookup itself but figuring out what to look up — and that comes from splitting each word into two halves.
Problem. Given a list of distinct words, return all pairs of distinct indices (i, j) such that
words[i] + words[j] is a palindrome (reads the same forwards and backwards).
Example: words = ["bat", "tab", "cat"] → answer [[1, 0], [0, 1]] because "tab" + "bat" = "tabbat"
and "bat" + "tab" = "battab" are both palindromes.
The slow way first
The obvious idea: try every ordered pair (i, j), glue the two words together, and check whether the result is a palindrome. With n words of length up to m, that is O(n² · m) — far too slow once the list grows.
The question to ask: while I am looking at one word, what partner would complete a palindrome — and can I find it instantly instead of scanning? A hash map of reversed words lets us answer that in O(1).
The idea: split the word, look up the reverse
For two words a and b, a + b is a palindrome exactly when we can split one of them so that one side is itself a palindrome and the reverse of the other side equals the partner word. So we pre-build a map rev of every reversed word to its index, then for each word try every split point.
The two cases per split: if the prefix is a palindrome and the suffix exists reversed in the map, the partner goes in front; if the suffix is a palindrome and the prefix exists, the partner goes behind. Guard against pairing a word with itself.
Walk through it
Step through the animation. First we build rev (each reversed word to its index). Then the pointer i scans the words. For "bat", the empty-prefix split finds "bat" in the map at index 1, giving the pair [1, 0]. For "tab", the same split finds "tab" at index 0, giving [0, 1]. "cat" matches nothing.
Pseudocode
build rev = { reverse(word): index } for every word
pairs = empty list
for each index i with word w:
for every split point k from 0 to len(w):
pre, suf = w[:k], w[k:]
if pre is a palindrome and reverse(suf) i.e. suf is a key in rev and rev[suf] != i:
add [rev[suf], i] # partner goes in front
if k > 0 and suf is a palindrome and pre is a key in rev and rev[pre] != i:
add [i, rev[pre]] # partner goes behind
return pairsThe Python solution
def palindrome_pairs(words):
rev = {w[::-1]: i for i, w in enumerate(words)}
pairs = []
for i, w in enumerate(words):
for k in range(len(w) + 1):
pre, suf = w[:k], w[k:]
if pre == pre[::-1] and suf in rev and rev[suf] != i:
pairs.append([rev[suf], i])
if k and suf == suf[::-1] and pre in rev and rev[pre] != i:
pairs.append([i, rev[pre]])
return pairsrevmaps each reversed word to its index, so a lookup is O(1).- For each word we try every split into
pre(prefix) andsuf(suffix), including the empty ones. - Line 7 is the front case: if
preis a palindrome and the suffix already exists reversed inrev, that stored word goes in front to complete the palindrome. - Line 9 is the back case: if
sufis a palindrome and the prefix exists reversed, the stored word goes behind. Thekguard (and the empty-prefix check on line 7) avoids counting the same pair twice. rev[...] != istops a word from pairing with itself.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (every ordered pair) | O(n² · m) (moderate) | glue and check each pair |
| Reversed-word map (this solution) | O(n · m²) (moderate) | n words, m splits, O(m) work per split |
O(n · m) (moderate)We trade the n² factor for an m² one — a big win when there are many words but they are short. The map costs O(n · m) space, which is the standard "remember everything so lookups are instant" trade.
When this pattern shows up
When a string problem asks you to match one piece against another, think: can I pre-index the pieces in a hash map so the match is O(1)? Reversing, hashing prefixes/suffixes, or storing words in a trie are all variations of the same move you first met in Two Sum.
Two traps: handle the empty string and the empty split (they are palindromes and pair with any
palindrome word), and make sure you never double-count or pair a word with itself — that is what the
k guard and the rev[...] != i check are for.
Practice
For words = ['bat', 'tab', 'cat'], when i points at 'tab' and we take the empty-prefix split, what do we look up in rev and what pair do we get?
1. What does the rev map store?
2. Why do we split each word into a prefix and a suffix?
3. What prevents a word from being paired with itself?
4. Compared to brute force O(n squared times m), what is the time of the reversed-word-map solution?