Facebook Pixel

3088. Make String Anti-palindrome πŸ”’

HardGreedyStringCounting SortSorting
Leetcode Link

Problem Description

You are given a string s of even length n. An anti-palindrome is defined as a string where for every position i (where 0 <= i < n), the character at position i is different from the character at its mirror position n - i - 1. In other words, s[i] != s[n - i - 1] for all valid indices.

Your task is to transform the given string s into an anti-palindrome by performing any number of swap operations (including zero operations). In each operation, you can select any two characters in the string and swap their positions.

The goal is to return the resulting anti-palindrome string. If multiple valid anti-palindrome strings can be formed, you must return the lexicographically smallest one. If it's impossible to create an anti-palindrome from the given string, return "-1".

For example:

  • If s = "abba", after swapping to get "abab", we have an anti-palindrome because s[0] != s[3] ('a' != 'b') and s[1] != s[2] ('b' != 'a').
  • The string must satisfy that no character at position i equals the character at position n - i - 1.
  • Among all possible anti-palindromes that can be formed, you need to find the one that comes first in dictionary order.
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To find the lexicographically smallest anti-palindrome, we should start by sorting the string. This ensures we're working with the smallest possible arrangement from the beginning.

The key insight is that after sorting, we only need to focus on the middle portion of the string. Why? Because in a sorted string, if it's already an anti-palindrome, we're done. If not, the problematic pairs (where s[i] == s[n-i-1]) will be concentrated around the middle.

Consider a sorted string like "aabbccdd". The potential issue occurs when the character at position i equals the character at position n-i-1. In a sorted string, this happens when we have too many of the same character clustered in the middle region.

The critical observation is that we only need to check if s[m] == s[m-1] (where m = n/2). If these middle characters are different, the string is already an anti-palindrome because:

  • The left half is sorted in non-decreasing order
  • The right half (when viewed from the mirror positions) is in non-increasing order
  • If the two middle elements are different, all mirror pairs will be different

When s[m] == s[m-1], we have a run of identical characters crossing the midpoint. We need to break this symmetry by swapping some characters from the right half with different characters. We look for the first different character s[i] in the right half and swap it with problematic positions. This maintains lexicographical order as much as possible while ensuring the anti-palindrome property.

If we can't find enough different characters to swap (meaning most or all characters in the string are the same), it's impossible to form an anti-palindrome, and we return "-1".

Learn more about Greedy and Sorting patterns.

Solution Approach

The implementation follows a greedy approach combined with sorting:

  1. Sort the string: Convert the input string to a sorted character array cs = sorted(s). This gives us the lexicographically smallest starting point.

  2. Calculate middle position: Find the middle index m = n // 2 where n is the length of the string.

  3. Check if adjustment is needed: Compare cs[m] with cs[m-1]. If they're different, the string is already an anti-palindrome and we can return the joined result.

  4. Handle the problematic case when cs[m] == cs[m-1]:

    • Initialize pointer i = m to find the first different character in the right half
    • Move i forward while cs[i] == cs[i-1] to skip over identical characters
    • Initialize pointer j = m to track positions that need swapping
  5. Perform swaps to fix violations:

    • While j < n and cs[j] == cs[n-j-1] (violation of anti-palindrome property):
      • If i >= n, no more different characters are available, return "-1"
      • Swap cs[i] with cs[j] to fix the violation
      • Increment both i and j to process the next position
  6. Return the result: Join the character array back into a string and return it.

The algorithm ensures that we make minimal changes to maintain lexicographical order while satisfying the anti-palindrome constraint. The swapping strategy specifically targets positions where the anti-palindrome property is violated (cs[j] == cs[n-j-1]) and fixes them using different characters from the right half of the sorted string.

The time complexity is O(n log n) due to sorting, and the space complexity is O(n) for storing the sorted character array.

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 the solution with s = "aabbcc" (n = 6).

Step 1: Sort the string

  • cs = ['a', 'a', 'b', 'b', 'c', 'c']

Step 2: Calculate middle position

  • m = 6 // 2 = 3

Step 3: Check if adjustment needed

  • Compare cs[3] with cs[2]: 'b' == 'b', so adjustment is needed

Step 4: Find first different character

  • Initialize i = 3 (at 'b')
  • Move i forward while cs[i] == cs[i-1]:
    • cs[3] == cs[2]? 'b' == 'b'? Yes, so i = 4
    • cs[4] == cs[3]? 'c' == 'b'? No, stop
  • Now i = 4 (pointing to first 'c')

Step 5: Fix violations

  • Initialize j = 3

  • Check positions that violate anti-palindrome:

    First iteration:

    • j = 3, check if cs[3] == cs[6-3-1] β†’ cs[3] == cs[2] β†’ 'b' == 'b'? Yes (violation!)
    • Swap cs[4] with cs[3]: ['a', 'a', 'b', 'c', 'b', 'c']
    • Increment: i = 5, j = 4

    Second iteration:

    • j = 4, check if cs[4] == cs[6-4-1] β†’ cs[4] == cs[1] β†’ 'b' == 'a'? No (no violation)
    • No more violations, exit loop

