#336 Palindrome Pairs
the problem
In plain words
You get an array of distinct words made of lowercase letters (a word may be empty). Return every pair of indices [i, j] with i ≠ j such that words[i] followed by words[j] reads the same forwards and backwards. The pairs can be in any order.
in words = ["abcd","dcba","lls","s","sssll"]
out [[0,1],[1,0],[3,2],[2,4]]
"abcddcba", "dcbaabcd", "slls", "llssssll".
in words = ["bat","tab","cat"]
out [[0,1],[1,0]]
in words = ["a",""]
out [[0,1],[1,0]]
The empty word pairs with every palindrome, on both sides.
- 1 ≤ words.length ≤ 5000
- 0 ≤ words[i].length ≤ 300
- Lowercase English letters; all words are distinct
your turn
Try it before you peek
Read the examples, say the brute force out loud, then write code: here or on LeetCode ↗. Stuck for 20 minutes? Open one hint at a time.
Solve it in the browser
LeetCode-style: complete the class. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
Solutions are hidden so you can try first. Even 20 minutes of real effort makes the answer stick far better than reading it.
Marking the problem solved, or opening every hint, also shows it.
the aha
Key insight
patternTrie (prefix tree)Hash lookup (seen before?)
If words[i] + words[j] is a palindrome, the beginning of words[i] is words[j] read backwards, and whatever is left over in the middle reads the same both ways. So store every word reversed — then reading words[i] forwards down a trie of reversed words checks it against all j at once.
solutions
From brute force to optimal
Concatenate every words[i] + words[j] (i ≠ j) and check it with two pointers.
- For every i and every j ≠ i: s = words[i] + words[j].
- Two pointers from both ends; add [i, j] if all characters match.
n² ordered pairs, each built and checked in up to 2k characters — 25 million pairs for n = 5000.
One concatenated string at a time (output excluded).
class Solution { public List<List<Integer>> palindromePairs(String[] words) { List<List<Integer>> pairs = new ArrayList<>(); for (int i = 0; i < words.length; i++) for (int j = 0; j < words.length; j++) if (i != j && isPalindrome(words[i] + words[j])) pairs.add(List.of(i, j)); return pairs; } private boolean isPalindrome(String s) { for (int lo = 0, hi = s.length() - 1; lo < hi; lo++, hi--) if (s.charAt(lo) != s.charAt(hi)) return false; return true; }}Cut words[i] into left + right at every position. If left is some word reversed and right is a palindrome, words[i] + that word works (and the mirror case for other + words[i]).
- Map every reversed word to its index.
- For each word and each cut 0..k: left = w[0..cut), right = w[cut..].
- map has left and right is a palindrome → pair [i, map(left)].
- cut > 0, map has right and left is a palindrome → pair [map(right), i] (cut > 0 avoids counting the whole word twice).
n words × (k + 1) cuts × an O(k) substring, hash and palindrome check.
The map of reversed words.
class Solution { public List<List<Integer>> palindromePairs(String[] words) { Map<String, Integer> indexOfReversed = new HashMap<>(); // reversed word → its index for (int i = 0; i < words.length; i++) indexOfReversed.put(new StringBuilder(words[i]).reverse().toString(), i); List<List<Integer>> pairs = new ArrayList<>(); for (int i = 0; i < words.length; i++) { String w = words[i]; for (int cut = 0; cut <= w.length(); cut++) { String left = w.substring(0, cut), right = w.substring(cut); // w + other: other must be reverse(left), and the right part must read the same both ways Integer j = indexOfReversed.get(left); if (j != null && j != i && isPalindrome(right)) pairs.add(List.of(i, j)); // other + w: other must be reverse(right), and the left part must be a palindrome // (cut > 0 so the whole-word match isn't counted twice) Integer k = indexOfReversed.get(right); if (cut > 0 && k != null && k != i && isPalindrome(left)) pairs.add(List.of(k, i)); } } return pairs; } private boolean isPalindrome(String s) { for (int lo = 0, hi = s.length() - 1; lo < hi; lo++, hi--) if (s.charAt(lo) != s.charAt(hi)) return false; return true; }}Insert each word back to front; at every node remember which words have a palindrome left in their unread part. Walk each word forwards and collect matches.
- Insert words[j] reversed. Before each step, if the part not yet inserted (a prefix of words[j]) is a palindrome, add j to the node's list; at the end, add j to the last node's list and mark the word ending there.
- Walk words[i] forwards. At each node where a reversed word j ends, if the rest of words[i] is a palindrome → [i, j].
- If words[i] is used up at a node, pair it with every j in that node's list (j ≠ i).
Each word is inserted and walked once (k steps each), but every step may run an O(k) palindrome check. No substrings or hashing, and walks stop where no reversed word continues.
Up to one trie node per letter, plus index lists (each word appears in at most k + 1 of them).
class Solution { private static class Node { Node[] children = new Node[26]; int wordEndsHere = -1; // index of the word whose reversal ends here List<Integer> restIsPalindrome = new ArrayList<>(); // words passing here whose unread part is a palindrome } public List<List<Integer>> palindromePairs(String[] words) { Node root = new Node(); for (int j = 0; j < words.length; j++) addReversed(root, words[j], j); List<List<Integer>> pairs = new ArrayList<>(); for (int i = 0; i < words.length; i++) collect(root, words, i, pairs); return pairs; } /** Inserts words[j] back to front. Before each step, notes j if the part not yet inserted is a palindrome. */ private void addReversed(Node root, String word, int j) { Node cur = root; for (int k = word.length() - 1; k >= 0; k--) { if (isPalindrome(word, 0, k)) cur.restIsPalindrome.add(j); // word[0..k] is still unread int c = word.charAt(k) - 'a'; if (cur.children[c] == null) cur.children[c] = new Node(); cur = cur.children[c]; } cur.restIsPalindrome.add(j); // nothing left: the empty rest is a palindrome cur.wordEndsHere = j; } /** Reads words[i] forwards down the trie of reversed words: every match is a pair (i, j). */ private void collect(Node root, String[] words, int i, List<List<Integer>> pairs) { String w = words[i]; Node cur = root; for (int k = 0; k < w.length(); k++) { // case 1: a whole reversed word j ended, and the rest of words[i] is a palindrome if (cur.wordEndsHere >= 0 && cur.wordEndsHere != i && isPalindrome(w, k, w.length() - 1)) { pairs.add(List.of(i, cur.wordEndsHere)); } cur = cur.children[w.charAt(k) - 'a']; if (cur == null) return; // no reversed word continues this way } // case 2: words[i] is used up; every word j whose unread beginning is a palindrome completes it for (int j : cur.restIsPalindrome) { if (j != i) pairs.add(List.of(i, j)); } } private boolean isPalindrome(String s, int lo, int hi) { while (lo < hi) if (s.charAt(lo++) != s.charAt(hi--)) return false; return true; }}in the room
How to say it in the interview
talk track
- Brute force first: n² pairs, each checked with two pointers — say the cost out loud.
- Then the condition: words[i] = A + B with A equal to words[j] reversed and B a palindrome — or the mirror case.
- That suggests looking words up reversed: a HashMap is the quickest to code; the trie walks each word once and handles every split together.
- Call out the traps: the empty word, a word with its exact reverse, and never pairing a word with itself.
traps
- Reporting a pair twice when a word and its reverse are both present — count the "whole word" split on one side only.
- Pairing a palindrome word with itself (i == j).
- Forgetting the empty word: it pairs with every palindrome, in both orders.
what they ask next
Follow-ups
LeetCode asks for O(sum of word lengths). How do you get there?
The extra factor of k comes from re-checking palindromes. Precompute, for each word, which of its prefixes and suffixes are palindromes in O(k) — Manacher's algorithm, or prefix hashes of the word and its reverse. Then each check is O(1) and the trie walk is linear in the total length.
before you submit
Edge cases to test
- The empty word pairs with every palindrome word, in both orders.
- A word and its exact reverse ("abcd", "dcba") → [i, j] and [j, i], each exactly once.
- A palindrome word must never pair with itself (i ≠ j).
practise next
Similar problems
- #125Valid PalindromeEasyin Two Pointers
- #5Longest Palindromic SubstringMediumin Dynamic Programming
- #214Shortest PalindromeHardin Range Queries & Strings