3216. Lexicographically Smallest String After a Swap
Problem Description
You are given a string s
that contains only digits. Your task is to find the lexicographically smallest string possible by swapping adjacent digits that have the same parity, but you can perform this swap at most once.
Two digits have the same parity when:
- Both digits are odd (like 1, 3, 5, 7, 9)
- Both digits are even (like 0, 2, 4, 6, 8)
For example:
- Digits 5 and 9 have the same parity (both odd)
- Digits 2 and 4 have the same parity (both even)
- Digits 6 and 9 have different parity (6 is even, 9 is odd)
You can only swap two digits if they are:
- Adjacent to each other in the string
- Have the same parity
- You haven't already performed a swap (maximum one swap allowed)
The goal is to make the resulting string as small as possible in lexicographical order. In lexicographical ordering, a string is smaller if it would appear earlier in a dictionary. For example, "123" is lexicographically smaller than "132".
If no beneficial swap can be made, return the original string.
Intuition
To get the lexicographically smallest string, we want smaller digits to appear as early as possible in the string. Since we can only swap adjacent digits with the same parity once, we need to make this single swap count.
The key insight is that we should perform the swap at the first opportunity where it would improve the string's lexicographical order. Why? Because moving a smaller digit earlier in the string has more impact on the lexicographical ordering than moving it later.
Consider scanning the string from left to right. When we encounter two adjacent digits with the same parity, we check if swapping them would make the string smaller. This happens when the left digit is larger than the right digit. For example, if we have "42", swapping gives us "24", which is lexicographically smaller.
The greedy approach works here because:
- We want to fix the leftmost position possible with a smaller digit
- Once we find the first beneficial swap, we should take it immediately
- Any swap opportunity we skip might never give us as much benefit as fixing an earlier position
For instance, if we have "531", we can swap positions 0-1 to get "351" (both 5 and 3 are odd). Even if there were other valid swaps later in a longer string, fixing position 0 with a smaller digit gives us the best lexicographical improvement.
This leads us to a simple algorithm: traverse the string once, find the first pair of adjacent same-parity digits where the left is greater than the right, swap them, and return. If no such pair exists, the string is already optimal.
Learn more about Greedy patterns.
Solution Approach
The implementation follows a greedy simulation approach by traversing the string from left to right and checking each pair of adjacent digits.
The algorithm works as follows:
-
Iterate through adjacent pairs: We traverse the string using
pairwise(map(ord, s))
which gives us pairs of adjacent characters converted to their ASCII values. Thepairwise
function creates pairs like(s[0], s[1])
,(s[1], s[2])
, and so on. -
Check parity condition: For each pair
(a, b)
, we check if they have the same parity using(a + b) % 2 == 0
. This clever trick works because:- If both digits are odd, their ASCII values are odd, so
odd + odd = even
, andeven % 2 = 0
- If both digits are even, their ASCII values are even, so
even + even = even
, andeven % 2 = 0
- If one is odd and one is even,
odd + even = odd
, andodd % 2 = 1
- If both digits are odd, their ASCII values are odd, so
-
Check if swap is beneficial: We verify if
a > b
, meaning the left digit is greater than the right digit. Swapping in this case would move the smaller digit to an earlier position, making the string lexicographically smaller. -
Perform the swap: If both conditions are met, we reconstruct the string by:
- Taking the substring from start to position
i
:s[:i]
- Adding the swapped digits:
s[i + 1] + s[i]
- Appending the rest of the string:
s[i + 2:]
- This effectively swaps the digits at positions
i
andi + 1
- Taking the substring from start to position
-
Return immediately: Once we find and perform the first beneficial swap, we return the modified string immediately, as we can only swap once and want the earliest possible improvement.
-
No swap needed: If we complete the iteration without finding any swappable pair, we return the original string
s
as it's already in its optimal form.
The time complexity is O(n)
where n
is the length of the string, as we traverse it at most once. The space complexity is O(n)
for creating the result 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 the solution with the example string s = "45320"
.
Step 1: Initialize and start iteration
- We begin traversing adjacent pairs from left to right
- Convert each character to its ASCII value for easier parity checking
Step 2: Check first pair (4, 5) at positions 0-1
- ASCII values: 4 β 52, 5 β 53
- Check parity:
(52 + 53) % 2 = 105 % 2 = 1
(different parity) - Since they have different parity, we cannot swap them
- Continue to next pair
Step 3: Check second pair (5, 3) at positions 1-2
- ASCII values: 5 β 53, 3 β 51
- Check parity:
(53 + 51) % 2 = 104 % 2 = 0
(same parity - both odd) - Check if beneficial:
5 > 3
? Yes! - This is our first opportunity for a beneficial swap
Step 4: Perform the swap
- Original:
"45320"
- Swap positions 1 and 2:
- Take substring before position 1:
"4"
- Add swapped digits:
"3" + "5"
="35"
- Add remaining substring:
"20"
- Take substring before position 1:
- Result:
"4" + "35" + "20"
="43520"
Step 5: Return immediately
- We've used our one allowed swap
- Return
"43520"
The string "43520"
is lexicographically smaller than "45320"
because at position 1, '3' < '5'. Even though there might be other swappable pairs later (like positions 3-4 with digits 2 and 0, both even), we take the first beneficial swap because fixing an earlier position gives us the best lexicographical improvement.
Solution Implementation
1class Solution:
2 def getSmallestString(self, s: str) -> str:
3 """
4 Find the lexicographically smallest string by swapping two adjacent digits
5 of the same parity (both even or both odd) at most once.
6
7 Args:
8 s: Input string consisting of digits
9
10 Returns:
11 Lexicographically smallest string after at most one swap
12 """
13 # Convert string to list for easier manipulation
14 chars = list(s)
15
16 # Iterate through adjacent pairs of characters
17 for i in range(len(s) - 1):
18 # Get ASCII values of current and next character
19 current_val = ord(s[i])
20 next_val = ord(s[i + 1])
21
22 # Check if both digits have same parity (both even or both odd)
23 # Two numbers have same parity if their sum is even
24 if (current_val + next_val) % 2 == 0:
25 # Check if swapping would make the string smaller
26 # (current digit is greater than next digit)
27 if current_val > next_val:
28 # Perform the swap and return immediately
29 # This ensures we get the lexicographically smallest result
30 return s[:i] + s[i + 1] + s[i] + s[i + 2:]
31
32 # If no beneficial swap was found, return the original string
33 return s
34
1class Solution {
2 /**
3 * Finds the lexicographically smallest string by swapping adjacent digits
4 * that have the same parity (both odd or both even).
5 * Only the first valid swap is performed.
6 *
7 * @param s Input string containing digits
8 * @return The lexicographically smallest string after at most one swap
9 */
10 public String getSmallestString(String s) {
11 // Convert string to character array for easier manipulation
12 char[] charArray = s.toCharArray();
13 int length = charArray.length;
14
15 // Iterate through adjacent pairs of characters
16 for (int i = 1; i < length; i++) {
17 // Get the current pair of adjacent characters
18 char previousChar = charArray[i - 1];
19 char currentChar = charArray[i];
20
21 // Check if swapping would create a smaller string
22 // and if both characters have the same parity
23 if (previousChar > currentChar && haveSameParity(previousChar, currentChar)) {
24 // Perform the swap
25 charArray[i] = previousChar;
26 charArray[i - 1] = currentChar;
27
28 // Return immediately after first swap
29 return new String(charArray);
30 }
31 }
32
33 // No beneficial swap found, return original string
34 return s;
35 }
36
37 /**
38 * Helper method to check if two digit characters have the same parity
39 *
40 * @param a First character
41 * @param b Second character
42 * @return true if both are odd or both are even
43 */
44 private boolean haveSameParity(char a, char b) {
45 return a % 2 == b % 2;
46 }
47}
48
1class Solution {
2public:
3 string getSmallestString(string s) {
4 int stringLength = s.length();
5
6 // Iterate through adjacent pairs of characters
7 for (int i = 1; i < stringLength; ++i) {
8 // Get the current pair of adjacent characters
9 char previousChar = s[i - 1];
10 char currentChar = s[i];
11
12 // Check if swapping would make the string lexicographically smaller
13 // and if both characters have the same parity (both even or both odd)
14 if (previousChar > currentChar && previousChar % 2 == currentChar % 2) {
15 // Perform the swap to get a smaller string
16 s[i - 1] = currentChar;
17 s[i] = previousChar;
18
19 // Only one swap is allowed, so exit after the first valid swap
20 break;
21 }
22 }
23
24 return s;
25 }
26};
27
1/**
2 * Finds the lexicographically smallest string by swapping two adjacent digits
3 * that have the same parity (both odd or both even) at most once.
4 *
5 * @param s - The input string containing only digits
6 * @returns The lexicographically smallest string after at most one swap
7 */
8function getSmallestString(s: string): string {
9 const length: number = s.length;
10 const characters: string[] = s.split('');
11
12 // Iterate through adjacent pairs of characters
13 for (let i = 1; i < length; i++) {
14 const previousChar: string = characters[i - 1];
15 const currentChar: string = characters[i];
16
17 // Check if swapping would make the string smaller
18 // and if both digits have the same parity
19 const previousDigit: number = Number(previousChar);
20 const currentDigit: number = Number(currentChar);
21
22 if (previousChar > currentChar && previousDigit % 2 === currentDigit % 2) {
23 // Perform the swap
24 characters[i - 1] = currentChar;
25 characters[i] = previousChar;
26
27 // Return immediately after the first swap
28 return characters.join('');
29 }
30 }
31
32 // No beneficial swap found, return original string
33 return s;
34}
35
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through the string once using pairwise
, checking each adjacent pair of characters. In the worst case, it examines all n-1
pairs before either finding a swap or returning the original string.
The space complexity is O(n)
, where n
is the length of the string s
. Although the pairwise
iterator and map
function create lazy iterators that don't consume extra space proportional to input size, the string slicing operations (s[:i] + s[i + 1] + s[i] + s[i + 2:]
) create a new string when a swap is performed. Since strings are immutable in Python, this concatenation creates a new string of length n
, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting Multiple Swaps Instead of Just One
A common mistake is trying to perform multiple swaps to get an even smaller string. The problem explicitly states we can swap at most once, but developers might accidentally continue swapping after finding the first beneficial swap.
Incorrect approach:
def getSmallestString(self, s: str) -> str:
chars = list(s)
for i in range(len(chars) - 1):
if (ord(chars[i]) + ord(chars[i + 1])) % 2 == 0:
if chars[i] > chars[i + 1]:
# Wrong: modifying chars and continuing the loop
chars[i], chars[i + 1] = chars[i + 1], chars[i]
return ''.join(chars)
Why it's wrong: This continues iterating and potentially swapping multiple times. For example, with input "7319", this would swap to "3719" then to "3179", performing two swaps instead of one.
Correct approach: Return immediately after the first swap:
def getSmallestString(self, s: str) -> str:
for i in range(len(s) - 1):
if (ord(s[i]) + ord(s[i + 1])) % 2 == 0:
if ord(s[i]) > ord(s[i + 1]):
# Return immediately after first swap
return s[:i] + s[i + 1] + s[i] + s[i + 2:]
return s
2. Incorrect Parity Check Using Digit Values Instead of ASCII Values
Some developers might check parity using the actual digit values rather than working with ASCII values or characters directly.
Incorrect approach:
def getSmallestString(self, s: str) -> str:
for i in range(len(s) - 1):
digit1 = int(s[i])
digit2 = int(s[i + 1])
# This works but adds unnecessary conversion overhead
if digit1 % 2 == digit2 % 2:
if digit1 > digit2:
return s[:i] + s[i + 1] + s[i] + s[i + 2:]
return s
Why it's suboptimal: While this works correctly, it performs unnecessary int conversions. The ASCII values of digits preserve the same parity as the digits themselves (e.g., '0' has ASCII 48 which is even, '1' has ASCII 49 which is odd).
Better approach: Use ASCII values or character comparison directly:
# Using ASCII values (more efficient)
if (ord(s[i]) + ord(s[i + 1])) % 2 == 0:
if s[i] > s[i + 1]: # Character comparison works for digits
return s[:i] + s[i + 1] + s[i] + s[i + 2:]
3. Not Considering the Greedy Nature of the Problem
Some might think they need to check all possible swaps and choose the best one, leading to unnecessary complexity.
Incorrect approach:
def getSmallestString(self, s: str) -> str:
best = s
for i in range(len(s) - 1):
if (ord(s[i]) + ord(s[i + 1])) % 2 == 0:
if s[i] > s[i + 1]:
swapped = s[:i] + s[i + 1] + s[i] + s[i + 2:]
if swapped < best:
best = swapped
return best
Why it's wrong: This tries to find the "best" swap among all possibilities, but since we traverse left to right and want the lexicographically smallest result, the first beneficial swap we encounter is always the optimal choice. Any swap further to the right affects less significant positions.
Correct greedy approach: Take the first beneficial swap:
for i in range(len(s) - 1):
if (ord(s[i]) + ord(s[i + 1])) % 2 == 0 and s[i] > s[i + 1]:
return s[:i] + s[i + 1] + s[i] + s[i + 2:]
return s
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
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!