Step 6: Verify the result

  • Final string: "aabcbc"
  • Check anti-palindrome property:
    • Position 0 and 5: 'a' != 'c' βœ“
    • Position 1 and 4: 'a' != 'b' βœ“
    • Position 2 and 3: 'b' != 'c' βœ“
  • All positions satisfy s[i] != s[n-i-1]

The result "aabcbc" is the lexicographically smallest anti-palindrome we can form from "aabbcc".

Solution Implementation

1class Solution:
2    def makeAntiPalindrome(self, s: str) -> str:
3        # Convert string to sorted list of characters
4        char_list = sorted(s)
5        n = len(char_list)
6        mid_index = n // 2
7      
8        # Check if the middle elements are the same (potential palindrome issue)
9        if char_list[mid_index] == char_list[mid_index - 1]:
10            # Find the end of the sequence of duplicate characters starting from middle
11            duplicate_end = mid_index
12            while duplicate_end < n and char_list[duplicate_end] == char_list[duplicate_end - 1]:
13                duplicate_end += 1
14          
15            # Start swapping from middle position to break palindrome pattern
16            swap_position = mid_index
17            while swap_position < n and char_list[swap_position] == char_list[n - swap_position - 1]:
18                # If we've exhausted all characters to swap with, no solution exists
19                if duplicate_end >= n:
20                    return "-1"
21              
22                # Swap characters to break the palindrome pattern
23                char_list[duplicate_end], char_list[swap_position] = char_list[swap_position], char_list[duplicate_end]
24              
25                # Move both pointers forward
26                duplicate_end += 1
27                swap_position += 1
28      
29        # Convert list back to string and return
30        return "".join(char_list)
31
1class Solution {
2    public String makeAntiPalindrome(String s) {
3        // Convert string to character array for manipulation
4        char[] charArray = s.toCharArray();
5      
6        // Sort the characters in ascending order
7        Arrays.sort(charArray);
8      
9        int length = charArray.length;
10        int midIndex = length / 2;
11      
12        // Check if the middle character equals the character before it
13        // This indicates potential palindrome issues after sorting
14        if (charArray[midIndex] == charArray[midIndex - 1]) {
15            // Find the first character different from the middle character
16            int differentCharIndex = midIndex;
17            while (differentCharIndex < length && 
18                   charArray[differentCharIndex] == charArray[differentCharIndex - 1]) {
19                ++differentCharIndex;
20            }
21          
22            // Try to fix palindrome by swapping characters
23            // Start from middle and check if character at position j equals its mirror
24            for (int currentPos = midIndex; 
25                 currentPos < length && charArray[currentPos] == charArray[length - currentPos - 1]; 
26                 ++differentCharIndex, ++currentPos) {
27              
28                // If we've exhausted all different characters, anti-palindrome is impossible
29                if (differentCharIndex >= length) {
30                    return "-1";
31                }
32              
33                // Swap the current position with a different character
34                char temp = charArray[differentCharIndex];
35                charArray[differentCharIndex] = charArray[currentPos];
36                charArray[currentPos] = temp;
37            }
38        }
39      
40        // Convert character array back to string and return
41        return new String(charArray);
42    }
43}
44
1class Solution {
2public:
3    string makeAntiPalindrome(string s) {
4        // Sort the string to group identical characters together
5        sort(s.begin(), s.end());
6      
7        int n = s.length();
8        int mid = n / 2;
9      
10        // Check if the character at middle position equals the one before it
11        // This could potentially create palindrome pairs
12        if (s[mid] == s[mid - 1]) {
13            // Find the first position where character changes
14            int nextDifferentPos = mid;
15            while (nextDifferentPos < n && s[nextDifferentPos] == s[nextDifferentPos - 1]) {
16                ++nextDifferentPos;
17            }
18          
19            // Starting from middle, check for palindrome violations
20            // and swap with different characters to fix them
21            for (int j = mid; j < n && s[j] == s[n - j - 1]; ++nextDifferentPos, ++j) {
22                // If we've exhausted all different characters, anti-palindrome is impossible
23                if (nextDifferentPos >= n) {
24                    return "-1";
25                }
26              
27                // Swap current position with a different character
28                swap(s[nextDifferentPos], s[j]);
29            }
30        }
31      
32        return s;
33    }
34};
35
1/**
2 * Creates an anti-palindrome by rearranging characters in the string.
3 * An anti-palindrome is a string where no character at position i equals 
4 * the character at position n-1-i (where n is the string length).
5 * 
6 * @param s - The input string to transform into an anti-palindrome
7 * @returns The rearranged anti-palindrome string, or '-1' if impossible
8 */
9function makeAntiPalindrome(s: string): string {
10    // Convert string to array and sort characters lexicographically
11    const characters: string[] = s.split('').sort();
12    const length: number = characters.length;
13    const midpoint: number = length >> 1; // Bit shift for integer division by 2
14  
15    // Check if the middle elements are the same (potential palindrome issue)
16    if (characters[midpoint] === characters[midpoint - 1]) {
17        // Find the first position where characters differ
18        let diffIndex: number = midpoint;
19        while (diffIndex < length && characters[diffIndex] === characters[diffIndex - 1]) {
20            diffIndex++;
21        }
22      
23        // Swap characters to break palindrome symmetry
24        for (let currentPos: number = midpoint; 
25             currentPos < length && characters[currentPos] === characters[length - currentPos - 1]; 
26             diffIndex++, currentPos++) {
27          
28            // If we've exhausted all different characters, anti-palindrome is impossible
29            if (diffIndex >= length) {
30                return '-1';
31            }
32          
33            // Swap characters to ensure position i doesn't mirror position n-1-i
34            [characters[currentPos], characters[diffIndex]] = 
35                [characters[diffIndex], characters[currentPos]];
36        }
37    }
38  
39    // Join the rearranged characters back into a string
40    return characters.join('');
41}
42

