3106. Lexicographically Smallest String After Operations With Constraint

Problem Description

You are given a string s consisting of lowercase English letters and an integer k.

The problem defines a distance function between two strings of the same length. For two strings s1 and s2 of length n, the distance is calculated as the sum of minimum distances between corresponding characters at each position i (from 0 to n-1).

The key point is that the alphabet is treated as cyclic - meaning after 'z' comes 'a' again. So for any two characters, you can calculate the distance in two ways:

  • Going forward in the alphabet
  • Going backward (wrapping around)

The minimum of these two distances is used.

For example:

  • distance("ab", "cd") = 4 because 'a' to 'c' has distance 2, and 'b' to 'd' has distance 2, totaling 4
  • distance("a", "z") = 1 because you can go backward from 'a' to 'z' in just 1 step

Your task is to transform the string s by changing any of its characters to any other lowercase letter (you can make multiple changes). The goal is to find the lexicographically smallest possible string t such that the distance between the original string s and the new string t is at most k.

A lexicographically smaller string means it would appear earlier in dictionary order - for instance, "abc" is lexicographically smaller than "abd".

You need to return this lexicographically smallest string that can be obtained with a total distance cost not exceeding k.

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

Intuition

To get the lexicographically smallest string, we should try to make characters as small as possible, starting from the leftmost position. This is because the leftmost characters have the highest priority in lexicographical ordering - changing the first character from 'c' to 'a' makes the string smaller than changing any later character.

The greedy approach works here: for each position from left to right, we want to change the current character to the smallest possible character we can afford with our remaining budget k.

For each character in the original string, we check all characters that are smaller than it (from 'a' up to but not including the current character). Why only smaller characters? Because we want the lexicographically smallest result, and changing to a character that's equal or larger wouldn't help achieve that goal.

When considering changing character c1 to a smaller character c2, we need to calculate the cost. Due to the cyclic nature of the alphabet, there are two possible paths:

  • Direct path: ord(c1) - ord(c2) (going backward in the alphabet)
  • Wrap-around path: 26 - ord(c1) + ord(c2) (going forward and wrapping around)

We take the minimum of these two as our actual cost d.

If we can afford this change (i.e., d <= k), we make it immediately, deduct the cost from our budget, and move to the next position. We don't need to consider other characters for this position because we've already found the smallest character we can change to within our budget.

This greedy strategy ensures we get the lexicographically smallest result because we're maximizing the "smallness" of earlier positions, which have more weight in lexicographical comparison.

Learn more about Greedy patterns.

Solution Implementation

