Facebook Pixel

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:

  1. The resulting string is not a palindrome
  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 position i. 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 Evaluator

Example Walkthrough

Let's walk through a few examples to illustrate the solution approach:

Example 1: palindrome = "abccba"

  1. Length check: n = 6, not 1, so we continue
  2. Convert to list: s = ['a', 'b', 'c', 'c', 'b', 'a']
  3. Scan first half (indices 0 to 2):
    • i = 0: s[0] = 'a', continue
    • i = 1: s[1] = 'b' (not 'a'), stop here
  4. Since i = 1 < n // 2 = 3, we found a non-'a' character
  5. Replace s[1] with 'a': s = ['a', 'a', 'c', 'c', 'b', 'a']
  6. Result: "aaccba" (no longer a palindrome, lexicographically smallest)

Example 2: palindrome = "aaaaa"

  1. Length check: n = 5, not 1, so we continue
  2. Convert to list: s = ['a', 'a', 'a', 'a', 'a']
  3. Scan first half (indices 0 to 1):
    • i = 0: s[0] = 'a', continue
    • i = 1: s[1] = 'a', continue
    • i = 2: We've reached n // 2 = 2, exit loop
  4. Since i = 2 = n // 2, all first half characters are 'a'
  5. Replace last character with 'b': s[-1] = 'b' β†’ s = ['a', 'a', 'a', 'a', 'b']
  6. Result: "aaaab" (no longer a palindrome)

Example 3: palindrome = "a"

  1. Length check: n = 1, return empty string ""
  2. Result: "" (impossible to make non-palindrome)

Example 4: palindrome = "racecar"

  1. Length check: n = 7, not 1, so we continue
  2. Convert to list: s = ['r', 'a', 'c', 'e', 'c', 'a', 'r']
  3. Scan first half (indices 0 to 2):
    • i = 0: s[0] = 'r' (not 'a'), stop here
  4. Since i = 0 < n // 2 = 3, we found a non-'a' character immediately
  5. Replace s[0] with 'a': s = ['a', 'a', 'c', 'e', 'c', 'a', 'r']
  6. 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 is O(n) time
  • String concatenation using join() takes O(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 requires O(n) space to store all characters
  • The final string created by join() also requires O(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:

  1. Only changing non-'a' characters to 'a' in the first half
  2. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More