1328. Break a Palindrome
Problem Description
You are given a palindromic string palindrome
consisting of lowercase English letters. Your task is to replace exactly one character with any lowercase English letter such that:
- The resulting string is not a palindrome
- The resulting string is the lexicographically smallest possible
If it's impossible to make the string not a palindrome by replacing exactly one character, return an empty string.
What is lexicographically smaller?
A string a
is lexicographically smaller than string b
(of the same length) if at the first position where they differ, a
has a character that comes earlier in the alphabet than the corresponding character in b
. For example, "abcc"
is lexicographically smaller than "abcd"
because at position 3 (0-indexed), 'c'
comes before 'd'
in the alphabet.
Key Points:
- You must replace exactly one character (no more, no less)
- The input is already a palindrome
- You want the smallest possible result that is not a palindrome
- If the palindrome has only 1 character, it's impossible to make it not a palindrome (since a single character is always a palindrome), so return
""
Strategy:
The solution uses a greedy approach. Since we want the lexicographically smallest result, we try to replace a character with 'a'
(the smallest letter) as early as possible in the string. We only need to check the first half of the palindrome because changing a character in the first half will break the palindrome property. If all characters in the first half are already 'a'
, we change the last character to 'b'
to ensure the string is no longer a palindrome while keeping it as small as possible.
Intuition
To make the lexicographically smallest non-palindrome, we need to think about two things: how to break the palindrome property and how to keep the result as small as possible.
Since we want the smallest possible string, we should try to change a character to 'a'
(the smallest letter) whenever possible. But where should we make this change?
Let's think about the structure of a palindrome. In a palindrome, the first half mirrors the second half. If we change any character in the first half, it will no longer match its corresponding character in the second half, breaking the palindrome property.
The key insight is that we should make changes as early as possible in the string (leftmost position) to get the lexicographically smallest result. So we scan from left to right in the first half of the string, looking for the first character that is not 'a'
. When we find such a character, we change it to 'a'
.
But what if all characters in the first half are already 'a'
? Consider a palindrome like "aaa"
or "aaaaa"
. If we change any 'a'
in the first half to something else (like 'b'
), we get a larger string. Instead, we should change the last character from 'a'
to 'b'
. This gives us the smallest possible non-palindrome in this special case.
Why do we only check the first half (n // 2
)? Because:
- Changing a character in the first half is sufficient to break the palindrome
- For odd-length palindromes, changing the middle character won't break the palindrome (it still mirrors itself)
- We want to make changes as early as possible for lexicographic minimality
The edge case of length 1 strings (like "a"
) cannot be made into non-palindromes since a single character always reads the same forwards and backwards, so we return an empty string.
Learn more about Greedy patterns.
Solution Approach
Following the greedy strategy, here's how we implement the solution step by step:
Step 1: Handle Edge Case
First, check if the palindrome has length 1. If n == 1
, return an empty string ""
since a single character cannot be made into a non-palindrome.
Step 2: Convert String to List
Convert the string to a list of characters s = list(palindrome)
to allow in-place modification, since strings in Python are immutable.
Step 3: Find First Non-'a' Character in First Half
Initialize a pointer i = 0
and traverse the first half of the palindrome (i < n // 2
). We look for the first character that is not 'a'
:
while i < n // 2 and s[i] == "a": i += 1
This loop continues as long as we're in the first half and the current character is 'a'
.
Step 4: Make the Replacement After the loop, we have two possible scenarios:
-
Case 1:
i == n // 2
- This means all characters in the first half are'a'
. In this case, we change the last character to'b'
:s[-1] = "b"
This ensures the string is no longer a palindrome while keeping it lexicographically small.
-
Case 2:
i < n // 2
- We found a non-'a' character at positioni
. We change it to'a'
:s[i] = "a"
This gives us the lexicographically smallest result by replacing the leftmost non-'a' character.
Step 5: Return the Result
Join the list back into a string and return it: return "".join(s)
Time Complexity: O(n)
where n
is the length of the palindrome, as we traverse at most half of the string.
Space Complexity: O(n)
for creating the character list from the input string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a few examples to illustrate the solution approach:
Example 1: palindrome = "abccba"
- Length check:
n = 6
, not 1, so we continue - Convert to list:
s = ['a', 'b', 'c', 'c', 'b', 'a']
- Scan first half (indices 0 to 2):
i = 0
:s[0] = 'a'
, continuei = 1
:s[1] = 'b'
(not 'a'), stop here
- Since
i = 1 < n // 2 = 3
, we found a non-'a' character - Replace
s[1]
with 'a':s = ['a', 'a', 'c', 'c', 'b', 'a']
- Result:
"aaccba"
(no longer a palindrome, lexicographically smallest)
Example 2: palindrome = "aaaaa"
- Length check:
n = 5
, not 1, so we continue - Convert to list:
s = ['a', 'a', 'a', 'a', 'a']
- Scan first half (indices 0 to 1):
i = 0
:s[0] = 'a'
, continuei = 1
:s[1] = 'a'
, continuei = 2
: We've reachedn // 2 = 2
, exit loop
- Since
i = 2 = n // 2
, all first half characters are 'a' - Replace last character with 'b':
s[-1] = 'b'
βs = ['a', 'a', 'a', 'a', 'b']
- Result:
"aaaab"
(no longer a palindrome)
Example 3: palindrome = "a"
- Length check:
n = 1
, return empty string""
- Result:
""
(impossible to make non-palindrome)
Example 4: palindrome = "racecar"
- Length check:
n = 7
, not 1, so we continue - Convert to list:
s = ['r', 'a', 'c', 'e', 'c', 'a', 'r']
- Scan first half (indices 0 to 2):
i = 0
:s[0] = 'r'
(not 'a'), stop here
- Since
i = 0 < n // 2 = 3
, we found a non-'a' character immediately - Replace
s[0]
with 'a':s = ['a', 'a', 'c', 'e', 'c', 'a', 'r']
- Result:
"aacecar"
(no longer a palindrome, changed 'r' to 'a' at position 0)
The algorithm efficiently finds the leftmost position where we can make the string smaller while breaking the palindrome property.
Solution Implementation
1class Solution:
2 def breakPalindrome(self, palindrome: str) -> str:
3 """
4 Break a palindrome by changing exactly one character to make it
5 lexicographically smallest non-palindrome string.
6
7 Args:
8 palindrome: A palindrome string to break
9
10 Returns:
11 The lexicographically smallest non-palindrome after changing one character,
12 or empty string if impossible (when length is 1)
13 """
14 n = len(palindrome)
15
16 # Single character palindrome cannot be broken
17 if n == 1:
18 return ""
19
20 # Convert string to list for character modification
21 chars = list(palindrome)
22
23 # Try to find the first non-'a' character in the first half
24 # We only check the first half because it's a palindrome
25 i = 0
26 while i < n // 2 and chars[i] == 'a':
27 i += 1
28
29 if i == n // 2:
30 # All characters in the first half are 'a'
31 # Change the last character to 'b' to get lexicographically smallest result
32 chars[-1] = 'b'
33 else:
34 # Found a non-'a' character in the first half
35 # Change it to 'a' to get lexicographically smallest result
36 chars[i] = 'a'
37
38 # Convert list back to string and return
39 return "".join(chars)
40
1class Solution {
2 public String breakPalindrome(String palindrome) {
3 // Get the length of the palindrome string
4 int length = palindrome.length();
5
6 // If palindrome has only one character, it cannot be broken
7 // Return empty string as per problem requirement
8 if (length == 1) {
9 return "";
10 }
11
12 // Convert string to character array for modification
13 char[] chars = palindrome.toCharArray();
14
15 // Iterate through the first half of the palindrome
16 // We only need to check the first half since it's a palindrome
17 int index = 0;
18 while (index < length / 2 && chars[index] == 'a') {
19 index++;
20 }
21
22 // If all characters in the first half are 'a'
23 // Change the last character to 'b' to ensure lexicographically smallest result
24 if (index == length / 2) {
25 chars[length - 1] = 'b';
26 } else {
27 // Found a non-'a' character in the first half
28 // Change it to 'a' to get the lexicographically smallest string
29 chars[index] = 'a';
30 }
31
32 // Convert character array back to string and return
33 return String.valueOf(chars);
34 }
35}
36
1class Solution {
2public:
3 string breakPalindrome(string palindrome) {
4 int length = palindrome.size();
5
6 // Single character palindrome cannot be broken while remaining valid
7 if (length == 1) {
8 return "";
9 }
10
11 // Iterate through the first half of the palindrome
12 int index = 0;
13 while (index < length / 2 && palindrome[index] == 'a') {
14 ++index;
15 }
16
17 // If all characters in the first half are 'a'
18 if (index == length / 2) {
19 // Change the last character to 'b' to break palindrome
20 // This creates the lexicographically smallest non-palindrome
21 palindrome[length - 1] = 'b';
22 } else {
23 // Found a non-'a' character in the first half
24 // Change it to 'a' for the lexicographically smallest result
25 palindrome[index] = 'a';
26 }
27
28 return palindrome;
29 }
30};
31
1/**
2 * Breaks a palindrome by changing exactly one character to make it lexicographically smallest
3 * non-palindrome string possible. If impossible, returns empty string.
4 *
5 * @param palindrome - The input palindrome string
6 * @returns The lexicographically smallest non-palindrome string, or empty string if impossible
7 */
8function breakPalindrome(palindrome: string): string {
9 const length: number = palindrome.length;
10
11 // Single character palindrome cannot be broken
12 if (length === 1) {
13 return '';
14 }
15
16 // Convert string to character array for mutation
17 const characters: string[] = palindrome.split('');
18
19 // Iterate through the first half of the palindrome
20 let index: number = 0;
21 const halfLength: number = length >> 1; // Equivalent to Math.floor(length / 2)
22
23 // Find the first non-'a' character in the first half
24 while (index < halfLength && characters[index] === 'a') {
25 index++;
26 }
27
28 // If all characters in the first half are 'a'
29 if (index === halfLength) {
30 // Change the last character to 'b' to create lexicographically smallest non-palindrome
31 characters[length - 1] = 'b';
32 } else {
33 // Change the first non-'a' character to 'a' for smallest lexicographical result
34 characters[index] = 'a';
35 }
36
37 // Join the character array back to string
38 return characters.join('');
39}
40
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the palindrome string.
The algorithm performs the following operations:
- Converting the string to a list takes
O(n)
time - The while loop iterates at most
n/2
times, which isO(n)
time - String concatenation using
join()
takesO(n)
time - All other operations (comparisons, single character assignments) are
O(1)
Therefore, the overall time complexity is O(n) + O(n) + O(n) = O(n)
.
Space Complexity: O(n)
, where n
is the length of the palindrome string.
The algorithm uses additional space for:
- Creating a list
s
from the input string, which requiresO(n)
space to store all characters - The final string created by
join()
also requiresO(n)
space
Since we need to store a mutable copy of the string as a list to modify it, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Attempting to Change the Middle Character in Odd-Length Palindromes
The Problem: When dealing with odd-length palindromes, there's a temptation to change the middle character since it seems like an easy target. However, this creates a subtle issue:
# Incorrect approach - might change middle character
for i in range(n // 2 + 1): # This includes the middle character!
if palindrome[i] != 'a':
chars[i] = 'a'
break
For a palindrome like "aba"
, changing the middle 'b' to 'a' gives us "aaa"
, which is still a palindrome! This violates our requirement.
The Solution: Always exclude the middle character from consideration in odd-length palindromes. The correct loop should only check the first half:
# Correct approach - strictly first half only
for i in range(n // 2): # Excludes middle character in odd-length strings
if palindrome[i] != 'a':
chars[i] = 'a'
break
Pitfall 2: Not Handling the All-'a' First Half Case
The Problem: Some implementations forget to handle the case where all characters in the first half are already 'a'. This leads to returning the original palindrome unchanged:
# Incorrect - doesn't handle all-'a' case
def breakPalindrome(palindrome):
chars = list(palindrome)
for i in range(len(palindrome) // 2):
if chars[i] != 'a':
chars[i] = 'a'
return "".join(chars)
# Oops! Forgot to handle when loop completes without finding non-'a'
return "".join(chars) # Returns unchanged palindrome!
The Solution: Always include a fallback that changes the last character to 'b' when all first-half characters are 'a':
# Correct - handles all cases
def breakPalindrome(palindrome):
chars = list(palindrome)
found_non_a = False
for i in range(len(palindrome) // 2):
if chars[i] != 'a':
chars[i] = 'a'
found_non_a = True
break
if not found_non_a:
chars[-1] = 'b' # Critical fallback!
return "".join(chars)
Pitfall 3: Incorrectly Assuming We Should Always Change to 'a'
The Problem: A naive approach might try to change every character to 'a' to get the smallest result, but this can keep the palindrome property:
# Incorrect logic palindrome = "aabaa" # Changing index 2 ('b') to 'a' gives "aaaaa" - still a palindrome!
The Solution: The algorithm correctly handles this by:
- Only changing non-'a' characters to 'a' in the first half
- If all first-half characters are already 'a', change the last character to 'b' (not 'a')
This ensures we always break the palindrome while maintaining lexicographic minimality.
Which type of traversal does breadth first search do?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!