3088. Make String Anti-palindrome π
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 becauses[0] != s[3]
('a' != 'b') ands[1] != s[2]
('b' != 'a'). - The string must satisfy that no character at position
i
equals the character at positionn - i - 1
. - Among all possible anti-palindromes that can be formed, you need to find the one that comes first in dictionary order.
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"
.
Solution Approach
The implementation follows a greedy approach combined with sorting:
-
Sort the string: Convert the input string to a sorted character array
cs = sorted(s)
. This gives us the lexicographically smallest starting point. -
Calculate middle position: Find the middle index
m = n // 2
wheren
is the length of the string. -
Check if adjustment is needed: Compare
cs[m]
withcs[m-1]
. If they're different, the string is already an anti-palindrome and we can return the joined result. -
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 whilecs[i] == cs[i-1]
to skip over identical characters - Initialize pointer
j = m
to track positions that need swapping
- Initialize pointer
-
Perform swaps to fix violations:
- While
j < n
andcs[j] == cs[n-j-1]
(violation of anti-palindrome property):- If
i >= n
, no more different characters are available, return"-1"
- Swap
cs[i]
withcs[j]
to fix the violation - Increment both
i
andj
to process the next position
- If
- While
-
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 EvaluatorExample 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]
withcs[2]
: 'b' == 'b', so adjustment is needed
Step 4: Find first different character
- Initialize
i = 3
(at 'b') - Move
i
forward whilecs[i] == cs[i-1]
:cs[3] == cs[2]
? 'b' == 'b'? Yes, soi = 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 ifcs[3] == cs[6-3-1]
βcs[3] == cs[2]
β 'b' == 'b'? Yes (violation!)- Swap
cs[4]
withcs[3]
: ['a', 'a', 'b', 'c', 'b', 'c'] - Increment:
i = 5
,j = 4
Second iteration:
j = 4
, check ifcs[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 requiresO(n)
space - The variable
cs
stores this sorted list ofn
characters - Other variables (
i
,j
,m
) use onlyO(1)
additional space - The final
"".join(cs)
operation creates a new string of lengthn
, 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:
- Pre-validates if an anti-palindrome is possible by checking character frequencies
- Systematically checks each position for violations
- Ensures swaps don't create new violations
- Performs final verification before returning the result
Which two pointer techniques do you use to check if a string is a palindrome?
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donβt Miss This!