Time and Space Complexity

The time complexity of this algorithm is O(n Γ— log n), where n is the length of the string s. This is dominated by the sorting operation sorted(s) at the beginning of the function, which uses a comparison-based sorting algorithm (typically Timsort in Python) that has a time complexity of O(n Γ— log n). The subsequent operations include:

  • The while loops that iterate through portions of the array, which take O(n) time in the worst case
  • Character swapping operations that take O(1) time each
  • The final join operation that takes O(n) time

Since O(n Γ— log n) + O(n) = O(n Γ— log n), the overall time complexity is O(n Γ— log n).

The space complexity is O(n), where n is the length of the string s. This is because:

  • The sorted(s) operation creates a new list of characters that requires O(n) space
  • The variable cs stores this sorted list of n characters
  • Other variables (i, j, m) use only O(1) additional space
  • The final "".join(cs) operation creates a new string of length n, but this is the return value

Therefore, the total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Misunderstanding When Swapping is Necessary

A critical pitfall is not recognizing that swapping is only needed when we have a "run" of identical characters crossing the middle boundary that creates palindromic pairs. The condition char_list[mid_index] == char_list[mid_index - 1] alone doesn't guarantee we need swaps.

Problem Example:

s = "aabbccdd"  # After sorting: "aabbccdd"
# mid_index = 4
# char_list[3] = 'b', char_list[4] = 'c' (different)
# But we still need to check if char_list[i] == char_list[n-i-1] for all positions

In this case, even though the middle elements differ, we have:

  • Position 0 ('a') vs Position 7 ('d') - OK
  • Position 1 ('a') vs Position 6 ('d') - OK
  • Position 2 ('b') vs Position 5 ('c') - OK
  • Position 3 ('b') vs Position 4 ('c') - OK

The code would work, but the logic can be misleading.

2. Incorrect Duplicate End Finding

The loop to find duplicate_end can go out of bounds or miss edge cases:

# Current problematic code:
duplicate_end = mid_index
while duplicate_end < n and char_list[duplicate_end] == char_list[duplicate_end - 1]:
    duplicate_end += 1

Issue: This finds the end of duplicates starting from mid_index, but what if we need characters that are different from char_list[mid_index-1], not just the next different character in sequence?

Example:

s = "aaaabbbb"  # After sorting: "aaaabbbb"
# We need to swap some 'b's into the first half to break the palindrome
# But duplicate_end will point to index 4 (first 'b'), when we actually need it

3. Insufficient Validation for Impossible Cases

The current code only returns "-1" when duplicate_end >= n during swapping, but there are other impossible cases:

Example:

s = "aaaa"  # All same characters
# After sorting: "aaaa"
# No matter how we arrange, s[0] will always equal s[3], s[1] will equal s[2]

Improved Solution:

class Solution:
    def makeAntiPalindrome(self, s: str) -> str:
        char_list = sorted(s)
        n = len(char_list)
      
        # First, check if anti-palindrome is possible
        char_count = {}
        for c in char_list:
            char_count[c] = char_count.get(c, 0) + 1
      
        # If any character appears more than n/2 times, impossible
        for count in char_count.values():
            if count > n // 2:
                return "-1"
      
        # Now arrange to ensure anti-palindrome property
        mid = n // 2
      
        # Check and fix violations from the middle outward
        for i in range(mid):
            if char_list[i] == char_list[n - 1 - i]:
                # Find a different character to swap with
                for j in range(i + 1, n - i - 1):
                    if char_list[j] != char_list[i] and char_list[j] != char_list[n - 1 - i]:
                        # Check if swapping maintains anti-palindrome at position j
                        mirror_j = n - 1 - j
                        if j < mid and char_list[i] != char_list[mirror_j]:
                            char_list[i], char_list[j] = char_list[j], char_list[i]
                            break
                        elif j >= mid and char_list[i] != char_list[mirror_j]:
                            char_list[n - 1 - i], char_list[j] = char_list[j], char_list[n - 1 - i]
                            break
      
        # Final verification
        for i in range(mid):
            if char_list[i] == char_list[n - 1 - i]:
                return "-1"
              
        return "".join(char_list)

This improved solution:

  1. Pre-validates if an anti-palindrome is possible by checking character frequencies
  2. Systematically checks each position for violations
  3. Ensures swaps don't create new violations
  4. Performs final verification before returning the result
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More