1class Solution: 2 def getSmallestString(self, s: str, k: int) -> str: 3 """ 4 Returns the lexicographically smallest string by replacing characters in s 5 with a total distance cost not exceeding k. 6 7 Args: 8 s: Input string to modify 9 k: Maximum total distance allowed for all character replacements 10 11 Returns: 12 The lexicographically smallest possible string 13 """ 14 # Import ascii_lowercase for iterating through 'a' to 'z' 15 from string import ascii_lowercase 16 17 # Convert string to list for in-place modifications 18 char_list = list(s) 19 20 # Process each character position from left to right 21 for i, original_char in enumerate(s): 22 # Try each possible replacement character from 'a' to 'z' 23 for replacement_char in ascii_lowercase: 24 # Skip if replacement is not lexicographically smaller 25 if replacement_char >= original_char: 26 break 27 28 # Calculate minimum distance between characters (considering wrap-around) 29 # Distance can be direct (c1 - c2) or wrap-around (26 - c1 + c2) 30 direct_distance = ord(original_char) - ord(replacement_char) 31 wrap_distance = 26 - ord(original_char) + ord(replacement_char) 32 min_distance = min(direct_distance, wrap_distance) 33 34 # If we have enough budget, make the replacement 35 if min_distance <= k: 36 char_list[i] = replacement_char 37 k -= min_distance 38 break 39 40 # Convert list back to string and return 41 return "".join(char_list) 42
1class Solution { 2 public String getSmallestString(String s, int k) { 3 // Convert string to character array for in-place modification 4 char[] characters = s.toCharArray(); 5 6 // Process each character from left to right 7 for (int i = 0; i < characters.length; ++i) { 8 char originalChar = characters[i]; 9 10 // Try each character from 'a' up to (but not including) the original character 11 for (char targetChar = 'a'; targetChar < originalChar; ++targetChar) { 12 // Calculate the minimum distance between two characters 13 // Distance can be either direct (c1 - c2) or wrap around (26 - c1 + c2) 14 int distance = Math.min( 15 originalChar - targetChar, // Direct distance 16 26 - originalChar + targetChar // Wrap-around distance 17 ); 18 19 // If we have enough operations remaining to make this change 20 if (distance <= k) { 21 // Replace the character with the smaller one 22 characters[i] = targetChar; 23 // Deduct the used operations from remaining count 24 k -= distance; 25 // Move to the next character position 26 break; 27 } 28 } 29 } 30 31 // Convert the modified character array back to string 32 return new String(characters); 33 } 34} 35
1class Solution { 2public: 3 string getSmallestString(string s, int k) { 4 // Iterate through each character in the string 5 for (int i = 0; i < s.size(); ++i) { 6 char currentChar = s[i]; 7 8 // Try to replace current character with a lexicographically smaller one 9 // Starting from 'a' up to (but not including) the current character 10 for (char targetChar = 'a'; targetChar < currentChar; ++targetChar) { 11 // Calculate the minimum distance to change currentChar to targetChar 12 // Two possible paths: direct distance or wrap around through 'z' to 'a' 13 int directDistance = currentChar - targetChar; 14 int wrapAroundDistance = 26 - currentChar + targetChar; 15 int minDistance = min(directDistance, wrapAroundDistance); 16 17 // If we have enough operations remaining, make the replacement 18 if (minDistance <= k) { 19 s[i] = targetChar; 20 k -= minDistance; 21 break; // Move to the next character in the string 22 } 23 } 24 } 25 26 return s; 27 } 28}; 29
1/** 2 * Finds the lexicographically smallest string by replacing characters in the input string 3 * with a total distance of at most k 4 * @param s - The input string to modify 5 * @param k - The maximum total distance allowed for all character replacements 6 * @returns The lexicographically smallest possible string 7 */ 8function getSmallestString(s: string, k: number): string { 9 // Convert string to character array for easier manipulation 10 const charArray: string[] = s.split(''); 11 12 // Iterate through each character position in the string 13 for (let i = 0; i < s.length; ++i) { 14 // Get ASCII code of current character 15 const currentCharCode = s[i].charCodeAt(0); 16 17 // Try each possible lowercase letter starting from 'a' (ASCII 97) 18 // Stop before reaching the current character 19 for (let targetCharCode = 97; targetCharCode < currentCharCode; ++targetCharCode) { 20 // Calculate the minimum distance to transform current character to target 21 // Distance can be calculated in two ways: 22 // 1. Direct distance: currentCharCode - targetCharCode 23 // 2. Wrap-around distance: go backward from current char to 'z', then from 'a' to target 24 const directDistance = currentCharCode - targetCharCode; 25 const wrapAroundDistance = 26 - currentCharCode + targetCharCode; 26 const minDistance = Math.min(directDistance, wrapAroundDistance); 27 28 // If we have enough remaining k to make this transformation 29 if (minDistance <= k) { 30 // Replace the character with the smaller one 31 charArray[i] = String.fromCharCode(targetCharCode); 32 // Decrease k by the distance used 33 k -= minDistance; 34 // Move to the next character position 35 break; 36 } 37 } 38 } 39 40 // Join the character array back into a string 41 return charArray.join(''); 42} 43

Solution Approach

The implementation follows a greedy traversal approach where we process each character position from left to right:

  1. Convert string to list: First, we convert the input string s to a list cs = list(s) to make it mutable, since strings in Python are immutable and we need to modify characters.

  2. Iterate through each position: We use enumerate(s) to get both the index i and character c1 at each position.

  3. Try smaller characters: For each position, we iterate through all lowercase letters using ascii_lowercase (which gives 'a' to 'z'). We only consider characters c2 that are smaller than the current character c1:

    • The condition if c2 >= c1: break stops the iteration once we reach characters that are not smaller than c1
  4. Calculate minimum distance: For each candidate character c2 that is smaller than c1, we calculate the minimum cyclic distance:

    d = min(ord(c1) - ord(c2), 26 - ord(c1) + ord(c2))
    • ord(c1) - ord(c2): Direct backward distance
    • 26 - ord(c1) + ord(c2): Forward distance with wrap-around
  5. Make the change if affordable: If the distance d is within our budget (d <= k):

    • Update the character at position i: cs[i] = c2
    • Deduct the cost from our budget: k -= d
    • Break from the inner loop to move to the next position
  6. Return the result: After processing all positions, we join the list back into a string using "".join(cs) and return it.

The algorithm ensures optimality because it greedily makes each position as small as possible from left to right, using the available budget efficiently. The time complexity is O(26n) where n is the length of the string, since for each character we potentially check up to 26 lowercase letters.

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 = "zbbz" and k = 3.

Initial state: cs = ['z', 'b', 'b', 'z'], k = 3

Position 0 (character 'z'):

  • We try to change 'z' to smaller characters starting from 'a'
  • For 'z' → 'a':
    • Direct backward distance: ord('z') - ord('a') = 25
    • Forward wrap-around distance: 26 - ord('z') + ord('a') = 26 - 25 + 0 = 1
    • Minimum distance: min(25, 1) = 1
    • Since 1 ≤ 3, we can make this change!
  • Update: cs[0] = 'a', k = 3 - 1 = 2
  • Result so far: ['a', 'b', 'b', 'z']

Position 1 (character 'b'):

  • We try to change 'b' to 'a' (the only smaller character)
  • For 'b' → 'a':
    • Direct backward distance: ord('b') - ord('a') = 1
    • Forward wrap-around distance: 26 - ord('b') + ord('a') = 26 - 1 + 0 = 25
    • Minimum distance: min(1, 25) = 1
    • Since 1 ≤ 2, we can make this change!
  • Update: cs[1] = 'a', k = 2 - 1 = 1
  • Result so far: ['a', 'a', 'b', 'z']

Position 2 (character 'b'):

  • We try to change 'b' to 'a'
  • For 'b' → 'a':
    • Minimum distance: 1 (as calculated above)
    • Since 1 ≤ 1, we can make this change!
  • Update: cs[2] = 'a', k = 1 - 1 = 0
  • Result so far: ['a', 'a', 'a', 'z']

Position 3 (character 'z'):

  • We have k = 0, so we cannot make any more changes
  • The character remains 'z'

Final result: "aaaz"

The algorithm successfully transformed "zbbz" into "aaaz" using exactly 3 units of distance, creating the lexicographically smallest possible string within the budget.

Time and Space Complexity

Time Complexity: O(n × |Σ|) where n is the length of the string s and |Σ| is the size of the character set (at most 26 for lowercase English letters).

The algorithm iterates through each character in the string s (outer loop runs n times). For each character position, it iterates through the lowercase alphabet checking for potential replacements (inner loop runs at most |Σ| = 26 times). The inner loop breaks as soon as it finds a valid replacement or when c2 >= c1, but in the worst case, it may check all 26 letters. All operations inside the nested loops (comparisons, arithmetic operations, and character replacement) take O(1) time.

Space Complexity: O(n) where n is the length of the string s.

The algorithm creates a list cs from the input string s, which requires O(n) space to store all characters. The variable ascii_lowercase is a constant string of 26 characters (O(1) space), and other variables like i, c1, c2, d, and k use O(1) space. The final string construction with "".join(cs) creates a new string of length n, but this is typically considered part of the output rather than auxiliary space.

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

Common Pitfalls

Pitfall 1: Not Considering All Possible Replacements

The Issue: The current implementation only tries to replace characters with lexicographically smaller ones. However, this misses opportunities where we might need to replace a character with a lexicographically larger one at an earlier position to save budget for more impactful changes at later positions.

Example: Consider s = "zzzzzz" with k = 3. The optimal answer is "aaazzz" (changing first 3 'z's to 'a's, each costing 1 due to wrap-around). However, if we had a string like "lmnxyz" with limited k, we might want to keep early characters unchanged or even change them to slightly larger characters if it helps us make more significant improvements to later positions.

The Real Issue: Actually, the main pitfall is that the code stops looking for replacements once it finds a character >= the original. This means it won't consider changing a character to itself (distance 0) or to a larger character that might have a small cyclic distance.

Corrected Solution:

class Solution:  def getSmallestString(self, s: str, k: int) -> str:  from string import ascii_lowercase   char_list = list(s)   for i, original_char in enumerate(s):  best_char = original_char  best_cost = 0   # Try all possible characters, not just smaller ones  for replacement_char in ascii_lowercase:  # Calculate minimum cyclic distance  diff = abs(ord(original_char) - ord(replacement_char))  min_distance = min(diff, 26 - diff)   # If this replacement is affordable and better  if min_distance <= k:  # Choose the lexicographically smallest character within budget  if replacement_char < best_char:  best_char = replacement_char  best_cost = min_distance  # Break early if we found 'a' (can't do better)  if best_char == 'a':  break   # Apply the best replacement found  if best_char != original_char:  char_list[i] = best_char  k -= best_cost   return "".join(char_list)

Pitfall 2: Incorrect Distance Calculation for Wrap-Around

The Issue: When calculating wrap-around distance, the formula 26 - ord(c1) + ord(c2) assumes going from c1 forward to c2. This works when c2 < c1, but if we're considering all possible replacements, we need a more general formula.

Correct Approach: Use min(abs(ord(c1) - ord(c2)), 26 - abs(ord(c1) - ord(c2))) which correctly handles all cases regardless of which character is larger.

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

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More