A clear explanation of building the shortest palindrome by finding the longest palindromic prefix using KMP.
Problem Restatement
We are given a string s.
We may add characters only in front of s.
We need to return the shortest palindrome that can be created this way.
The official statement says we can convert s to a palindrome by adding characters in front of it, and we must return the shortest palindrome possible.
Input and Output
| Item | Meaning |
|---|---|
| Input | String s |
| Output | Shortest palindrome formed by adding characters in front |
| Allowed operation | Add characters only before s |
| Constraint | 0 <= s.length <= 5 * 10^4 |
| Characters | Lowercase English letters |
Example function shape:
def shortestPalindrome(s: str) -> str:
...Examples
Example 1:
s = "aacecaaa"The shortest palindrome is:
"aaacecaaa"We add one "a" in front.
Example 2:
s = "abcd"The shortest palindrome is:
"dcbabcd"We add "dcb" in front.
These are the standard examples for the problem.
First Thought
Since we can only add characters to the front, the original string s must remain as the suffix of the final answer.
So the final answer looks like:
some_added_characters + sTo make the whole string a palindrome, the part of s that already works best should stay in place.
That part is the longest prefix of s that is already a palindrome.
Key Insight
Find the longest palindromic prefix of s.
Suppose:
s = "abcd"The longest palindromic prefix is:
"a"The remaining suffix is:
"bcd"To make a palindrome, reverse that suffix and add it to the front:
"dcb" + "abcd" = "dcbabcd"For:
s = "aacecaaa"The longest palindromic prefix is:
"aacecaa"The remaining suffix is:
"a"Reverse the suffix and add it to the front:
"a" + "aacecaaa" = "aaacecaaa"So the problem becomes:
How do we find the longest palindromic prefix efficiently?Brute Force Prefix Check
We could check every prefix from longest to shortest.
For each prefix:
- Check if it is a palindrome.
- If yes, use it.
class Solution:
def shortestPalindrome(self, s: str) -> str:
for end in range(len(s), -1, -1):
prefix = s[:end]
if prefix == prefix[::-1]:
suffix = s[end:]
return suffix[::-1] + s
return sThis is simple, but it may take quadratic time because each palindrome check copies and reverses a prefix.
For length up to 5 * 10^4, we want a linear method.
KMP Idea
KMP can find the longest prefix of one string that is also a suffix of another combined string.
We build:
combined = s + "#" + reversed_sThe separator "#" prevents accidental matching across the boundary.
The last value of the KMP prefix table tells us the length of the longest prefix of combined that is also a suffix of combined.
Because of how we built the string, this value becomes the length of the longest palindromic prefix of s.
Why This Works
Let:
s = "aacecaaa"
reversed_s = "aaacecaa"
combined = "aacecaaa#aaacecaa"The longest prefix of combined that is also a suffix is:
"aacecaa"This means:
- it appears at the start of
s - it appears at the end of
reversed_s
A prefix of s appearing at the end of reversed_s means that prefix reads the same forward and backward.
So it is a palindromic prefix.
Prefix Table
The KMP prefix table is usually called lps.
lps[i] means:
the length of the longest proper prefix of combined[0:i+1]
that is also a suffix of combined[0:i+1]Example idea:
combined = "abab"For the full string "abab":
- prefix
"ab" - suffix
"ab"
So the last lps value is 2.
For this problem, we only need the final lps value.
Algorithm
- Reverse
s. - Build
combined = s + "#" + reverse(s). - Compute the KMP
lpsarray forcombined. - Let
prefix_len = lps[-1]. - The suffix after the palindromic prefix is
s[prefix_len:]. - Reverse that suffix and put it in front of
s.
Walkthrough
Use:
s = "abcd"Reverse:
reversed_s = "dcba"Combined string:
"abcd#dcba"The longest palindromic prefix has length 1, which is:
"a"Suffix after that prefix:
"bcd"Reverse suffix:
"dcb"Answer:
"dcb" + "abcd" = "dcbabcd"Correctness
The final answer must keep s unchanged as its suffix because we are only allowed to add characters before it.
To minimize the number of added characters, we need to maximize the part of s that already belongs to the front-side palindrome structure. This part must be a prefix of s, and it must itself be a palindrome.
So the optimal strategy is to find the longest palindromic prefix of s.
The KMP construction s + "#" + reverse(s) computes the longest prefix of s that also matches a suffix of reverse(s). Such a match means the prefix reads the same forward and backward, so it is a palindromic prefix.
The separator prevents matches from crossing between s and reverse(s).
After finding this longest palindromic prefix, every remaining character in the suffix must be mirrored before s. Adding the reverse of that suffix to the front is sufficient and uses the fewest possible characters.
Therefore the algorithm returns the shortest palindrome obtainable by adding characters in front.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Build reverse string and one KMP table |
| Space | O(n) | Store combined string and lps array |
Implementation
class Solution:
def shortestPalindrome(self, s: str) -> str:
reversed_s = s[::-1]
combined = s + "#" + reversed_s
lps = [0] * len(combined)
for i in range(1, len(combined)):
length = lps[i - 1]
while length > 0 and combined[i] != combined[length]:
length = lps[length - 1]
if combined[i] == combined[length]:
length += 1
lps[i] = length
prefix_len = lps[-1]
suffix = s[prefix_len:]
return suffix[::-1] + sCode Explanation
Reverse the string:
reversed_s = s[::-1]Build the KMP input:
combined = s + "#" + reversed_sCreate the prefix table:
lps = [0] * len(combined)For each position, start from the previous matched length:
length = lps[i - 1]If the next character does not match, fall back to a shorter border:
while length > 0 and combined[i] != combined[length]:
length = lps[length - 1]If it matches, extend the current border:
if combined[i] == combined[length]:
length += 1Store it:
lps[i] = lengthThe final value gives the longest palindromic prefix length:
prefix_len = lps[-1]Take the unmatched suffix:
suffix = s[prefix_len:]Reverse it and add it in front:
return suffix[::-1] + sSimpler Two-Pointer Recursive Version
There is also a compact recursive solution.
class Solution:
def shortestPalindrome(self, s: str) -> str:
i = 0
for j in range(len(s) - 1, -1, -1):
if s[i] == s[j]:
i += 1
if i == len(s):
return s
suffix = s[i:]
return suffix[::-1] + self.shortestPalindrome(s[:i]) + suffixThis version is shorter, but the KMP version gives a clean linear-time guarantee.
Testing
def run_tests():
s = Solution()
assert s.shortestPalindrome("aacecaaa") == "aaacecaaa"
assert s.shortestPalindrome("abcd") == "dcbabcd"
assert s.shortestPalindrome("") == ""
assert s.shortestPalindrome("a") == "a"
assert s.shortestPalindrome("aba") == "aba"
assert s.shortestPalindrome("abb") == "bbabb"
assert s.shortestPalindrome("aaab") == "baaab"
print("all tests passed")
run_tests()| Test | Why |
|---|---|
"aacecaaa" | Standard example |
"abcd" | Only first character is palindromic prefix |
"" | Empty string |
"a" | Single character |
"aba" | Already palindrome |
"abb" | Prefix length one |
"aaab" | Long repeated prefix |